package puzzle; import com.google.gson.Gson; import lombok.Getter; import lombok.val; import precomp.Neighbors9x8; import precomp.Neighbors9x8.nbrs_16; import precomp.Neighbors9x8.nbrs_8; import precomp.Neighbors9x8.rci; import puzzle.Export.Bit; import puzzle.Export.Bit1029; import puzzle.Export.Gridded; import puzzle.Export.Strings; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Locale; import java.util.function.IntConsumer; /** * SwedishGenerator.java * * Usage: * javac SwedishGenerator.java * java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt] */ @SuppressWarnings("ALL") public record SwedishGenerator(Rng rng) { record CandidateInfo(int[] indices, int count) { } //@formatter:off @FunctionalInterface interface SlotVisitor { void visit(int key, long packedPos, int len); } //@formatter:on static final int LOG_EVERY_MS = 200; static final int BAR_LEN = 22; static final int C = Config.PUZZLE_COLS; static final double CROSS_R = (C - 1) / 2.0; static final int R = Config.PUZZLE_ROWS; static final double CROSS_C = (R - 1) / 2.0; static final int SIZE = C * R;// ~18 static final double SIZED = (double) SIZE;// ~18 static final int TARGET_CLUES = SIZE >> 2; static final int MAX_WORD_LENGTH = C <= R ? C : R; static final int MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1; static final int MIN_LEN = Config.MIN_LEN; static final int CLUE_SIZE = Config.CLUE_SIZE; static final int SIMPLICITY_DEFAULT_SCORE = 2; static final int MAX_TRIES_PER_SLOT = Config.MAX_TRIES_PER_SLOT; static final char C_DASH = '\0'; static final byte _1 = 49, _9 = 57, A = 65, Z = 90, DASH = (byte) C_DASH; static final ThreadLocal CTX = ThreadLocal.withInitial(Context::new); static boolean isLetter(byte b) { return (b & 64) != 0; } static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); } record Pick(Slot slot, CandidateInfo info, boolean done) { } // Directions for '1'..'6' static final nbrs_16[] OFFSETS = Neighbors9x8.OFFSETS; static final nbrs_8[] nbrs8 = Neighbors9x8.nbrs8; static final nbrs_8[] nbrs4 = Neighbors9x8.nbrs4; static final rci[] IT = Neighbors9x8.IT; static final long[] INBR8_PACKEDT = Neighbors9x8.NBR8_PACKED; static final int[][] MUTATE_RI = new int[SIZE][625]; static { for (int i = 0; i < SIZE; i++) { int k = 0; for (int dr1 = -2; dr1 <= 2; dr1++) for (int dr2 = -2; dr2 <= 2; dr2++) for (int dc1 = -2; dc1 <= 2; dc1++) for (int dc2 = -2; dc2 <= 2; dc2++) MUTATE_RI[i][k++] = Grid.offset(clamp(Grid.r(i) + dr1 + dr2, 0, R - 1), clamp(Grid.c(i) + dc1 + dc2, 0, C - 1)); } } static final Pick PICK_DONE = new Pick(null, null, true); static final Pick PICK_NOT_DONE = new Pick(null, null, false); public static final class FillStats { public long nodes; public long backtracks; public double seconds; public int lastMRV; public double simplicity; } public static record FillResult(boolean ok, Gridded grid, HashMap clueMap, FillStats stats) { public FillResult { if (ok) { clueMap.forEach((k, v) -> stats.simplicity += v.simpel); stats.simplicity = clueMap.isEmpty() ? 0 : stats.simplicity / clueMap.size(); } } } static record Context(int[] covH, int[] covV, int[] cellCount, int[] stack, Bit seen, byte[] pattern, IntList[] intListBuffer, int[] undo, int[] inter1, int[] inter2) { public Context() { this(new int[SIZE], new int[SIZE], new int[SIZE], new int[SIZE], new Bit(), new byte[MAX_WORD_LENGTH], new IntList[MAX_WORD_LENGTH], new int[2048], new int[160000], new int[160000]); } void setPatter(byte[] chars) { System.arraycopy(chars, 0, this.pattern, 0, chars.length); } } static final class Rng { @Getter private int x; Rng(int seed) { var s = seed; if (s == 0) s = 1; this.x = s; } int nextU32() { var y = x; y ^= (y << 13); y ^= (y >>> 17); y ^= (y << 5); x = y; return y; } byte randbyte(int min, int max) { var u = (nextU32() & 0xFFFFFFFFL); var range = (long) max - (long) min + 1L; return (byte) (min + (u % range)); } int randint(int min, int max) { var u = (nextU32() & 0xFFFFFFFFL); var range = (long) max - (long) min + 1L; return (int) (min + (u % range)); } double nextFloat() { return (nextU32() & 0xFFFFFFFFL) / 4294967295.0; } } record Grid(byte[] g, long[] bo) { public Grid(byte[] g) { this(g, new long[2]); } static Grid createEmpty() { return new Grid(new byte[SIZE], new long[2]); } int digitAt(int index) { return g[index] - 48; } public static int r(int offset) { return offset & 7; } public static int c(int offset) { return offset >>> 3; } static int offset(int r, int c) { return r | (c << 3); } Grid deepCopyGrid() { return new Grid(g.clone(), bo.clone()); } public byte byteAt(int pos) { return g[pos]; } void setByteAt(int idx, byte ch) { g[idx] = ch; } void setClue(int idx, byte ch) { g[idx] = ch; if (idx < 64) bo[0] |= (1L << idx); else bo[1] |= (1L << (idx & 63)); } void clear(int idx) { g[idx] = DASH; } void clearClue(int idx) { g[idx] = DASH; if (idx < 64) bo[0] &= ~(1L << idx); else bo[1] &= ~(1L << (idx & 63)); } static boolean isDigit(byte b) { return (b & 48) == 48; } boolean isDigitAt(int index) { return isDigit(g[index]); } boolean isClue(int index) { if (index < 64) { return ((bo[0] >> index) & 1L) != 0; } return ((bo[1] >> (index & 63)) & 1L) != 0; } boolean notClue(int index) { if (index < 64) { return ((bo[0] >> index) & 1L) == 0L; } return ((bo[1] >> (index & 63)) & 1L) == 0L; } boolean clueless(int idx) { if (idx < 64) { val test = (1L << idx); if ((test & bo[0]) != 0L) { g[idx] = DASH; bo[0] &= ~test; return true; } return false; } else { val test = (1L << (idx & 63)); if ((test & bo[1]) != 0L) { g[idx] = DASH; bo[1] &= ~test; return true; } return false; } } static boolean isLetter(byte b) { return (b & 64) != 0; } public boolean isLetterSet(int idx) { return isLetter(g[idx]); } static boolean notDigit(byte b) { return (b & 48) != 48; } public boolean isLetterAt(int index) { return notDigit(g[index]); } public double similarity(Grid b) { var same = 0; for (int i = 0; i < SIZE; i++) if (g[i] == b.g[i]) same++; return same / SIZED; } int clueCount() { return Long.bitCount(bo[0]) + Long.bitCount(bo[1]); } void forEachSetBit71(IntConsumer consumer) { for (var lo = bo[0]; lo != 0; lo &= lo - 1) consumer.accept(Long.numberOfTrailingZeros(lo)); for (var hi = bo[1]; hi != 0; hi &= hi - 1) consumer.accept(64 + Long.numberOfTrailingZeros(hi)); } void forEachSlot(SlotVisitor visitor) { for (var lo = bo[0]; lo != 0; lo &= lo - 1) processSlot(this, visitor, Long.numberOfTrailingZeros(lo)); for (var hi = bo[1]; hi != 0; hi &= hi - 1) processSlot(this, visitor, 64 + Long.numberOfTrailingZeros(hi)); } } static final class IntList { int[] a = new int[8]; int n = 0; void add(int v) { if (n >= a.length) a = Arrays.copyOf(a, a.length * 2); a[n++] = v; } int size() { return n; } int[] data() { return a; } } static record DictEntry(ArrayList words, IntList[][] pos) { public DictEntry(int L) { this(new ArrayList<>(), new IntList[L][26]); for (var i = 0; i < L; i++) for (var j = 0; j < 26; j++) pos[i][j] = new IntList(); } } public static record Lemma(int index, byte[] word, int simpel, String[] clue) { static int LEMMA_COUNTER = 0; public Lemma(int index, String word, int simpel, String[] clu) { this(index, word.getBytes(StandardCharsets.US_ASCII), simpel, clu); } public Lemma(String word, int simpel, String clue) { this(LEMMA_COUNTER++, word, simpel, new String[]{ clue }); } byte byteAt(int idx) { return word[idx]; } @Override public int hashCode() { return index; } @Override public boolean equals(Object o) { return (o == this) || (o instanceof Lemma l && l.index == index); } } public static record Dict( DictEntry[] index, int length) { static final Gson GSON = new Gson(); public Dict(Lemma[] wordz) { var index = new DictEntry[MAX_WORD_LENGTH_PLUS_ONE]; Arrays.setAll(index, i -> new DictEntry(i)); for (var lemma : wordz) { var L = lemma.word.length; var entry = index[L]; var idx = entry.words.size(); entry.words.add(lemma); for (var i = 0; i < L; i++) { var letter = lemma.byteAt(i) - 'A'; if (letter < 0 || letter >= 26) throw new RuntimeException("Illegal letter: " + letter + " in word " + lemma); entry.pos[i][letter].add(idx); } } for (int i = MIN_LEN; i < index.length; i++) { var len = index[i].words.size(); if (len <= 0) { throw new RuntimeException("No words for length " + i); } } this(index, Arrays.stream(index).mapToInt(i -> i.words.size()).sum()); } static Dict loadDict(String wordsPath) { String raw; try { raw = Files.readString(Path.of(wordsPath), StandardCharsets.UTF_8); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("Failed to load dictionary from " + wordsPath, e); } var map = new ArrayList(); var first = true; for (var line : raw.split("\\R")) { if (line.isBlank()) { System.err.println("Empty line: " + line); continue; } var parts = line.split(",", 5); var id = Integer.parseInt(parts[0].trim()); var word = parts[1].trim(); if (first && word.equalsIgnoreCase("WOORD")) { first = false; continue; } first = false; var s = word.toUpperCase(Locale.ROOT); if (!s.matches("^[A-Z]{2,8}$")) { System.err.println("Invalid word: " + line); continue; } // CSV has level 1-10. llmScores use 10-level. int score = Integer.parseInt(parts[2].trim()); if (score < 1) { if (Main.VERBOSE) System.err.println("Word too complex: " + line); continue; } int simpel = Integer.parseInt(parts[3].trim()); var rawClue = parts[4].trim(); if (rawClue.startsWith("\"") && rawClue.endsWith("\"")) { rawClue = rawClue.substring(1, rawClue.length() - 1).replace("\"\"", "\""); } map.add(new Lemma(id, s, simpel, GSON.fromJson(rawClue, String[].class))); } return new Dict(map.toArray(Lemma[]::new)); } } static int intersectSorted(int[] a, int aLen, int[] b, int bLen, int[] out) { int i = 0, j = 0, k = 0, x, y; while (i < aLen && j < bLen) { x = a[i]; y = b[j]; if (x == y) { out[k++] = x; i++; j++; } else if (x < y) i++; else j++; } return k; } static record Slot(int key, long packedPos) { static Slot from(int key, long packedPos, int len) { return new Slot(key, packedPos | ((long) len << 56)); } void undoPlace(Grid grid, int mask) { for (int i = 0, len = len(); i < len; i++) if ((mask & (1L << i)) != 0) grid.clear(pos(i)); } public int len() { return (int) (packedPos >>> 56); } public int clueR() { return Grid.r((key >>> 4)); } public int clueIndex() { return key >>> 4; } public int clueC() { return Grid.c((key >>> 4)); } public int dir() { return key & 15; } public boolean horiz() { return horiz(key); } public int pos(int i) { return offset(packedPos, i); } public static boolean horiz(int key) { return ((key & 15) & 1) == 0; } public static int offset(long packedPos, int i) { return (int) ((packedPos >> (i * 7)) & 127); } } private static void processSlot(Grid grid, SlotVisitor visitor, int idx) { var d = grid.digitAt(idx); var packed = OFFSETS[d].path()[idx]; long packedPos = 0; int k = 0; for (int n = (int) (packed >>> 56), iidx; k < n && k < MAX_WORD_LENGTH; k++) { iidx = (int) ((packed >>> (k * 7)) & 0x7F); if (grid.isClue(iidx)) break; packedPos |= (long) iidx << (k * 7); } if (k > 0) { visitor.visit((idx << 4) | d, packedPos, k); } } static ArrayList extractSlots(Grid grid) { var slots = new ArrayList(32); grid.forEachSlot((key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len))); return slots; } static boolean hasRoomForClue(Grid grid, long packed) { for (int n = (int) (packed >>> 56), k = 0; k < n && k < MAX_WORD_LENGTH; ) { if (grid.isClue((int) ((packed >>> (k * 7)) & 0x7F))) break; if (++k >= MIN_LEN) return true; } return false; } long maskFitness(Grid grid) { var clueCount = grid.clueCount(); long penalty = ((long) Math.abs(clueCount - TARGET_CLUES)) << 3; var ctx = CTX.get(); var covH = ctx.covH; var covV = ctx.covV; Arrays.fill(covH, 0, SIZE, 0); Arrays.fill(covV, 0, SIZE, 0); boolean hasSlots = false; long lo_cl = grid.bo[0], hi_cl = grid.bo[1]; while (lo_cl != 0L) { int clueIdx = Long.numberOfTrailingZeros(lo_cl); lo_cl &= (lo_cl - 1); var d = grid.digitAt(clueIdx); var nbrs16 = OFFSETS[d]; long packed = nbrs16.path()[clueIdx]; int n = (int) (packed >>> 56), k, idx; var horiz = Slot.horiz(d) ? covH : covV; for (k = 0; k < n && k < MAX_WORD_LENGTH; k++) { idx = (int) ((packed >>> (k * 7)) & 0x7F); if (grid.isClue(idx)) break; horiz[idx] += 1; } if (k == 0) continue; hasSlots = true; if (k < MIN_LEN) penalty += 8000; } while (hi_cl != 0L) { int clueIdx = 64 + Long.numberOfTrailingZeros(hi_cl); hi_cl &= (hi_cl - 1); var d = grid.digitAt(clueIdx); var nbrs16 = OFFSETS[d]; long packed = nbrs16.path()[clueIdx]; int n = (int) (packed >>> 56), k, idx; var horiz = Slot.horiz(d) ? covH : covV; for (k = 0; k < n && k < MAX_WORD_LENGTH; k++) { idx = (int) ((packed >>> (k * 7)) & 0x7F); if (grid.isClue(idx)) break; horiz[idx] += 1; } if (k == 0) continue; hasSlots = true; if (k < MIN_LEN) penalty += 8000; } if (!hasSlots) return 1_000_000_000L; // clue clustering (8-connected) var seen = ctx.seen; seen.clear(); var stack = ctx.stack; for (var lo = grid.bo[0]; lo != 0; lo &= lo - 1) penalty += clueStackPenalty(seen, stack, grid, Long.numberOfTrailingZeros(lo)); for (var hi = grid.bo[1]; hi != 0; hi &= hi - 1) penalty += clueStackPenalty(seen, stack, grid, 64 + Long.numberOfTrailingZeros(hi)); // dead-end-ish letter cell (3+ walls) int walls, wc, wr; /* for (var rci : IT) { if (grid.isDigitAt(rci.i())) continue; walls = 0; for (var d : nbrs4) { wr = rci.r() + d.r(); wc = rci.c() + d.c(); if (wr < 0 || wr >= R || wc < 0 || wc >= C || grid.isDigitAt(Grid.offset(wr, wc))) walls++; } if (walls >= 3) penalty[0] += 400; }*/ int h, v; for (var rci : IT) { if (grid.isClue(rci.i())) continue; if ((4 - rci.nbrCount()) + Long.bitCount(rci.n1() & grid.bo[0]) + Long.bitCount(rci.n2() & grid.bo[1]) >= 3) penalty += 400; h = covH[rci.i()]; v = covV[rci.i()]; if (h == 0 && v == 0) penalty += 1500; else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty += 200; else penalty += 600; } return penalty; } static long clueStackPenalty(Bit seen, int[] stack, Grid grid, int clueIdx) { if (seen.get(clueIdx)) return 0; var sp = 0; stack[sp++] = clueIdx; seen.set(clueIdx); var size = 0; while (sp > 0) { var p = stack[--sp]; size++; long packed = Neighbors9x8.NBR8_PACKED[p]; int n = (int) (packed >>> 56); for (int k = 0; k < n; k++) { int nidx = (int) ((packed >>> (k * 7)) & 0x7F); if (seen.get(nidx) || grid.notClue(nidx)) continue; seen.set(nidx); stack[sp++] = nidx; } } return (size >= 2) ? (size - 1L) * 120L : 0; } Grid randomMask() { var g = Grid.createEmpty(); for (int placed = 0, guard = 0, idx; placed < TARGET_CLUES && guard < 4000; guard++) { idx = rng.randint(0, SIZE - 1); if (g.isClue(idx)) continue; var d = OFFSETS[rng.randbyte(1, 4)]; if (hasRoomForClue(g, d.path()[idx])) { g.setClue(idx, d.dbyte()); placed++; } } return g; } Grid mutate(Grid grid) { var g = grid.deepCopyGrid(); var centerIdx = rng.randint(0, SIZE - 1); var steps = 4; for (var k = 0; k < steps; k++) { var ri = MUTATE_RI[centerIdx][rng.randint(0, 624)]; if (!g.clueless(ri)) { var d = OFFSETS[rng.randint(1, 4)]; if (hasRoomForClue(g, d.path()[ri])) g.setClue(ri, d.dbyte()); } } return g; } Grid crossover(Grid a, Grid b) { var out = a.deepCopyGrid(); var theta = rng.nextFloat() * Math.PI; var nc = Math.cos(theta); var nr = Math.sin(theta); long bo0 = out.bo[0], bo1 = out.bo[1]; for (var rci : IT) { int i = rci.i(); if ((rci.r() - CROSS_C) * nc + (rci.c() - CROSS_R) * nr < 0) { byte ch = b.g[i]; if (out.g[i] != ch) { out.g[i] = ch; if (Grid.isDigit(ch)) { if (i < 64) bo0 |= (1L << i); else bo1 |= (1L << (i & 63)); } else { if (i < 64) bo0 &= ~(1L << i); else bo1 &= ~(1L << (i & 63)); } } } } out.bo[0] = bo0; out.bo[1] = bo1; for (var lo = out.bo[0]; lo != 0L; lo &= lo - 1L) clearClues(out, Long.numberOfTrailingZeros(lo)); for (var hi = out.bo[1]; hi != 0L; hi &= hi - 1L) clearClues(out, 64 + Long.numberOfTrailingZeros(hi)); return out; } public static void clearClues(Grid out, int idx) { if (!hasRoomForClue(out, OFFSETS[out.digitAt(idx)].path()[idx])) out.clearClue(idx); } Grid hillclimb(Grid start, int limit) { var best = start; var bestF = maskFitness(best); var fails = 0; while (fails < limit) { var cand = mutate(best); var f = maskFitness(cand); if (f < bestF) { best = cand; bestF = f; fails = 0; } else { fails++; } } return best; } public Grid generateMask(int popSize, int gens) { class GridAndFit { Grid grid; Long fite; GridAndFit(Grid grid) { this.grid = grid; } long fit() { if (fite == null) this.fite = maskFitness(grid); return this.fite; } } if (Main.VERBOSE) System.out.println("generateMask init pop: " + popSize); var pop = new ArrayList(); for (var i = 0; i < popSize; i++) { pop.add(new GridAndFit(hillclimb(randomMask(), 180))); } for (var gen = 0; gen < gens; gen++) { if (Thread.currentThread().isInterrupted()) break; var children = new ArrayList(); var pairs = Math.max(popSize, (int) Math.floor(popSize * 1.5)); for (var k = 0; k < pairs; k++) { var p1 = pop.get(rng.randint(0, pop.size() - 1)); var p2 = pop.get(rng.randint(0, pop.size() - 1)); var child = crossover(p1.grid, p2.grid); children.add(new GridAndFit(hillclimb(child, 70))); } pop.addAll(children); pop.sort(Comparator.comparingLong(GridAndFit::fit)); var next = new ArrayList(); for (var cand : pop) { if (next.size() >= popSize) break; var ok = true; for (var kept : next) { if (cand.grid.similarity(kept.grid) > 0.92) { ok = false; break; } } if (ok) next.add(cand); } pop = next; if (Main.VERBOSE && gen % 10 == 0) { var bestF = pop.get(0).fit(); System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + bestF); } } pop.sort(Comparator.comparingLong(GridAndFit::fit)); return pop.get(0).grid; } static void patternForSlot(Grid grid, Slot s, byte[] pat) { for (int i = 0, len = s.len(); i < len; i++) { var ch = grid.byteAt(s.pos(i)); pat[i] = isLetter(ch) ? ch : DASH; } } static int slotScore(int[] count, Slot s) { int cross = 0, len = s.len(); for (int i = 0; i < len; i++) cross += (count[s.pos(i)] - 1); return cross * 10 + len; } static boolean placeWord(Grid grid, Slot s, Lemma w, int[] undoBuffer, int offset) { int mask = 0; byte cur, ch; for (int i = 0, leng = s.len(), idx; i < leng; i++) { idx = s.pos(i); cur = grid.byteAt(idx); ch = w.byteAt(i); if (cur == DASH) { mask |= (1 << i); grid.setByteAt(idx, ch); } else if (cur != ch) { for (var j = 0; j < i; j++) { if ((mask & (1 << j)) != 0) { grid.clear(s.pos(j)); } } return false; } } undoBuffer[offset] = mask; return true; } static CandidateInfo candidateInfoForPattern(Context ctx, DictEntry entry, int len) { var pattern = ctx.pattern; var listBuffer = ctx.intListBuffer; var listCount = 0; for (var i = 0; i < len; i++) { var ch = pattern[i]; if (isLetter(ch)) { listBuffer[listCount++] = entry.pos[i][ch - 'A']; } } if (listCount == 0) { return new CandidateInfo(null, entry.words.size()); } // Sort constraints by size to optimize intersection for (int i = 0; i < listCount - 1; i++) { for (int j = i + 1; j < listCount; j++) { if (listBuffer[j].size() < listBuffer[i].size()) { var tmp = listBuffer[i]; listBuffer[i] = listBuffer[j]; listBuffer[j] = tmp; } } } var cur = listBuffer[0].data(); var curLen = listBuffer[0].size(); if (listCount == 1) return new CandidateInfo(cur, curLen); int[] b1 = ctx.inter1; int[] b2 = ctx.inter2; int[] in = cur; int[] out = b1; for (var k = 1; k < listCount; k++) { var nxt = listBuffer[k]; curLen = intersectSorted(in, curLen, nxt.data(), nxt.size(), out); in = out; out = (out == b1) ? b2 : b1; if (curLen == 0) break; } return new CandidateInfo(in, curLen); } public FillResult fillMask(Grid mask, DictEntry[] dictIndex, int timeLimitMs) { val multiThreaded = Thread.currentThread().getName().contains("pool"); val grid = mask.deepCopyGrid(); val used = new Bit1029(); val assigned = new HashMap(); val ctx = CTX.get(); val count = ctx.cellCount; Arrays.fill(count, 0, SIZE, 0); val slots = extractSlots(grid); for (var s : slots) for (int i = 0, len = s.len(); i < len; i++) count[s.pos(i)]++; val t0 = System.currentTimeMillis(); val stats = new FillStats(); val TOTAL = slots.size(); class Solver { long lastLog = t0; void renderProgress() { if (!Main.VERBOSE || multiThreaded) return; var now = System.currentTimeMillis(); if ((now - lastLog) < LOG_EVERY_MS) return; lastLog = (now); var done = assigned.size(); var pct = (TOTAL == 0) ? 100 : (int) Math.floor((done / (double) TOTAL) * 100); var filled = Math.min(BAR_LEN, (int) Math.floor((pct / 100.0) * BAR_LEN)); var bar = "[" + "#".repeat(filled) + "-".repeat(BAR_LEN - filled) + "]"; var elapsed = String.format(Locale.ROOT, "%.1fs", (now - t0) / 1000.0); var msg = String.format( Locale.ROOT, "%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s", bar, done, TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, elapsed ); System.out.print("\r" + Strings.padRight(msg, 120)); System.out.flush(); } Pick chooseMRV() { Slot best = null; CandidateInfo bestInfo = null; int bestSlot = -1; for (var s : slots) { if (assigned.containsKey(s.key())) continue; var entry = dictIndex[s.len()]; if (entry == null) return PICK_NOT_DONE; patternForSlot(grid, s, ctx.pattern); var info = candidateInfoForPattern(ctx, entry, s.len()); if (info.count == 0) return PICK_NOT_DONE; var slotScore = -1; if (best == null || info.count < bestInfo.count || (info.count == bestInfo.count && (slotScore = slotScore(count, s)) > bestSlot)) { best = s; bestSlot = (slotScore != -1) ? slotScore : slotScore(count, s); if (info.indices != null && (info.indices == ctx.inter1 || info.indices == ctx.inter2)) { bestInfo = new CandidateInfo(Arrays.copyOf(info.indices, info.count), info.count); } else { bestInfo = info; } if (info.count <= 1) break; } } if (best == null) { return PICK_DONE; } else { return new Pick(best, bestInfo, false); } } boolean backtrack(int depth) { if (Thread.currentThread().isInterrupted()) return false; stats.nodes++; if (timeLimitMs > 0 && (System.currentTimeMillis() - t0) > timeLimitMs) return false; var pick = chooseMRV(); if (pick.done) return true; if (pick.slot == null) { stats.backtracks++; return false; } val info = pick.info; stats.lastMRV = info.count; renderProgress(); var s = pick.slot; var k = s.key(); int patLen = s.len(); var entry = dictIndex[patLen]; var pat = new byte[patLen]; patternForSlot(grid, s, pat); if (info.indices != null && info.indices.length > 0) { var idxs = info.indices; var L = idxs.length; var tries = Math.min(MAX_TRIES_PER_SLOT, L); for (var t = 0; t < tries; t++) { double r = rng.nextFloat(); int idxInArray = (int) (r * r * r * L); var idx = idxs[idxInArray]; var w = entry.words.get(idx); if (used.get(w.index())) continue; boolean match = true; for (var i = 0; i < patLen; i++) { if (pat[i] != DASH && pat[i] != w.byteAt(i)) { match = false; break; } } if (!match || !placeWord(grid, s, w, ctx.undo, depth)) continue; used.set(w.index()); assigned.put(k, w); if (backtrack(depth + 1)) return true; assigned.remove(k); used.clear(w.index); s.undoPlace(grid, ctx.undo[depth]); } stats.backtracks++; return false; } var N = entry.words.size(); if (N == 0) { stats.backtracks++; return false; } var tries = Math.min(MAX_TRIES_PER_SLOT, N); for (var t = 0; t < tries; t++) { double r = rng.nextFloat(); int idxInArray = (int) (r * r * r * N); var w = entry.words.get(idxInArray); if (used.get(w.index())) continue; boolean match = true; for (var i = 0; i < patLen; i++) { if (pat[i] != DASH && pat[i] != w.byteAt(i)) { match = false; break; } } if (!match || !placeWord(grid, s, w, ctx.undo, depth)) continue; used.set(w.index()); assigned.put(k, w); if (backtrack(depth + 1)) return true; assigned.remove(k); used.clear(w.index); s.undoPlace(grid, ctx.undo[depth]); } stats.backtracks++; return false; } } // initial render (same feel) var solver = new Solver(); solver.renderProgress(); var ok = solver.backtrack(0); // final progress line if (!multiThreaded) { System.out.print("\r" + Strings.padRight("", 120) + "\r"); System.out.flush(); } stats.seconds = (System.currentTimeMillis() - t0) / 1000.0; var res = new FillResult(ok, new Gridded(grid), assigned, stats); // print a final progress line if (Main.VERBOSE && !multiThreaded) { System.out.println( String.format(Locale.ROOT, "[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs", assigned.size(), TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, stats.seconds ) ); } return res; } }