956 lines
35 KiB
Java
956 lines
35 KiB
Java
package puzzle;
|
|
|
|
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.DictEntryDTO;
|
|
import puzzle.Export.Gridded;
|
|
import puzzle.Export.Strings;
|
|
import java.io.IOException;
|
|
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.Map;
|
|
import static java.nio.charset.StandardCharsets.*;
|
|
|
|
/**
|
|
* 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) {
|
|
|
|
public CandidateInfo(int n) { this(null, n); }
|
|
}
|
|
|
|
// static final CandidateInfo[] CANDIDATES = IntStream.range(0, 10192 << 2).mapToObj(CandidateInfo::new).toArray(CandidateInfo[]::new);
|
|
|
|
//@formatter:off
|
|
@FunctionalInterface interface SlotVisitor { void visit(int key, long packedPos, int len); }
|
|
//@formatter:on
|
|
static final long GT_1_OFFSET_53_BIT = 0x3E00000000000000L;
|
|
static final long X = 0L;
|
|
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 int SIZE_MIN_1 = SIZE - 1;// ~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 MIN_LEN7 = Config.MIN_LEN * 7;
|
|
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<Context> 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) { }
|
|
|
|
static final byte B0 = (byte) 0;
|
|
static final byte B64 = (byte) 64;
|
|
static final byte B48 = (byte) 48;
|
|
// Directions for '1'..'6'
|
|
static final nbrs_16[] OFFSETS = Neighbors9x8.OFFSETS;
|
|
static final nbrs_16[] OFFSETS_FOUR = Neighbors9x8.OFFSETS_FOUR;
|
|
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 final long[] NBR8_PACKED_LO = Neighbors9x8.NBR8_PACKED_LO;
|
|
static final long[] NBR8_PACKED_HI = Neighbors9x8.NBR8_PACKED_HI;
|
|
|
|
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,
|
|
Map<Integer, Lemma> clueMap,
|
|
FillStats stats) {
|
|
|
|
public void calcSimpel() {
|
|
if (ok) {
|
|
clueMap.forEach((k, v) -> stats.simplicity += v.simpel());
|
|
stats.simplicity = clueMap.isEmpty() ? 0 : stats.simplicity / clueMap.size();
|
|
}
|
|
}
|
|
}
|
|
|
|
static final class Context {
|
|
|
|
final Bit covH2 = new Bit();
|
|
final Bit covV2 = new Bit();
|
|
final int[] cellCount = new int[SIZE];
|
|
final int[] stack = new int[SIZE];
|
|
final Bit seen = new Bit();
|
|
long pattern;
|
|
final int[] undo = new int[2048];
|
|
final long[] bitset = new long[2500];
|
|
|
|
void setPattern(long p) { this.pattern = p; }
|
|
}
|
|
|
|
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 randint2bit() { return nextU32() & 3; }
|
|
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; }
|
|
}
|
|
|
|
static class Grid {
|
|
|
|
final byte[] g;
|
|
long lo, hi;
|
|
|
|
public Grid(byte[] g) { this(g, 0, 0); }
|
|
public Grid(byte[] g, long lo, long hi) {
|
|
this.g = g;
|
|
this.lo = lo;
|
|
this.hi = hi;
|
|
}
|
|
static Grid createEmpty() { return new Grid(new byte[SIZE], X, X); }
|
|
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(), lo, hi); }
|
|
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) == 0) lo |= (1L << idx);
|
|
else hi |= (1L << (idx & 63));
|
|
}
|
|
void clearletter(int idx) { g[idx] = DASH; }
|
|
void clearClue(int idx) {
|
|
g[idx] = DASH;
|
|
if ((idx & 64) == 0) lo &= ~(1L << idx);
|
|
else hi &= ~(1L << (idx & 63));
|
|
}
|
|
static boolean isDigit(byte b) { return (b & B48) == B48; }
|
|
boolean isDigitAt(int index) { return isDigit(g[index]); }
|
|
boolean isClue(long index) {
|
|
if ((index & 64) == 0) return ((lo >> index) & 1L) != X;
|
|
return ((hi >> (index & 63)) & 1L) != X;
|
|
}
|
|
boolean isClue(int index) {
|
|
if ((index & 64) == 0) return ((lo >> index) & 1L) != 0;
|
|
return ((hi >> (index & 63)) & 1L) != 0;
|
|
}
|
|
boolean notClue(long index) {
|
|
if ((index & 64) == 0) return ((lo >> index) & 1L) == X;
|
|
return ((hi >> (index & 63)) & 1L) == X;
|
|
}
|
|
boolean notClue(int index) { return ((index & 64) == 0) ? ((lo >> index) & 1L) == X : ((hi >> (index & 63)) & 1L) == X; }
|
|
boolean clueless(int idx) {
|
|
if ((idx & 64) == 0) {
|
|
val test = (1L << idx);
|
|
if ((test & lo) == X) return false;
|
|
g[idx] = DASH;
|
|
|
|
lo &= ~test;
|
|
} else {
|
|
val test = (1L << (idx & 63));
|
|
if ((test & hi) == X) return false;
|
|
g[idx] = DASH;
|
|
|
|
hi &= ~test;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static boolean isLetter(byte b) { return (b & B64) != B0; }
|
|
public boolean isLetterSet(int idx) { return isLetter(g[idx]); }
|
|
static boolean notDigit(byte b) { return (b & B48) != B48; }
|
|
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(lo) + Long.bitCount(hi); }
|
|
boolean hasRoomForClue(long packed) { return (packed & GT_1_OFFSET_53_BIT) != X && notClue(packed & 0x7FL) && notClue((packed >>> 7) & 0x7FL); }
|
|
void forEachSlot(SlotVisitor visitor) {
|
|
for (var l = lo; l != X; l &= l - 1) processSlot(this, visitor, Long.numberOfTrailingZeros(l));
|
|
for (var h = hi; h != X; h &= h - 1) processSlot(this, visitor, 64 + Long.numberOfTrailingZeros(h));
|
|
}
|
|
}
|
|
|
|
static record DictEntry(Lemma[] words, long[][] posBitsets) { }
|
|
|
|
public static record Lemma(int index, long word) {
|
|
|
|
static int LEMMA_COUNTER = 0;
|
|
static long pack(String word) {
|
|
return pack(word.getBytes(US_ASCII));
|
|
}
|
|
static long pack(byte[] b) {
|
|
long w = 0;
|
|
for (var i = 0; i < b.length; i++) w |= ((long) b[i] & ~64) << (i * 5);
|
|
return w;
|
|
}
|
|
public Lemma(int index, String word) {
|
|
this(index, pack(word.getBytes(US_ASCII)));
|
|
}
|
|
public Lemma(String word) { this(LEMMA_COUNTER++, word); }
|
|
byte byteAt(int idx) { return (byte) ((word >>> (idx * 5)) & 0b11111 | B64); }// word[]; }
|
|
int intAt(int idx) { return (int) (((word >>> (idx * 5))) & 0b11111); }// word[]; }
|
|
@Override public int hashCode() { return index; }
|
|
@Override public boolean equals(Object o) { return (o == this) || (o instanceof Lemma l && l.index == index); }
|
|
String[] clue() { return CsvIndexService.clues(index); }
|
|
int simpel() { return CsvIndexService.simpel(index); }
|
|
int length() {
|
|
if (word == 0) return 0;
|
|
int highestBit = 63 - Long.numberOfLeadingZeros(word & 0xffffffffffffffffL);
|
|
return (highestBit / 5) + 1;
|
|
}
|
|
public String asWord() {
|
|
var b = new byte[length()];
|
|
for (var i = 0; i < length(); i++) b[i] = (byte) ((word >>> (i * 5)) & 0b11111 | B64);
|
|
return new String(b, US_ASCII);
|
|
}
|
|
}
|
|
|
|
public static record Dict(
|
|
DictEntry[] index,
|
|
int length) {
|
|
|
|
public Dict(Lemma[] wordz) {
|
|
var index = new DictEntryDTO[MAX_WORD_LENGTH_PLUS_ONE];
|
|
Arrays.setAll(index, i -> new DictEntryDTO(i));
|
|
for (var lemma : wordz) {
|
|
var L = lemma.length();
|
|
|
|
var entry = index[L];
|
|
var idx = entry.words().size();
|
|
entry.words().add(lemma);
|
|
|
|
for (var i = 0; i < L; i++) {
|
|
var letter = lemma.intAt(i) - 1;
|
|
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++) if (index[i].words().size() <= 0) throw new RuntimeException("No words for length " + i);
|
|
this(Arrays.stream(index).map(i -> {
|
|
var words = i.words().toArray(Lemma[]::new);
|
|
int numWords = words.length;
|
|
int numLongs = (numWords + 63) >>> 6;
|
|
var bitsets = new long[i.pos().length * 26][numLongs];
|
|
for (int p = 0; p < i.pos().length; p++) {
|
|
for (int l = 0; l < 26; l++) {
|
|
var list = i.pos()[p][l];
|
|
var bs = bitsets[p * 26 + l];
|
|
for (int k = 0; k < list.size(); k++) {
|
|
int wordIdx = list.data()[k];
|
|
bs[wordIdx >>> 6] |= (1L << (wordIdx & 63));
|
|
}
|
|
}
|
|
}
|
|
return new DictEntry(words, bitsets);
|
|
}).toArray(DictEntry[]::new),
|
|
Arrays.stream(index).mapToInt(i -> i.words().size()).sum());
|
|
}
|
|
static Dict loadDict(String wordsPath) {
|
|
try {
|
|
var map = new ArrayList<Lemma>();
|
|
Files.lines(Path.of(wordsPath), UTF_8).forEach(line -> CsvIndexService.lineToLemma(line, map::add));
|
|
return new Dict(map.toArray(Lemma[]::new));
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
throw new RuntimeException("Failed to load dictionary from " + wordsPath, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int intersectSorted(int[] a, int aLen, int[] b, int bLen, int[] out) {
|
|
if (aLen == 0 || bLen == 0) return 0;
|
|
if (aLen < bLen >>> 4) {
|
|
int k = 0;
|
|
for (int i = 0; i < aLen; i++) {
|
|
int x = a[i];
|
|
if (Arrays.binarySearch(b, 0, bLen, x) >= 0) out[k++] = x;
|
|
}
|
|
return k;
|
|
}
|
|
if (bLen < aLen >>> 4) {
|
|
int k = 0;
|
|
for (int i = 0; i < bLen; i++) {
|
|
int y = b[i];
|
|
if (Arrays.binarySearch(a, 0, aLen, y) >= 0) out[k++] = y;
|
|
}
|
|
return k;
|
|
}
|
|
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 final int BIT_FOR_DIR = 3;
|
|
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.clearletter(pos(i)); }
|
|
public int len() { return (int) (packedPos >>> 56); }
|
|
public int clueR() { return Grid.r((key >>> BIT_FOR_DIR)); }
|
|
public int clueIndex() { return key >>> BIT_FOR_DIR; }
|
|
public int clueC() { return Grid.c((key >>> BIT_FOR_DIR)); }
|
|
public int dir() { return key & 7; }
|
|
public boolean horiz() { return horiz(key); }
|
|
public int pos(int i) { return offset(packedPos, i); }
|
|
public static boolean horiz(int key) { return (key & 1) == 0; }
|
|
public static int offset(long packedPos, int i) { return (int) ((packedPos >> (i * 7)) & 127); }
|
|
public static int packSlotDir(int idx, int d) { return (idx << BIT_FOR_DIR) | d; }
|
|
}
|
|
|
|
private static void processSlot(Grid grid, SlotVisitor visitor, int idx) {
|
|
var d = grid.digitAt(idx);
|
|
var packed = OFFSETS[d].path()[idx];
|
|
long packedPos = 0L;
|
|
int k = 0;
|
|
for (long n = (packed >>> 56), offset = 0L, iidx; k < n; k++, offset += 7L) {
|
|
iidx = ((packed >>> offset) & 0x7FL);
|
|
if (grid.isClue(iidx)) break;
|
|
packedPos |= iidx << offset;
|
|
}
|
|
if (k > 0) {
|
|
visitor.visit(Slot.packSlotDir(idx, d), packedPos, k);
|
|
}
|
|
}
|
|
|
|
static ArrayList<Slot> extractSlots(Grid grid) {
|
|
var slots = new ArrayList<Slot>(32);
|
|
grid.forEachSlot((key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len)));
|
|
return slots;
|
|
}
|
|
|
|
long maskFitness(Grid grid) {
|
|
var ctx = CTX.get();
|
|
var covH = ctx.covH2;
|
|
var covV = ctx.covV2;
|
|
covH.clear();
|
|
covV.clear();
|
|
/*Arrays.fill(covH, 0, SIZE, 0);
|
|
Arrays.fill(covV, 0, SIZE, 0);*/
|
|
long lo_cl = grid.lo, hi_cl = grid.hi;
|
|
long penalty = (((long) Math.abs(grid.clueCount() - TARGET_CLUES)) * 16000L);
|
|
boolean hasSlots = false;
|
|
|
|
for (int i = 0; i < 65; i += 64) {
|
|
for (long bits = (i == 0 ? lo_cl : hi_cl); bits != X; bits &= bits - 1) {
|
|
int clueIdx = i + Long.numberOfTrailingZeros(bits);
|
|
var d = grid.digitAt(clueIdx);
|
|
var nbrs16 = OFFSETS[d];
|
|
long packed = nbrs16.path()[clueIdx];
|
|
int n = (int) (packed >>> 56) * 7, k, idx;
|
|
var horiz = Slot.horiz(d) ? covH : covV;
|
|
for (k = 0; k < n; k += 7) {
|
|
idx = (int) ((packed >>> (k)) & 0x7F);
|
|
if (grid.isClue(idx)) break;
|
|
horiz.set(idx);
|
|
}
|
|
if (k > 0) {
|
|
hasSlots = true;
|
|
if (k < MIN_LEN7) penalty += 8000;
|
|
} else {
|
|
penalty += 25000;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasSlots) return 1_000_000_000L;
|
|
|
|
var stack = ctx.stack;
|
|
long seenLo = 0L, seenHi = 0L;
|
|
|
|
// loop over beide helften
|
|
for (int base = 0; base <= 64; base += 64) {
|
|
long clueMask = (base == 0) ? lo_cl : hi_cl;
|
|
long seenMask = (base == 0) ? seenLo : seenHi;
|
|
|
|
// "unseen clues" in deze helft
|
|
for (long bits = clueMask & ~seenMask; bits != 0L; bits &= bits - 1) {
|
|
int clueIdx = base + Long.numberOfTrailingZeros(bits);
|
|
|
|
// start nieuwe component
|
|
int size = 0;
|
|
int sp = 0;
|
|
stack[sp++] = clueIdx;
|
|
|
|
// mark seen
|
|
if ((clueIdx & 64) == 0) seenLo |= 1L << clueIdx;
|
|
else seenHi |= 1L << (clueIdx & 63);
|
|
|
|
// flood fill / bfs
|
|
while (sp > 0) {
|
|
int cur = stack[--sp];
|
|
size++;
|
|
|
|
// neighbors als 2x long masks
|
|
long nLo = NBR8_PACKED_LO[cur];
|
|
long nHi = NBR8_PACKED_HI[cur];
|
|
|
|
// filter: alleen clues, en nog niet seen
|
|
nLo &= lo_cl & ~seenLo;
|
|
nHi &= hi_cl & ~seenHi;
|
|
|
|
// push lo-neighbors
|
|
while (nLo != 0L) {
|
|
long lsb = nLo & -nLo;
|
|
int nidx = Long.numberOfTrailingZeros(nLo); // 0..63
|
|
seenLo |= lsb;
|
|
|
|
stack[sp++] = nidx;
|
|
|
|
nLo &= nLo - 1;
|
|
}
|
|
|
|
// push hi-neighbors
|
|
while (nHi != 0L) {
|
|
long lsb = nHi & -nHi;
|
|
int nidx = 64 | Long.numberOfTrailingZeros(nHi); // 64..127
|
|
seenHi |= lsb;
|
|
|
|
stack[sp++] = nidx;
|
|
|
|
nHi &= nHi - 1;
|
|
}
|
|
}
|
|
|
|
if (size >= 2) penalty += (size - 1L) * 120L;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < 65; i += 64) {
|
|
long bits = (i == 0 ? ~lo_cl : (~hi_cl & 0xFFL));
|
|
for (; bits != X; bits &= bits - 1) {
|
|
int clueIdx = i + Long.numberOfTrailingZeros(bits);
|
|
var rci = IT[clueIdx];
|
|
if ((4 - rci.nbrCount()) + Long.bitCount(rci.n1() & lo_cl) + Long.bitCount(rci.n2() & hi_cl) >= 3) penalty += 400;
|
|
var h = covH.get(clueIdx);
|
|
var v = covV.get(clueIdx);
|
|
if (!h && !v) penalty += 1500;
|
|
else if (h && v) { /* ok */ } else if (h | v) penalty += 200;
|
|
else penalty += 600;
|
|
}
|
|
}
|
|
|
|
return penalty;
|
|
}
|
|
|
|
Grid randomMask() {
|
|
var g = Grid.createEmpty();
|
|
for (int placed = 0, guard = 0, idx; placed < TARGET_CLUES && guard < 4000; guard++) {
|
|
idx = rng.randint(0, SIZE_MIN_1);
|
|
if (g.isClue(idx)) continue;
|
|
var d = OFFSETS_FOUR[rng.randint2bit()];
|
|
|
|
if (g.hasRoomForClue(d.path()[idx])) {
|
|
g.setClue(idx, d.dbyte());
|
|
placed++;
|
|
}
|
|
}
|
|
return g;
|
|
}
|
|
Grid mutate(Grid grid) {
|
|
var g = grid.deepCopyGrid();
|
|
int ri;
|
|
var bytes = MUTATE_RI[rng.randint(0, SIZE_MIN_1)];
|
|
nbrs_16 d;
|
|
for (var k = 0; k < 4; k++) {
|
|
ri = bytes[rng.randint(0, 624)];
|
|
if (!g.clueless(ri)) {
|
|
d = OFFSETS_FOUR[rng.randint2bit()];
|
|
if (g.hasRoomForClue(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.lo, bo1 = out.hi;
|
|
for (var rci : IT) {
|
|
int i = rci.i();
|
|
if ((rci.cross_r()) * nc + (rci.cross_c()) * nr < 0) {
|
|
byte ch = b.g[i];
|
|
if (out.g[i] != ch) {
|
|
out.g[i] = ch;
|
|
if (Grid.isDigit(ch)) {
|
|
if ((i & 64) == 0) bo0 |= (1L << i);
|
|
else bo1 |= (1L << (i & 63));
|
|
} else {
|
|
if ((i & 64) == 0) bo0 &= ~(1L << i);
|
|
else bo1 &= ~(1L << (i & 63));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out.lo = bo0;
|
|
out.hi = bo1;
|
|
for (var lo = out.lo; lo != X; lo &= lo - 1L) clearClues(out, Long.numberOfTrailingZeros(lo));
|
|
for (var hi = out.hi; hi != X; hi &= hi - 1L) clearClues(out, 64 + Long.numberOfTrailingZeros(hi));
|
|
return out;
|
|
}
|
|
public static void clearClues(Grid out, int idx) { if (!out.hasRoomForClue(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, int pairs) {
|
|
class GridAndFit {
|
|
|
|
Grid grid;
|
|
long fite = -1;
|
|
GridAndFit(Grid grid) { this.grid = grid; }
|
|
long fit() {
|
|
if (fite == -1) this.fite = maskFitness(grid);
|
|
return this.fite;
|
|
}
|
|
}
|
|
if (Main.VERBOSE) System.out.println("generateMask init pop: " + popSize);
|
|
var pop = new ArrayList<GridAndFit>();
|
|
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<GridAndFit>();
|
|
|
|
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<GridAndFit>();
|
|
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 & 15) == 15) System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + pop.get(0).fit());
|
|
}
|
|
GridAndFit best = pop.get(0);
|
|
for (int i = 1; i < pop.size(); i++) {
|
|
var x = pop.get(i);
|
|
if (x.fit() < best.fit()) best = x;
|
|
}
|
|
//pop.sort(Comparator.comparingLong(GridAndFit::fit));
|
|
//return pop.get(0).grid;
|
|
return best.grid;
|
|
}
|
|
static int usedCharsInPattern(long p) {
|
|
if (p == 0) return 0;
|
|
int highestBit = 63 - Long.numberOfLeadingZeros(p); // 0-based
|
|
return (highestBit / 5) + 1;
|
|
}
|
|
static long patternForSlot(Grid grid, Slot s) {
|
|
long p = 0;
|
|
for (int i = 0, len = s.len(); i < len; i++) {
|
|
byte ch = grid.byteAt(s.pos(i));
|
|
if (isLetter(ch)) {
|
|
p |= ((long) (ch & 31)) << (i * 5);
|
|
}
|
|
}
|
|
return p;
|
|
}
|
|
|
|
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.clearletter(s.pos(j));
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
undoBuffer[offset] = mask;
|
|
return true;
|
|
}
|
|
|
|
static CandidateInfo candidateInfoForPattern(Context ctx, DictEntry entry, int lenb) {
|
|
var pattern = ctx.pattern;
|
|
if (pattern == X) {
|
|
return new CandidateInfo(null, entry.words.length);
|
|
}
|
|
|
|
int numLongs = (entry.words.length + 63) >>> 6;
|
|
long[] res = ctx.bitset;
|
|
boolean first = true;
|
|
|
|
for (int i = 0, len = usedCharsInPattern(pattern); i < len; i++) {
|
|
int val = (int) ((pattern >>> (i * 5)) & 31);
|
|
if (val != 0) {
|
|
long[] bs = entry.posBitsets[i * 26 + (val - 1)];
|
|
if (first) {
|
|
System.arraycopy(bs, 0, res, 0, numLongs);
|
|
first = false;
|
|
} else {
|
|
for (int k = 0; k < numLongs; k++) res[k] &= bs[k];
|
|
}
|
|
}
|
|
}
|
|
|
|
int count = 0;
|
|
for (int k = 0; k < numLongs; k++) count += Long.bitCount(res[k]);
|
|
|
|
if (count == 0) return new CandidateInfo(null, 0);
|
|
|
|
int[] indices = new int[count];
|
|
int ki = 0;
|
|
for (int k = 0; k < numLongs; k++) {
|
|
long w = res[k];
|
|
while (w != 0) {
|
|
int t = Long.numberOfTrailingZeros(w);
|
|
indices[ki++] = (k << 6) | t;
|
|
w &= w - 1;
|
|
}
|
|
}
|
|
|
|
return new CandidateInfo(indices, count);
|
|
}
|
|
|
|
static int candidateCountForPattern(Context ctx, DictEntry entry) {
|
|
long pattern = ctx.pattern;
|
|
if (pattern == X) return entry.words.length;
|
|
|
|
int numLongs = (entry.words.length + 63) >>> 6;
|
|
long[] res = ctx.bitset;
|
|
boolean first = true;
|
|
|
|
for (int i = 0, len = usedCharsInPattern(pattern); i < len; i++) {
|
|
int val = (int) ((pattern >>> (i * 5)) & 31);
|
|
if (val != 0) {
|
|
long[] bs = entry.posBitsets[i * 26 + (val - 1)];
|
|
if (first) {
|
|
System.arraycopy(bs, 0, res, 0, numLongs);
|
|
first = false;
|
|
} else {
|
|
for (int k = 0; k < numLongs; k++) res[k] &= bs[k];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (first) return entry.words.length; // should not happen if pattern != X
|
|
|
|
int count = 0;
|
|
for (int k = 0; k < numLongs; k++) count += Long.bitCount(res[k]);
|
|
return count;
|
|
}
|
|
//72 << 3;
|
|
static final int BIGG = 581 + 1;
|
|
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<Integer, Lemma>();
|
|
|
|
Lemma[] assigned = new Lemma[BIGG];
|
|
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 slotScores = new int[slots.size()];
|
|
for (int i = 0; i < slots.size(); i++) slotScores[i] = slotScore(count, slots.get(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 = 0;
|
|
for (var lemma : assigned) {
|
|
if (lemma != null) done++;
|
|
}
|
|
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 bestScore = -1;
|
|
for (int i = 0, n = slots.size(); i < n; i++) {
|
|
var s = slots.get(i);
|
|
if (assigned[s.key()] != null) continue;
|
|
var entry = dictIndex[s.len()];
|
|
if (entry == null) return PICK_NOT_DONE;
|
|
ctx.pattern = patternForSlot(grid, s);
|
|
int count = candidateCountForPattern(ctx, entry);
|
|
|
|
if (count == 0) return PICK_NOT_DONE;
|
|
if (best == null
|
|
|| count < bestInfo.count
|
|
|| (count == bestInfo.count && slotScores[i] > bestScore)) {
|
|
best = s;
|
|
bestScore = slotScores[i];
|
|
bestInfo = new CandidateInfo(null, count);
|
|
if (count <= 1) break;
|
|
}
|
|
}
|
|
|
|
if (best == null) return PICK_DONE;
|
|
|
|
// Re-calculate for the best slot to get actual indices
|
|
ctx.pattern = patternForSlot(grid, best);
|
|
bestInfo = candidateInfoForPattern(ctx, dictIndex[best.len()], best.len());
|
|
|
|
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];
|
|
|
|
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[idx];
|
|
|
|
if (used.get(w.index)) continue;
|
|
|
|
if (!placeWord(grid, s, w, ctx.undo, depth)) continue;
|
|
|
|
used.set(w.index);
|
|
assigned[k] = w;
|
|
|
|
if (backtrack(depth + 1)) return true;
|
|
|
|
assigned[k] = null;
|
|
used.clear(w.index);
|
|
s.undoPlace(grid, ctx.undo[depth]);
|
|
}
|
|
stats.backtracks++;
|
|
return false;
|
|
}
|
|
|
|
var N = entry.words.length;
|
|
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[idxInArray];
|
|
|
|
if (used.get(w.index)) continue;
|
|
|
|
if (!placeWord(grid, s, w, ctx.undo, depth)) continue;
|
|
|
|
used.set(w.index);
|
|
assigned[k] = w;
|
|
|
|
if (backtrack(depth + 1)) return true;
|
|
|
|
assigned[k] = null;
|
|
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;
|
|
Map<Integer, Lemma> lemmaMap = new HashMap<>();
|
|
for (var i = 0; i < assigned.length; i++) {
|
|
if (assigned[i] != null) {
|
|
lemmaMap.put(i, assigned[i]);
|
|
}
|
|
}
|
|
var res = new FillResult(ok, new Gridded(grid), lemmaMap, 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",
|
|
lemmaMap.size(), TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, stats.seconds
|
|
)
|
|
);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
}
|