From 4092dadfc6b02b8b6acfa30cacfcd18f7d62aad2 Mon Sep 17 00:00:00 2001 From: Wenyuan Huang <157926136+whuang369@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:48:09 -0500 Subject: [PATCH] Fix #182: count problem 145 loop solutions with an exact plug DP The checker counted loop solutions with a plain backtracking search over the 312 grid edges (static edge order, local propagation only) and stopped early only after finding six solutions. Its cost grows exponentially with how weakly the clues constrain the board, and weak clues (many 1s and 2s) are exactly what the task rewards. On such boards it neither found six solutions nor exhausted the space within the judge's 10 s checker limit, so the checker was killed and the case scored 0. Replace the search with a plug (profile) DP over the 13 x 13 grid points. The profile holds the vertical edges crossing the sweep line as a bracket sequence of path ends, one top-edge bit per column for clue cells whose bottom edge is still open, and a flag for "loop already closed". It counts edge sets forming exactly one non-empty loop that satisfies every clue, saturating at six, so the count is exact and does not depend on how weakly the board is constrained. States live in a small open-addressing map that packs the count into each entry. Input validation, the scoring formula and the verdict message are unchanged. The checker no longer prints solution grids to stderr; the judge reads only the Ratio: line. Verified: no mismatches against brute force on small grids (2.29M instances, every 3x3 clue grid included), against the old checker on 367 boards it finishes, or against an independent SAT counter on 2,434 comparisons covering every bucket from 0 to six or more. The hardest board found (all 2s with one 3) takes about 1.2 s and 70 MB. Co-Authored-By: Claude Opus 5.5 (1M context) --- algorithmic/problems/145/checker.cpp | 603 +++++++-------------------- 1 file changed, 156 insertions(+), 447 deletions(-) diff --git a/algorithmic/problems/145/checker.cpp b/algorithmic/problems/145/checker.cpp index 0843fbb48..a5278762b 100644 --- a/algorithmic/problems/145/checker.cpp +++ b/algorithmic/problems/145/checker.cpp @@ -1,6 +1,8 @@ // checker.cpp -// C++11 testlib special judge for the Slitherlink-like problem. -// Fully reproduces behavior of the original Python+Z3 checker. +// testlib special judge for the Number Loop construction problem. +// Counts the single loops satisfying every clue of the contestant's puzzle, +// capped at six, with an exact plug DP over the grid points: the count does +// not depend on how weakly the clues constrain the board. // Requires testlib.h in include path. #include "testlib.h" @@ -58,8 +60,6 @@ double score_percent(int solution_count){ return 50.0 + 50.0 * u; } -// helper printing to stderr (like original) -void eprint(const string &s){ cerr << s << "\n"; } void myAssert(bool cond, const string &msg){ if(!cond) { // In SPJ we usually call quitf for WA, but here follow original: print to stderr and exit @@ -93,405 +93,180 @@ vector readSol(InStream &in){ return res; } -// Edge representation -struct Edge { - bool is_h; // horizontal if true, vertical if false - int i, j; // coordinates: H[i][j] or V[i][j] - vector> adj_cells; // adjacent cells (r,c) - vector> adj_points; // endpoints (pi,pj) - int score; // heuristic: number of adjacent numbered cells -}; - -vector edges; -int Ecnt; - -// Backtracking state -vector edge_val; // -1 unassigned, 0 false, 1 true -int cell_assigned_true[N][N]; -int cell_unassigned[N][N]; -int point_deg[N+1][N+1]; -int point_unassigned[N+1][N+1]; - -struct Change { - int type; // 0: edge val, 1: cell_assigned_true, 2: cell_unassigned, 3: point_deg, 4: point_unassigned - int a; // encoded index (edge idx or r*100 + c or pi*100 + pj) - int old; -}; -vector changes; - -void apply_assign(int eidx, int val){ - // assume edge_val[eidx] == -1 - changes.push_back({0, eidx, edge_val[eidx]}); - edge_val[eidx] = val; - const Edge &ed = edges[eidx]; - // adjacent cells - for(size_t k=0;k j : V[i-1][k-1], entering (i, k-1) from above +// A slot is 0 (no edge) or an end of an open path, 1 for its left end and 2 +// for its right end, so the slots form a balanced bracket sequence. Cell +// (r, c) is checked when point (r+1, c) decides its bottom edge; its top edge +// H[r][c] has left the profile by then, so it is kept as one bit per column. +// One more bit records that the loop is closed, after which no edge may be +// added. Every edge set is reached by exactly one path through the DP. + +const int P = N + 1; // grid points per side +const int SLOTS = P + 1; +const int TOP_SHIFT = 2 * SLOTS; // top-edge bits follow the slots +const uint64_t PLUG_MASK = (1ULL << TOP_SHIFT) - 1; +const uint64_t CLOSED = 1ULL << (TOP_SHIFT + N); + +int slot_of(uint64_t s, int k){ return (int)((s >> (2 * k)) & 3); } + +uint64_t with_slot(uint64_t s, int k, int v){ + return (s & ~(3ULL << (2 * k))) | ((uint64_t)v << (2 * k)); } -void rollback_to(int sz){ - while((int)changes.size() > sz){ - Change ch = changes.back(); changes.pop_back(); - if(ch.type == 0){ - edge_val[ch.a] = ch.old; - } else if(ch.type == 1){ - int r = ch.a / 100, c = ch.a % 100; - cell_assigned_true[r][c] = ch.old; - } else if(ch.type == 2){ - int r = ch.a / 100, c = ch.a % 100; - cell_unassigned[r][c] = ch.old; - } else if(ch.type == 3){ - int pi = ch.a / 100, pj = ch.a % 100; - point_deg[pi][pj] = ch.old; - } else if(ch.type == 4){ - int pi = ch.a / 100, pj = ch.a % 100; - point_unassigned[pi][pj] = ch.old; - } +// Slot holding the other end of the path that ends in slot k. +int matching_slot(uint64_t s, int k){ + const int own = slot_of(s, k); + const int dir = own == 1 ? 1 : -1; + int depth = 0; + for(int t = k; t >= 0 && t < SLOTS; t += dir){ + const int v = slot_of(s, t); + if(v == 0) continue; + depth += v == own ? 1 : -1; + if(depth == 0) return t; } -} - -// Propagation: enforce cell counts and point degree constraints (0 or 2) -bool contradiction_check_and_propagate(){ - bool changed = true; - int iter = 0; - while(changed){ - changed = false; - iter++; - if(iter > 300000) break; // safety - // cells - for(int r=0;r need) return false; - if(have + rem < need) return false; - if(rem == 0){ - if(have != need) return false; - } else { - if(have == need){ - // force remaining adjacent edges false - for(int e=0;e all remaining edges adjacent must be false - for(int e=0;e best_score){ - best_score = edges[e].score; - best = e; - } - } - if(best != -1) return best; - for(int e=0;e> &H, vector> &V){ - H.assign(N+1, vector(N,0)); - V.assign(N, vector(N+1,0)); - for(int e=0;e= N || c < 0 || c >= N || grid[r][c] == ' ') return -1; + return grid[r][c] - '0'; } -// split assignment into loops (each loop is vector of (tag,i,j)) -vector > > split_loops_from_assignment(){ - vector > H(N+1, vector(N,0)); - vector > V(N, vector(N+1,0)); - for(int e=0;e table; + vector used; // occupied entries + + StateMap(){ bits = 0; rehash(16); } + + size_t find(uint64_t state) const { + const size_t mask = table.size() - 1; + size_t h = (size_t)((state * 0x9E3779B97F4A7C15ULL) >> (64 - bits)); + while(table[h] != EMPTY && (table[h] & STATE_BITS) != state) h = (h + 1) & mask; + return h; } - auto incident_edges_at_point = [&](int pi, int pj){ - vector< tuple > res; - if(pi>0 && V[pi-1][pj]) res.push_back(make_tuple('v', pi-1, pj)); - if(pi < N && V[pi][pj]) res.push_back(make_tuple('v', pi, pj)); - if(pj>0 && H[pi][pj-1]) res.push_back(make_tuple('h', pi, pj-1)); - if(pj < N && H[pi][pj]) res.push_back(make_tuple('h', pi, pj)); - return res; - }; - - auto remove_edge_mat = [&](char tag, int i, int j){ - if(tag=='h') H[i][j] = 0; - else V[i][j] = 0; - }; - - vector > > loop_list; - - while(true){ - bool found_edge = false; - char start_tag = 0; - int start_i=-1, start_j=-1; - for(int i=0;i<=N && !found_edge;i++){ - for(int j=0;j old_table(1ULL << new_bits, EMPTY); + vector old_used; + table.swap(old_table); + used.swap(old_used); + bits = new_bits; + for(uint32_t h : old_used){ + const size_t g = find(state(old_table[h])); + table[g] = old_table[h]; + used.push_back((uint32_t)g); } - for(int i=0;i > loop; - char prev_tag = 0; int prev_i = -1, prev_j = -1; - int init_pi = cur_pi, init_pj = cur_pj; - while(true){ - vector< tuple > inc = incident_edges_at_point(cur_pi, cur_pj); - bool moved = false; - for(size_t k=0;k(inc[k]); - int ei = get<1>(inc[k]); - int ej = get<2>(inc[k]); - if(prev_tag != 0 && t==prev_tag && ei==prev_i && ej==prev_j) continue; - loop.push_back(inc[k]); - remove_edge_mat(t, ei, ej); - // move to other endpoint - if(t=='h'){ - if(cur_pi==ei && cur_pj==ej){ - prev_tag = 'h'; prev_i = ei; prev_j = ej; - cur_pi = ei; cur_pj = ej+1; - } else { - prev_tag = 'h'; prev_i = ei; prev_j = ej; - cur_pi = ei; cur_pj = ej; - } - } else { - if(cur_pi==ei && cur_pj==ej){ - prev_tag = 'v'; prev_i = ei; prev_j = ej; - cur_pi = ei+1; cur_pj = ej; - } else { - prev_tag = 'v'; prev_i = ei; prev_j = ej; - cur_pi = ei; cur_pj = ej; - } - } - moved = true; - break; - } - if(!moved) break; - if(cur_pi==init_pi && cur_pj==init_pj && !loop.empty()) break; - } - if(!loop.empty()) loop_list.push_back(loop); - else break; } - return loop_list; -} -// generate hv/vv from one loop and check against grid -bool generateSol_from_loop(const vector< tuple > &loop, vector> &Hout, vector> &Vout){ - Hout.assign(N+1, vector(N,0)); - Vout.assign(N, vector(N+1,0)); - vector> cnt(N, vector(N,0)); - for(size_t k=0;k(loop[k]); - int i = get<1>(loop[k]); - int j = get<2>(loop[k]); - if(tag=='h'){ - Hout[i][j] = 1; - if(i < N) cnt[i][j] += 1; - if(i > 0) cnt[i-1][j] += 1; - } else { - Vout[i][j] = 1; - if(j < N) cnt[i][j] += 1; - if(j > 0) cnt[i][j-1] += 1; - } + static uint64_t state(uint64_t entry){ return entry & STATE_BITS; } + static int ways(uint64_t entry){ return (int)(entry >> WAYS_SHIFT); } + + // ways[s] += w, capped at limit (below 256, to fit in the entry). + void add(uint64_t s, int w, int limit){ + if(2 * (used.size() + 1) > table.size()) rehash(bits + 1); + const size_t h = find(s); + int now = w; + if(table[h] == EMPTY) used.push_back((uint32_t)h); + else now += ways(table[h]); + table[h] = s | ((uint64_t)min(limit, now) << WAYS_SHIFT); } - for(int i=0;i> &H, const vector> &V){ - string s; - s.reserve((N+1)*N + N*(N+1) + 10); - for(int i=0;i<=N;i++){ - for(int j=0;j> &H, const vector> &V){ - eprint("Sol:"); - for(int i=0;i<=N;i++){ - string s = " "; - for(int k=0;k= 0){ + const int have = (int)((s >> (TOP_SHIFT + j)) & 1) + (U != 0) + + (slot_of(s, j + 2) != 0) + right; + if(have != d_done) continue; + } + if(d_left >= 0){ + const int have = (L != 0) + (slot_of(s, j - 1) != 0) + down; + if(have > d_left || have + 1 < d_left) continue; + } + if(d_here >= 0 && (right + down > d_here || right + down + 2 < d_here)) continue; + + uint64_t t = with_slot(with_slot(s, j, 0), j + 1, 0); + if(L != 0 && U != 0){ + if(L == 1 && U == 1){ + t = with_slot(t, matching_slot(s, j + 1), 1); + } else if(L == 2 && U == 2){ + t = with_slot(t, matching_slot(s, j), 2); + } else if(L == 1 && U == 2){ + // Joining the two ends of one path closes a + // loop, which must then be the only one. + if(t & PLUG_MASK) continue; + t |= CLOSED; + } + } else if(L != 0 || U != 0){ + t = with_slot(t, down ? j : j + 1, L | U); + } else if(deg != 0){ + t = with_slot(with_slot(t, j, 1), j + 1, 2); + } -// container for unique solutions -vector>, vector>>> sol_list; -unordered_set sol_set; // serialized strings for dedup - -// recursive search (stop when found >= limit_solutions) -bool try_search(int limit_solutions){ - int unassigned = 0; - for(int e=0;e > > loops = split_loops_from_assignment(); - for(size_t i=0;i> Hsol, Vsol; - if(generateSol_from_loop(loops[i], Hsol, Vsol)){ - string key = serialize_solution(Hsol, Vsol); - if(sol_set.find(key) == sol_set.end()){ - sol_set.insert(key); - sol_list.push_back(make_pair(Hsol, Vsol)); - if((int)sol_list.size() >= limit_solutions) return true; + if(j < N){ + t &= ~(1ULL << (TOP_SHIFT + j)); + if(right && d_here >= 0) t |= 1ULL << (TOP_SHIFT + j); + } + nxt.add(t, ways, limit); + } } } + swap(cur, nxt); } - return false; - } - int e = pick_next_edge(); - if(e == -1) return false; - for(int val = 0; val <= 1; ++val){ - int save_sz = (int)changes.size(); - apply_assign(e, val); - bool ok = contradiction_check_and_propagate(); - if(ok){ - if(try_search(limit_solutions)) return true; + // Next row: every slot moves one to the right and slot 0 opens empty. + nxt.clear(); + for(uint32_t h : cur.used){ + const uint64_t s = StateMap::state(cur.table[h]); + nxt.add((s & ~PLUG_MASK) | ((s << 2) & PLUG_MASK), StateMap::ways(cur.table[h]), limit); } - rollback_to(save_sz); + swap(cur, nxt); } - return false; + return cur.get(CLOSED); } int main(int argc, char **argv){ @@ -505,80 +280,14 @@ int main(int argc, char **argv){ // read contestant output (ouf) as grid grid grid = readSol(ouf); - // build edge list - edges.clear(); - // horizontals H[0..12][0..11] - for(int i=0;i<=N;i++){ - for(int j=0;j0) ed.adj_cells.push_back(make_pair(i-1,j)); - if(i0) ed.adj_cells.push_back(make_pair(i,j-1)); - if(j