
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <algorithm>
#include <unordered_map>
#include <cassert>
#include <cstdint>
#include <climits>
#include <set>
#include <map>
#include <queue>
#include <sys/time.h>
#include <functional>
#include <numeric>

using namespace std;
static double get_time() {
    struct timeval tv;
    gettimeofday(&tv, nullptr);
    return tv.tv_sec + tv.tv_usec * 1e-6;
}
static double start_time;
static const double TIME_LIMIT = 590.0; // solver timeout (evaluation = 600s)

// ---- Globals ----
static int nvars, nclauses;
static vector<vector<int>> all_clauses;

struct XORConstraint {
    vector<int> vars;
    int parity;
};

// ---- Parse CNF ----
bool parse_cnf(const char* filename) {
    FILE* f = fopen(filename, "r");
    if (!f) return false;
    char buf[1<<20];
    while (fgets(buf, sizeof(buf), f)) {
        if (buf[0]=='c'||buf[0]=='%'||buf[0]=='\n'||buf[0]=='\r') continue;
        if (buf[0]=='p') { sscanf(buf, "p cnf %d %d", &nvars, &nclauses); continue; }
        vector<int> cl;
        char* p = buf;
        while (*p) {
            while (*p==' '||*p=='\t'||*p=='\n'||*p=='\r') p++;
            if (!*p) break;
            int l=0, s=1;
            if (*p=='-') { s=-1; p++; }
            while (*p>='0'&&*p<='9') { l=l*10+(*p-'0'); p++; }
            if (!l) break;
            cl.push_back(s*l);
        }
        if (!cl.empty()) all_clauses.push_back(cl);
    }
    fclose(f);
    return true;
}

// ---- Detect XOR constraints ----
vector<XORConstraint> detect_xors(int& remaining) {
    struct VH { size_t operator()(const vector<int>&v) const {
        size_t h=v.size(); for(int x:v) h=h*1000003+x; return h; }};
    unordered_map<vector<int>,vector<int>,VH> groups;
    for (int i = 0; i < (int)all_clauses.size(); i++) {
        vector<int> vs;
        for (int l : all_clauses[i]) vs.push_back(abs(l));
        sort(vs.begin(), vs.end());
        groups[vs].push_back(i);
    }
    vector<XORConstraint> xors;
    remaining = 0;
    for (auto& [vs, idx] : groups) {
        int n = vs.size();
        if (n > 20 || (int)idx.size() != (1<<(n-1))) { remaining += idx.size(); continue; }
        int ec = 0, oc = 0;
        for (int i : idx) {
            int ng = 0; for (int l : all_clauses[i]) if (l<0) ng++;
            if (ng%2==0) ec++; else oc++;
        }
        int exp = 1<<(n-1);
        if (ec == exp) xors.push_back({vs, 1});
        else if (oc == exp) xors.push_back({vs, 0});
        else remaining += idx.size();
    }
    return xors;
}

// ---- Repair incomplete XOR groups by expanding shorter clauses ----
struct VirtualClause {
    vector<int> lits;  // The clause to derive
    int parent_ci;     // Index in all_clauses of the subsuming shorter clause
};

vector<VirtualClause> virtual_clauses;

void repair_xors(vector<XORConstraint>& xors, int& remaining) {
    struct VH { size_t operator()(const vector<int>&v) const {
        size_t h=v.size(); for(int x:v) h=h*1000003+x; return h; }};

    // Rebuild groups
    unordered_map<vector<int>,vector<int>,VH> groups;
    for (int i = 0; i < (int)all_clauses.size(); i++) {
        vector<int> vs;
        for (int l : all_clauses[i]) vs.push_back(abs(l));
        sort(vs.begin(), vs.end());
        groups[vs].push_back(i);
    }

    // Find incomplete groups that could be repaired
    vector<pair<vector<int>, vector<int>>> incomplete; // (vars, clause_indices)
    for (auto& [vs, idx] : groups) {
        int n = vs.size();
        if (n < 2 || n > 10) continue;
        int exp = 1 << (n-1);
        if ((int)idx.size() >= exp) continue; // already complete or over
        if ((int)idx.size() < exp - 4) continue; // too many missing, skip

        // Count parities
        int ec = 0, oc = 0;
        for (int i : idx) {
            int ng = 0; for (int l : all_clauses[i]) if (l<0) ng++;
            if (ng%2==0) ec++; else oc++;
        }

        // Check if one parity is almost complete
        int target_par = -1;
        if (ec >= exp - 4 && ec < exp) target_par = 0; // need more even clauses
        if (oc >= exp - 4 && oc < exp) target_par = 1; // need more odd clauses
        if (target_par < 0) continue;

        incomplete.push_back({vs, idx});
    }

    if (incomplete.empty()) return;

    // Build map of short clauses by variable subset
    unordered_map<vector<int>,vector<int>,VH> short_groups;
    for (auto& [vs, idx] : groups) {
        if ((int)vs.size() <= 6) short_groups[vs] = idx;
    }

    int repaired = 0;

    for (auto& [vs, idx] : incomplete) {
        int n = vs.size();
        int exp = 1 << (n-1);

        // Determine target parity
        int ec = 0, oc = 0;
        for (int i : idx) {
            int ng = 0; for (int l : all_clauses[i]) if (l<0) ng++;
            if (ng%2==0) ec++; else oc++;
        }
        int target_par; // 0 = even negation, 1 = odd negation
        if (oc > ec) target_par = 1; else target_par = 0;
        int have = (target_par == 0) ? ec : oc;
        if (have >= exp) continue;

        // Find which patterns are present
        set<uint32_t> present;
        for (int i : idx) {
            uint32_t pat = 0;
            int ng = 0;
            for (int l : all_clauses[i]) {
                int v = abs(l);
                for (int j = 0; j < n; j++)
                    if (vs[j] == v && l < 0) pat |= (1u << j);
                if (l < 0) ng++;
            }
            if (ng % 2 == target_par) present.insert(pat);
        }

        // Find missing patterns
        vector<uint32_t> missing;
        for (uint32_t pat = 0; pat < (1u << n); pat++) {
            if (__builtin_popcount(pat) % 2 != target_par) continue;
            if (!present.count(pat)) missing.push_back(pat);
        }

        if ((int)missing.size() > 4) continue;

        // For each missing pattern, try to find a subsuming shorter clause
        bool all_found = true;
        vector<VirtualClause> new_vcs;

        for (uint32_t mp : missing) {
            // Build the missing clause
            vector<int> mcl;
            for (int j = 0; j < n; j++) {
                if ((mp >> j) & 1) mcl.push_back(-vs[j]);
                else mcl.push_back(vs[j]);
            }

            // Check all shorter clause groups for subsumption
            bool found = false;
            for (auto& [svs, sidx] : short_groups) {
                if ((int)svs.size() >= n) continue;
                // Check if svs ⊂ vs
                bool subset = true;
                for (int sv : svs) {
                    bool in_vs = false;
                    for (int v : vs) if (v == sv) { in_vs = true; break; }
                    if (!in_vs) { subset = false; break; }
                }
                if (!subset) continue;

                for (int si : sidx) {
                    // Check if clauses[si] subsumes mcl
                    bool subsumes = true;
                    for (int l : all_clauses[si]) {
                        bool in_mcl = false;
                        for (int ml : mcl) if (ml == l) { in_mcl = true; break; }
                        if (!in_mcl) { subsumes = false; break; }
                    }
                    if (subsumes) {
                        new_vcs.push_back({mcl, si});
                        found = true;
                        break;
                    }
                }
                if (found) break;
            }
            if (!found) { all_found = false; break; }
        }

        if (!all_found) continue;

        // Repair successful: add virtual clauses and create XOR constraint
        vector<int> all_idx = idx; // original clause indices
        set<int> used_parent_ids; // Track which short clauses were used
        for (auto& vc : new_vcs) {
            int new_ci = all_clauses.size();
            all_clauses.push_back(vc.lits);
            all_idx.push_back(new_ci);
            virtual_clauses.push_back(vc);
            used_parent_ids.insert(vc.parent_ci);
        }

        // Now check if the full group is a valid XOR
        int new_ec = 0, new_oc = 0;
        for (int i : all_idx) {
            int ng = 0; for (int l : all_clauses[i]) if (l<0) ng++;
            if (ng%2==0) new_ec++; else new_oc++;
        }

        if (new_ec == exp) {
            xors.push_back({vs, 1});
            remaining -= idx.size(); // Remove original ternary clauses from remaining
            // Also subtract the short clauses used in subsumption
            for (int pid : used_parent_ids) {
                vector<int> pvs;
                for (int l : all_clauses[pid]) pvs.push_back(abs(l));
                sort(pvs.begin(), pvs.end());
                remaining -= groups[pvs].size();
            }
            repaired++;
        } else if (new_oc == exp) {
            xors.push_back({vs, 0});
            remaining -= idx.size();
            for (int pid : used_parent_ids) {
                vector<int> pvs;
                for (int l : all_clauses[pid]) pvs.push_back(abs(l));
                sort(pvs.begin(), pvs.end());
                remaining -= groups[pvs].size();
            }
            repaired++;
        }
    }

    if (repaired > 0) {
        fprintf(stderr, "c Repaired %d incomplete XOR groups (%d virtual clauses)\n",
                repaired, (int)virtual_clauses.size());
    }
}

// ---- GF(2) system for SAT/UNSAT ----
struct GF2System {
    int nv, nw, nr;
    vector<vector<uint64_t>> rows;
    vector<int> parities;
    void init(int n) { nv=n; nw=(n+63)/64; nr=0; }
    void add_row(const vector<int>& vars, int par) {
        vector<uint64_t> row(nw,0);
        for (int v:vars) row[(v-1)/64]^=(1ULL<<((v-1)%64));
        rows.push_back(row); parities.push_back(par); nr++;
    }
    bool get_bit(int r,int c) const { return(rows[r][c/64]>>(c%64))&1; }
    int eliminate() {
        int cur=0;
        for(int col=0;col<nv&&cur<nr;col++){
            int piv=-1;
            for(int r=cur;r<nr;r++) if(get_bit(r,col)){piv=r;break;}
            if(piv<0)continue;
            if(piv!=cur){swap(rows[piv],rows[cur]);swap(parities[piv],parities[cur]);}
            for(int r=0;r<nr;r++){
                if(r==cur||!get_bit(r,col))continue;
                for(int w=0;w<nw;w++)rows[r][w]^=rows[cur][w];
                parities[r]^=parities[cur];
            }
            cur++;
        }
        for(int r=cur;r<nr;r++){
            bool z=true; for(int w=0;w<nw;w++) if(rows[r][w]){z=false;break;}
            if(z&&parities[r])return-1;
        }
        return cur;
    }
    vector<int> get_solution() {
        vector<int> sol(nv,0);
        vector<int> pc(nr,-1);
        for(int r=0;r<nr;r++) for(int c=0;c<nv;c++) if(get_bit(r,c)){pc[r]=c;break;}
        for(int r=nr-1;r>=0;r--){
            if(pc[r]<0)continue;
            int v=parities[r];
            for(int c=pc[r]+1;c<nv;c++) if(get_bit(r,c)) v^=sol[c];
            sol[pc[r]]=v;
        }
        return sol;
    }
};

// ---- Model verification ----
bool verify_model(const vector<int>& sol) {
    for (auto& cl : all_clauses) {
        bool sat = false;
        for (int l : cl) {
            int v = abs(l)-1;
            if ((l>0 && sol[v]==1)||(l<0 && sol[v]==0)) { sat=true; break; }
        }
        if (!sat) return false;
    }
    return true;
}

// ---- LiveXOR for proof generation ----
struct LiveXOR {
    vector<int> vars; // sorted var IDs (1-based)
    int parity;
    unordered_map<uint32_t, int> pat2id; // sign pattern bitmask -> constraint ID

    int find_pos(int v) const {
        auto it = lower_bound(vars.begin(), vars.end(), v);
        if (it != vars.end() && *it == v) return (int)(it - vars.begin());
        return -1;
    }

    bool empty() const { return vars.empty() && pat2id.empty(); }
};

// Build initial LiveXOR from XOR constraint and its clauses
LiveXOR build_live(const XORConstraint& xc, const vector<int>& clause_indices) {
    LiveXOR lx;
    lx.vars = xc.vars;
    lx.parity = xc.parity;
    for (int ci : clause_indices) {
        uint32_t pat = 0;
        for (int l : all_clauses[ci]) {
            int v = abs(l);
            int pos = lx.find_pos(v);
            if (pos >= 0 && l < 0) pat |= (1u << pos);
        }
        lx.pat2id[pat] = ci + 1; // 1-based VeriPB ID
    }
    return lx;
}

// Combine two LiveXORs, eliminating all shared variables.
// Uses iterative pol+s: one shared variable at a time.
// Batch-delete constraint IDs from VeriPB
void emit_batch_del(FILE* f, const unordered_map<uint32_t, int>& pats, long long& total_lines) {
    if (pats.empty()) return;
    vector<int> ids;
    ids.reserve(pats.size());
    for (auto& [pat, id] : pats) ids.push_back(id);
    for (int i = 0; i < (int)ids.size(); i += 1000) {
        fprintf(f, "del id");
        int end = min(i + 1000, (int)ids.size());
        for (int j = i; j < end; j++) fprintf(f, " %d", ids[j]);
        fprintf(f, " ;\n");
        total_lines++;
    }
}

// Returns combined LiveXOR. On failure: returns LiveXOR with failed=true indication.
bool combine_live(LiveXOR& A, LiveXOR& B, LiveXOR& result, FILE* f, int& next_id, long long& total_lines) {
    // Find shared variables using merge of sorted arrays
    vector<int> shared;
    {
        int ai=0, bi=0;
        while (ai<(int)A.vars.size() && bi<(int)B.vars.size()) {
            if (A.vars[ai]==B.vars[bi]) { shared.push_back(A.vars[ai]); ai++; bi++; }
            else if (A.vars[ai]<B.vars[bi]) ai++; else bi++;
        }
    }
    int k = shared.size();
    if (k == 0) return false;
    
    // New vars = symmetric difference
    set<int> sset(shared.begin(), shared.end());
    vector<int> new_vars;
    for (int v : A.vars) if (!sset.count(v)) new_vars.push_back(v);
    for (int v : B.vars) if (!sset.count(v)) new_vars.push_back(v);
    sort(new_vars.begin(), new_vars.end());
    int new_par = A.parity ^ B.parity;
    
    // Check feasibility: estimate max intermediate width
    // Width 26 → 2^25 = 32M entries, might use ~2GB. Guard with entry count check.
    int phase0_w = (int)A.vars.size() + (int)B.vars.size() - k - 1; // after removing shared[0]
    if (phase0_w > 26) return false; // width limit
    
    // Phase 0: pair A and B clauses (opposite on shared[0], same on shared[1..k-1])
    // Build intermediate variable list: (A.vars ∪ B.vars) \ {shared[0]}
    vector<int> inter_vars;
    {
        set<int> all_v(A.vars.begin(), A.vars.end());
        all_v.insert(B.vars.begin(), B.vars.end());
        all_v.erase(shared[0]);
        inter_vars.assign(all_v.begin(), all_v.end());
    }
    
    // Position mappings
    int s0_posA = A.find_pos(shared[0]);
    int s0_posB = B.find_pos(shared[0]);
    
    // Positions of shared[1..k-1] in A and B
    vector<int> sh_posA(k), sh_posB(k);
    for (int i = 0; i < k; i++) {
        sh_posA[i] = A.find_pos(shared[i]);
        sh_posB[i] = B.find_pos(shared[i]);
    }
    
    // Build position maps: A vars -> inter_vars, B unique vars -> inter_vars
    // For A: map each var (except shared[0]) from its A-position to its inter_vars position
    vector<pair<int,int>> a_to_inter; // (posInA, posInInter)
    for (int j = 0; j < (int)A.vars.size(); j++) {
        if (A.vars[j] == shared[0]) continue;
        int ip = (int)(lower_bound(inter_vars.begin(), inter_vars.end(), A.vars[j]) - inter_vars.begin());
        a_to_inter.push_back({j, ip});
    }
    // For B unique vars: map from B-position to inter_vars position
    vector<pair<int,int>> b_to_inter;
    for (int j = 0; j < (int)B.vars.size(); j++) {
        if (sset.count(B.vars[j])) continue; // skip all shared (they're mapped via A)
        int ip = (int)(lower_bound(inter_vars.begin(), inter_vars.end(), B.vars[j]) - inter_vars.begin());
        b_to_inter.push_back({j, ip});
    }
    
    unordered_map<uint32_t, int> cur_pats;
    // Memory guard: limit total entries to prevent OOM
    // Each entry ≈ 64 bytes in unordered_map; 25M entries ≈ 1.6GB
    size_t max_entries = 25000000;

    for (auto& [patA, idA] : A.pat2id) {
        int s0A = (patA >> s0_posA) & 1;
        
        for (auto& [patB, idB] : B.pat2id) {
            // shared[0] must have opposite sign
            int s0B = (patB >> s0_posB) & 1;
            if (s0A == s0B) continue;
            
            // shared[1..k-1] must have same sign
            bool ok = true;
            for (int i = 1; i < k && ok; i++) {
                if (((patA >> sh_posA[i]) & 1) != ((patB >> sh_posB[i]) & 1)) ok = false;
            }
            if (!ok) continue;
            
            // Build intermediate pattern
            uint32_t ipat = 0;
            for (auto& [pA, pI] : a_to_inter)
                if ((patA >> pA) & 1) ipat |= (1u << pI);
            for (auto& [pB, pI] : b_to_inter)
                if ((patB >> pB) & 1) ipat |= (1u << pI);
            
            if (cur_pats.count(ipat)) continue;
            if (cur_pats.size() >= max_entries) return false; // memory guard
            fprintf(f, "pol %d %d + s\n", idA, idB);
            cur_pats[ipat] = next_id++;
            total_lines++;
            if (total_lines > 300000000LL) return false;
        }
    }

    // Delete consumed A and B constraint IDs
    emit_batch_del(f, A.pat2id, total_lines);
    emit_batch_del(f, B.pat2id, total_lines);

    // Subsequent phases: eliminate shared[1], shared[2], ..., shared[k-1]
    vector<int> cur_vars = inter_vars;
    
    for (int phase = 1; phase < k; phase++) {
        int sv = shared[phase];
        int sv_pos = (int)(lower_bound(cur_vars.begin(), cur_vars.end(), sv) - cur_vars.begin());
        
        vector<int> next_vars;
        for (int v : cur_vars) if (v != sv) next_vars.push_back(v);
        
        unordered_map<uint32_t, int> next_pats;
        
        // Group by pattern-without-sv-bit
        unordered_map<uint32_t, pair<int,int>> by_base; // base_pat -> (id_with_sv=0, id_with_sv=1)
        uint32_t sv_bit = 1u << sv_pos;
        
        for (auto& [pat, id] : cur_pats) {
            // Compute base pattern (without sv bit, with shifted higher bits)
            uint32_t base = 0;
            for (int j = 0; j < (int)cur_vars.size(); j++) {
                if (j == sv_pos) continue;
                int nj = j < sv_pos ? j : j - 1;
                if ((pat >> j) & 1) base |= (1u << nj);
            }
            int sv_val = (pat >> sv_pos) & 1;
            auto& entry = by_base[base];
            if (sv_val == 0) entry.first = id;
            else entry.second = id;
        }
        
        for (auto& [base, ids] : by_base) {
            if (ids.first == 0 || ids.second == 0) continue;
            if (next_pats.count(base)) continue;
            if (next_pats.size() >= max_entries) return false; // memory guard
            fprintf(f, "pol %d %d + s\n", ids.first, ids.second);
            next_pats[base] = next_id++;
            total_lines++;
            if (total_lines > 300000000LL) return false;
        }

        // Delete previous phase constraints
        emit_batch_del(f, cur_pats, total_lines);

        cur_vars = next_vars;
        cur_pats = next_pats;
    }
    
    result.vars = new_vars;
    result.parity = new_par;
    result.pat2id = cur_pats;
    return true;
}

// ---- Compute ordering with given start and heuristic ----
struct OrderResult {
    vector<int> order;
    long long estimated_lines;
    int max_combine_width;
};

// Fast frontier width computation using bitset (vector<bool>)
void compute_order_stats(OrderResult& res, const vector<XORConstraint>& xors) {
    int max_var = 0;
    for (auto& x : xors) for (int v : x.vars) max_var = max(max_var, v);
    vector<bool> in_frontier(max_var + 1, false);
    int frontier_size = 0;
    res.estimated_lines = 0;
    res.max_combine_width = 0;
    for (int i = 0; i < (int)res.order.size(); i++) {
        int idx = res.order[i];
        if (i > 0) {
            int shared = 0;
            for (int v : xors[idx].vars)
                if (in_frontier[v]) shared++;
            if (shared > 0) {
                int cw = frontier_size + (int)xors[idx].vars.size() - shared - 1;
                if (cw > res.max_combine_width) res.max_combine_width = cw;
                if (cw < 40) res.estimated_lines += (1LL << (cw - 1)) * 2;
                else res.estimated_lines = (long long)1e18;
            }
        }
        for (int v : xors[idx].vars) {
            if (in_frontier[v]) { in_frontier[v] = false; frontier_size--; }
            else { in_frontier[v] = true; frontier_size++; }
        }
    }
}

OrderResult compute_ordering(const vector<XORConstraint>& xors,
                              const unordered_map<int, vector<int>>& var2xors,
                              int start, int heuristic) {
    int nxors = xors.size();
    OrderResult res;
    res.order.push_back(start);
    vector<bool> in_order(nxors, false);
    in_order[start] = true;
    set<int> frontier_vars;
    for (int v : xors[start].vars) frontier_vars.insert(v);

    while ((int)res.order.size() < nxors) {
        int best = -1;
        int best_new_frontier = INT_MAX;
        int best_shared = -1;

        set<int> candidates;
        for (int v : frontier_vars)
            for (int xi : var2xors.at(v))
                if (!in_order[xi]) candidates.insert(xi);

        for (int c : candidates) {
            int shared = 0, newv = 0;
            for (int v : xors[c].vars) {
                if (frontier_vars.count(v)) shared++;
                else newv++;
            }
            int new_frontier = (int)frontier_vars.size() - shared + newv;
            bool better = false;
            if (heuristic == 0) {
                // Max shared, then min new vars
                better = (shared > best_shared || (shared == best_shared && new_frontier < best_new_frontier));
            } else {
                // Min resulting frontier
                better = (new_frontier < best_new_frontier ||
                          (new_frontier == best_new_frontier && shared > best_shared));
            }
            if (better) {
                best_shared = shared;
                best_new_frontier = new_frontier;
                best = c;
            }
        }

        if (best < 0) {
            for (int i = 0; i < nxors; i++)
                if (!in_order[i]) { best = i; break; }
            if (best < 0) break;
        }

        res.order.push_back(best);
        in_order[best] = true;
        for (int v : xors[best].vars) {
            if (frontier_vars.count(v)) frontier_vars.erase(v);
            else frontier_vars.insert(v);
        }
    }

    // Compute estimated proof size and max combine width using fast method
    compute_order_stats(res, xors);

    return res;
}

// Linear ordering based on spectral-like heuristic (distance-based)
// Good for grid-like graphs: finds the natural "long axis" to follow
OrderResult spectral_ordering(const vector<XORConstraint>& xors,
                               const unordered_map<int, vector<int>>& var2xors,
                               const vector<vector<int>>& xor_adj) {
    int nxors = xors.size();

    // BFS to find distances from a node
    auto bfs_dist = [&](int start) -> vector<int> {
        vector<int> dist(nxors, -1);
        queue<int> q;
        dist[start] = 0;
        q.push(start);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : xor_adj[u]) {
                if (dist[v] < 0) {
                    dist[v] = dist[u] + 1;
                    q.push(v);
                }
            }
        }
        return dist;
    };

    // Find two pseudo-peripheral nodes
    auto dist0 = bfs_dist(0);
    int far1 = max_element(dist0.begin(), dist0.end()) - dist0.begin();
    auto distA = bfs_dist(far1);
    int far2 = max_element(distA.begin(), distA.end()) - distA.begin();
    auto distB = bfs_dist(far2);

    // Sort by distA - distB (gives linear ordering along the long axis)
    // Break ties by distA + distB (layer ordering)
    vector<int> order(nxors);
    iota(order.begin(), order.end(), 0);
    sort(order.begin(), order.end(), [&](int a, int b) {
        int da = distA[a] - distB[a], db = distA[b] - distB[b];
        if (da != db) return da < db;
        return distA[a] + distB[a] < distA[b] + distB[b];
    });

    // Compute estimated proof size using fast method
    OrderResult res;
    res.order = order;
    compute_order_stats(res, xors);
    return res;
}

// DFS post-order ordering - excellent for tree-like graphs, O(n)
OrderResult dfs_postorder(const vector<XORConstraint>& xors,
                           const unordered_map<int, vector<int>>& var2xors,
                           const vector<vector<int>>& xor_adj,
                           int start) {
    int nxors = xors.size();
    vector<int> order;
    order.reserve(nxors);
    vector<bool> visited(nxors, false);

    // Iterative DFS post-order
    struct Frame { int node; int child_idx; };
    vector<Frame> stack;
    stack.push_back({start, 0});
    visited[start] = true;

    while (!stack.empty()) {
        Frame& f = stack.back();
        if (f.child_idx < (int)xor_adj[f.node].size()) {
            int child = xor_adj[f.node][f.child_idx++];
            if (!visited[child]) {
                visited[child] = true;
                stack.push_back({child, 0});
            }
        } else {
            order.push_back(f.node);
            stack.pop_back();
        }
    }

    // Handle disconnected components
    for (int i = 0; i < nxors; i++) {
        if (!visited[i]) {
            stack.push_back({i, 0});
            visited[i] = true;
            while (!stack.empty()) {
                Frame& f = stack.back();
                if (f.child_idx < (int)xor_adj[f.node].size()) {
                    int child = xor_adj[f.node][f.child_idx++];
                    if (!visited[child]) {
                        visited[child] = true;
                        stack.push_back({child, 0});
                    }
                } else {
                    order.push_back(f.node);
                    stack.pop_back();
                }
            }
        }
    }

    // Compute estimated proof size using fast method
    OrderResult res;
    res.order = order;
    compute_order_stats(res, xors);
    return res;
}

// ---- Polynomial parity-sum proof for Tseitin/XOR UNSAT ----
// Uses VeriPB red rule with extension (carry) variables.
// Proof size is O(n) regardless of treewidth.
// For degree >= 4 XORs: decomposes into degree-3 sub-XORs, introduces
// sub-XOR clauses via red rules, then applies parity-sum on expanded system.
// Parity-sum UNSAT proof for Tseitin formulas.
// Handles degree 2, 3, and 4 XORs.
// For degree 4 p=0: decomposes into degree-3 sub-XORs.
// Returns false for degree >= 5 or unsupported cases.
bool gen_parity_sum_proof(const char* proof_path, const vector<XORConstraint>& xors) {
    int nxors = (int)xors.size();
    if (nxors == 0) return false;

    int parity_sum = 0;
    for (auto& x : xors) parity_sum ^= x.parity;
    if (parity_sum == 0) return false;

    vector<int> var_count(nvars + 1, 0);
    for (auto& x : xors)
        for (int v : x.vars) var_count[v]++;
    for (int v = 1; v <= nvars; v++)
        if (var_count[v] != 0 && var_count[v] != 2) return false;

    int max_degree = 0;
    for (auto& x : xors) max_degree = max(max_degree, (int)x.vars.size());
    if (max_degree > 7) {
        fprintf(stderr, "c Parity proof: max degree %d > 7\n", max_degree);
        return false;
    }

    // Build clause groups
    struct VH { size_t operator()(const vector<int>&v) const {
        size_t h=v.size(); for(int x:v) h=h*1000003+x; return h; }};
    unordered_map<vector<int>,vector<int>,VH> var_groups;
    int total_clauses = (int)all_clauses.size();
    for (int ci = 0; ci < total_clauses; ci++) {
        vector<int> vs;
        for (int l : all_clauses[ci]) vs.push_back(abs(l));
        sort(vs.begin(), vs.end());
        var_groups[vs].push_back(ci);
    }
    vector<vector<int>> clause_groups(nxors);
    for (int i = 0; i < nxors; i++) {
        auto it = var_groups.find(xors[i].vars);
        if (it == var_groups.end()) return false;
        clause_groups[i] = it->second;
    }

    // Build expanded XOR system
    struct ExXOR {
        vector<int> vars; // 2 or 3 variables
        int parity;
        bool is_original; // clauses from original formula
        int clause_pids[4]; // 1-based constraint IDs
    };
    vector<ExXOR> expanded;
    int next_fresh = nvars + 1;

    // For degree-4 p=0 decomposition info
    struct DecompInfo {
        int orig_idx;
        int v0, v1, v2, v3, y;
        int first_idx, second_idx; // indices in expanded
    };
    vector<DecompInfo> decomps;

    // For degree-5+ chain decomposition info
    struct GenDecompInfo {
        int orig_idx;
        vector<int> orig_vars; // v0, ..., v_{k-1}
        int parity;
        vector<int> fresh_vars; // y0, ..., y_{k-4}
        vector<int> expanded_indices; // indices in expanded[] for each sub (k-2 subs)
    };
    vector<GenDecompInfo> gen_decomps;

    for (int i = 0; i < nxors; i++) {
        int k = (int)xors[i].vars.size();
        int p = xors[i].parity;
        auto& vars = xors[i].vars;

        if (k <= 3 || (k == 4 && p == 1)) {
            ExXOR ex;
            ex.vars = vars;
            ex.parity = p;
            ex.is_original = true;
            expanded.push_back(ex);
        } else if (k == 4 && p == 0) {
            int y = next_fresh++;
            DecompInfo di;
            di.orig_idx = i;
            di.v0 = vars[0]; di.v1 = vars[1];
            di.v2 = vars[2]; di.v3 = vars[3];
            di.y = y;

            ExXOR ex1;
            ex1.vars = {vars[0], vars[1], y};
            sort(ex1.vars.begin(), ex1.vars.end());
            ex1.parity = 0; ex1.is_original = false;
            di.first_idx = (int)expanded.size();
            expanded.push_back(ex1);

            ExXOR ex2;
            ex2.vars = {y, vars[2], vars[3]};
            sort(ex2.vars.begin(), ex2.vars.end());
            ex2.parity = 0; ex2.is_original = false;
            di.second_idx = (int)expanded.size();
            expanded.push_back(ex2);

            decomps.push_back(di);
        } else {
            // k >= 5: chain decomposition into k-2 degree-3 sub-XORs
            // sub_0: XOR(v0, v1, y0) = p
            // sub_j (1<=j<=k-4): XOR(y_{j-1}, v_{j+1}, y_j) = 0
            // sub_{k-3}: XOR(y_{k-4}, v_{k-2}, v_{k-1}) = 0
            GenDecompInfo gd;
            gd.orig_idx = i;
            gd.orig_vars = vars;
            gd.parity = p;

            int nsubs = k - 2;
            int nfresh = k - 3; // y0, ..., y_{k-4}
            gd.fresh_vars.resize(nfresh);
            for (int j = 0; j < nfresh; j++)
                gd.fresh_vars[j] = next_fresh++;

            gd.expanded_indices.resize(nsubs);
            for (int j = 0; j < nsubs; j++) {
                ExXOR ex;
                ex.parity = (j == 0) ? p : 0;
                ex.is_original = false;

                if (j == 0) {
                    // sub_0: XOR(v0, v1, y0)
                    ex.vars = {vars[0], vars[1], gd.fresh_vars[0]};
                } else if (j < nsubs - 1) {
                    // sub_j: XOR(y_{j-1}, v_{j+1}, y_j)
                    ex.vars = {gd.fresh_vars[j-1], vars[j+1], gd.fresh_vars[j]};
                } else {
                    // sub_{k-3}: XOR(y_{k-4}, v_{k-2}, v_{k-1})
                    ex.vars = {gd.fresh_vars[j-1], vars[k-2], vars[k-1]};
                }
                sort(ex.vars.begin(), ex.vars.end());

                gd.expanded_indices[j] = (int)expanded.size();
                expanded.push_back(ex);
            }

            gen_decomps.push_back(gd);
        }
    }

    int nexp = (int)expanded.size();
    { int ps = 0; for (auto& e : expanded) ps ^= e.parity;
      if (ps == 0) return false; }
    { unordered_map<int,int> vc;
      for (auto& e : expanded) for (int v : e.vars) vc[v]++;
      for (auto& [v,c] : vc) if (c != 2) return false; }

    fprintf(stderr, "c Parity proof: %d XORs -> %d expanded (max_deg=%d)\n",
            nxors, nexp, max_degree);

    FILE* pf = fopen(proof_path, "w");
    if (!pf) return false;
    fprintf(pf, "pseudo-Boolean proof version 3.0\n");
    // Use nclauses (original formula clause count) for f line
    fprintf(pf, "f %d ;\n", nclauses);
    int next_pid = nclauses + 1;

    // Derive virtual clauses (from XOR repair) via RUP
    for (auto& vc : virtual_clauses) {
        fprintf(pf, "rup");
        for (int l : vc.lits) {
            fprintf(pf, " 1 %sx%d", l < 0 ? "~" : "", abs(l));
        }
        fprintf(pf, " >= 1 ;\n");
        next_pid++;
    }

    // Helper: generate 4 clause masks for XOR(vars[0..2])=parity
    // Each mask: 3 bits, bit j set = vars[j] negated in clause
    // For parity p: forbidden assignments have sum ≡ 1-p (mod 2)
    // mask = forbidden assignment, neg_count = sum of assignment
    auto get_xor3_masks = [](int p) -> vector<int> {
        vector<int> masks;
        for (int m = 0; m < 8; m++) {
            int nc = __builtin_popcount(m);
            if (nc % 2 != p) // wrong parity = forbidden
                masks.push_back(m);
        }
        return masks;
    };

    // Phase 1: Introduce sub-XOR clauses for decomposed degree-4 XORs
    for (auto& di : decomps) {
        int v0 = di.v0, v1 = di.v1, v2 = di.v2, v3 = di.v3, y = di.y;
        int oi = di.orig_idx;

        // Build orig clause map: assignment mask -> 1-based clause ID
        unordered_map<int,int> orig_map; // mask over (v0,v1,v2,v3) -> clause pid
        for (int ci : clause_groups[oi]) {
            auto& clause = all_clauses[ci];
            int mask = 0;
            for (int l : clause) {
                int v = abs(l);
                int bit = -1;
                if (v == v0) bit = 0; else if (v == v1) bit = 1;
                else if (v == v2) bit = 2; else if (v == v3) bit = 3;
                if (bit >= 0 && l < 0) mask |= (1 << bit);
            }
            orig_map[mask] = ci + 1;
        }

        // First sub-XOR: XOR(v0,v1,y)=0. vars in this order: [v0,v1,y]
        // y is brand new. Group masks by y sign.
        auto masks1 = get_xor3_masks(0); // p=0
        // vars order: bit 0=v0, bit 1=v1, bit 2=y
        vector<int> pos_y_masks1, neg_y_masks1;
        for (int m : masks1) {
            if ((m >> 2) & 1) neg_y_masks1.push_back(m);
            else pos_y_masks1.push_back(m);
        }

        // Introduce pos-y clauses (witness y→1, auto-prove since y is new)
        int sub1_pos_pids[2];
        for (int ci = 0; ci < 2; ci++) {
            int mask = pos_y_masks1[ci];
            fprintf(pf, "red ");
            int vars1[3] = {v0, v1, y};
            for (int j = 0; j < 3; j++)
                fprintf(pf, "1 %sx%d ", (mask >> j) & 1 ? "~" : "", vars1[j]);
            fprintf(pf, ">= 1 : x%d -> 1 ;\n", y);
            sub1_pos_pids[ci] = next_pid++;
        }

        // Introduce neg-y clauses (witness y→0, need subproof for pos-y)
        int sub1_neg_pids[2];
        for (int ci = 0; ci < 2; ci++) {
            int mask = neg_y_masks1[ci];
            fprintf(pf, "red ");
            int vars1[3] = {v0, v1, y};
            for (int j = 0; j < 3; j++)
                fprintf(pf, "1 %sx%d ", (mask >> j) & 1 ? "~" : "", vars1[j]);
            fprintf(pf, ">= 1 : x%d -> 0 : subproof\n", y);
            int neg_red = next_pid++;

            // proofgoal #1: clause|_{y=0} = clause with ~y replaced by 1
            // Since y is negative in clause, y=0 means ~y=1, so clause becomes trivially true
            fprintf(pf, "  proofgoal #1\n");
            next_pid++;
            fprintf(pf, "  qed ;\n");

            // proofgoals for pos-y clauses
            for (int pi = 0; pi < 2; pi++) {
                fprintf(pf, "  proofgoal %d\n", sub1_pos_pids[pi]);
                int neg_pg = next_pid++;
                // pos clause has y positive. Under y=0: y=0, so pos_clause|_{y=0} = other 2 lits >= 1
                // ¬(pos_clause|_{y=0}) = negate other 2 lits >= 2 (both forced)
                // ¬(current neg-y clause) = negate 3 lits >= 3 (all forced)
                // pol ¬pg + ¬red → trivially false (sum of coeff > max possible)
                fprintf(pf, "    pol %d %d + ;\n", neg_pg, neg_red);
                next_pid++;
                fprintf(pf, "  qed : %d ;\n", next_pid - 1);
            }

            // proofgoal for earlier neg-y clause (if ci == 1)
            if (ci == 1) {
                fprintf(pf, "  proofgoal %d\n", sub1_neg_pids[0]);
                next_pid++;
                // neg clause|_{y=0} = clause with ~y replaced by 1, trivially true
                fprintf(pf, "  qed ;\n");
            }

            fprintf(pf, "qed ;\n");
            sub1_neg_pids[ci] = next_pid++;
        }

        expanded[di.first_idx].clause_pids[0] = sub1_pos_pids[0];
        expanded[di.first_idx].clause_pids[1] = sub1_pos_pids[1];
        expanded[di.first_idx].clause_pids[2] = sub1_neg_pids[0];
        expanded[di.first_idx].clause_pids[3] = sub1_neg_pids[1];

        // Second sub-XOR: XOR(y,v2,v3)=0. vars order: [y,v2,v3]
        // y already has constraints from first sub-XOR.
        auto masks2 = get_xor3_masks(0);
        // bit 0=y, bit 1=v2, bit 2=v3
        vector<int> pos_y_masks2, neg_y_masks2;
        for (int m : masks2) {
            if (m & 1) neg_y_masks2.push_back(m); // y negated
            else pos_y_masks2.push_back(m); // y positive
        }

        int sub2_pos_pids[2];
        int sub2_neg_pids[2];

        // Introduce pos-y clauses (witness y→1, need subproof for first sub neg-y)
        for (int ci = 0; ci < 2; ci++) {
            int mask2 = pos_y_masks2[ci];
            fprintf(pf, "red ");
            int vars2[3] = {y, v2, v3};
            for (int j = 0; j < 3; j++)
                fprintf(pf, "1 %sx%d ", (mask2 >> j) & 1 ? "~" : "", vars2[j]);
            fprintf(pf, ">= 1 : x%d -> 1 : subproof\n", y);
            int neg_red = next_pid++;

            // proofgoal #1: y positive, y=1 makes it trivially true
            fprintf(pf, "  proofgoal #1\n");
            next_pid++;
            fprintf(pf, "  qed ;\n");

            // proofgoals for first sub neg-y clauses
            for (int pi = 0; pi < 2; pi++) {
                int neg_mask1 = neg_y_masks1[pi];
                fprintf(pf, "  proofgoal %d\n", sub1_neg_pids[pi]);
                int neg_pg = next_pid++;
                // sub1_neg has y negative. Under y=1: ~y=0, clause becomes other 2 lits >= 1
                // ¬(clause|_{y=1}) forces specific v0,v1 values
                // ¬(current) forces specific y,v2,v3 values
                // Combined: specific (v0,v1,v2,v3) with odd sum → use orig clause

                // Determine v0,v1 from ¬(sub1_neg|_{y=1})
                // sub1_neg_mask bits: 0=v0, 1=v1, 2=y. y=1 in assignment → bit2=1.
                // Under y=1: clause|_{y=1} uses remaining lits (bits 0,1 of mask).
                // ¬(clause|_{y=1}) inverts those bits.
                int v0_val = !((neg_mask1 >> 0) & 1); // inverted by negation of proofgoal
                int v1_val = !((neg_mask1 >> 1) & 1);
                // But wait: the mask indicates which vars are NEGATED in clause.
                // For bit 0 (v0): if mask bit=1, clause has ~v0. Under y=1, clause|_{y=1} has ~v0.
                // The FULL clause|_{y=1} is just the remaining literals (v0 and v1 parts).
                // If mask bit=0: literal is v0 (positive). If mask bit=1: literal is ~v0.
                // The clause is: lit_v0 + lit_v1 >= 1
                // ¬(clause) = ~lit_v0 + ~lit_v1 >= 2 → both forced to their negation
                // So v0_val = (mask bit 0 set ? 0 : 1)... no:
                // If mask bit 0 = 0: literal is v0 (positive). ¬literal = ~v0 >= 1 → v0=0.
                // If mask bit 0 = 1: literal is ~v0. ¬literal = v0 >= 1 → v0=1.
                v0_val = (neg_mask1 >> 0) & 1; // bit set means ~v0 in clause, ¬clause means v0=1
                v1_val = (neg_mask1 >> 1) & 1;

                // Determine v2,v3 from ¬(current)
                // current mask2 bits: 0=y, 1=v2, 2=v3. y is positive (bit0=0).
                // ¬current inverts all lits and needs sum >= 3.
                // So y=0 (from ¬(y literal)), v2 and v3 inverted.
                int v2_val = (mask2 >> 1) & 1; // bit set means ~v2 in clause, ¬clause means v2=1
                int v3_val = (mask2 >> 2) & 1;

                int orig_mask = (v0_val) | (v1_val << 1) | (v2_val << 2) | (v3_val << 3);
                auto it = orig_map.find(orig_mask);
                if (it == orig_map.end()) {
                    fprintf(stderr, "c Parity proof: missing orig mask=%d for decomp\n", orig_mask);
                    fclose(pf); return false;
                }
                fprintf(pf, "    pol %d %d + %d + ;\n", neg_pg, neg_red, it->second);
                next_pid++;
                fprintf(pf, "  qed : %d ;\n", next_pid - 1);
            }

            // proofgoal for earlier pos-y clause of second sub (if ci == 1)
            if (ci == 1) {
                fprintf(pf, "  proofgoal %d\n", sub2_pos_pids[0]);
                next_pid++;
                // y positive, y=1 → trivially true
                fprintf(pf, "  qed ;\n");
            }

            fprintf(pf, "qed ;\n");
            sub2_pos_pids[ci] = next_pid++;
        }

        // Introduce neg-y clauses (witness y→0, need subproof for all pos-y)
        for (int ci = 0; ci < 2; ci++) {
            int mask2 = neg_y_masks2[ci];
            fprintf(pf, "red ");
            int vars2[3] = {y, v2, v3};
            for (int j = 0; j < 3; j++)
                fprintf(pf, "1 %sx%d ", (mask2 >> j) & 1 ? "~" : "", vars2[j]);
            fprintf(pf, ">= 1 : x%d -> 0 : subproof\n", y);
            int neg_red = next_pid++;

            // proofgoal #1: y negative, y=0 → ~y=1, trivially true
            fprintf(pf, "  proofgoal #1\n");
            next_pid++;
            fprintf(pf, "  qed ;\n");

            // proofgoals for first sub pos-y clauses
            for (int pi = 0; pi < 2; pi++) {
                int pos_mask1 = pos_y_masks1[pi];
                fprintf(pf, "  proofgoal %d\n", sub1_pos_pids[pi]);
                int neg_pg = next_pid++;
                // sub1_pos has y positive. Under y=0: y=0, clause becomes remaining lits >= 1
                // ¬(proofgoal) inverts remaining lits → specific v0,v1
                // ¬(current) inverts all → specific y,v2,v3 (y=1)
                int v0_val = (pos_mask1 >> 0) & 1;
                int v1_val = (pos_mask1 >> 1) & 1;

                // current neg-y: y negative (bit0=1). ¬current: all inverted >= 3
                // bit0=1 (y neg in clause) → ¬(~y) = y=1
                // bit1: if set, ~v2 in clause → ¬ means v2=1. If not, v2 → ¬ means v2=0.
                // Wait, ¬clause means all literals are false. If literal is ~v2: ~v2=0 → v2=1.
                // If literal is v2: v2=0 → v2=0.
                int v2_val = (mask2 >> 1) & 1;
                int v3_val = (mask2 >> 2) & 1;

                int orig_mask = (v0_val) | (v1_val << 1) | (v2_val << 2) | (v3_val << 3);
                auto it = orig_map.find(orig_mask);
                if (it == orig_map.end()) {
                    fprintf(stderr, "c Parity proof: missing orig mask=%d for decomp neg\n", orig_mask);
                    fclose(pf); return false;
                }
                fprintf(pf, "    pol %d %d + %d + ;\n", neg_pg, neg_red, it->second);
                next_pid++;
                fprintf(pf, "  qed : %d ;\n", next_pid - 1);
            }

            // proofgoals for first sub neg-y clauses (NOT needed here since these
            // are neg-y, and under y=0 the first sub neg-y clauses have ~y=1 → trivially true)
            // Actually wait: under witness y=0, existing neg-y constraint|_{y=0}: ~y=1, trivially true.
            // But existing POS-y constraint|_{y=0}: y=0, remaining lits >= 1 (needs proof).
            // We already handled pos-y above.
            // For existing neg-y: |_{y=0} has ~y=1, trivially true → auto-proved.
            // Hmm but these aren't from sub1, they are also from sub2 pos.

            // proofgoals for sub2 pos-y clauses
            for (int pi = 0; pi < 2; pi++) {
                fprintf(pf, "  proofgoal %d\n", sub2_pos_pids[pi]);
                int neg_pg = next_pid++;
                // sub2_pos has y positive. Under y=0: remaining lits >= 1
                // ¬(proofgoal) and ¬(current) may conflict directly
                fprintf(pf, "    pol %d %d + ;\n", neg_pg, neg_red);
                next_pid++;
                fprintf(pf, "  qed : %d ;\n", next_pid - 1);
            }

            // proofgoal for earlier neg-y clause of second sub (if ci == 1)
            if (ci == 1) {
                fprintf(pf, "  proofgoal %d\n", sub2_neg_pids[0]);
                next_pid++;
                // y negative, y=0 → ~y=1, trivially true
                fprintf(pf, "  qed ;\n");
            }

            fprintf(pf, "qed ;\n");
            sub2_neg_pids[ci] = next_pid++;
        }

        expanded[di.second_idx].clause_pids[0] = sub2_pos_pids[0];
        expanded[di.second_idx].clause_pids[1] = sub2_pos_pids[1];
        expanded[di.second_idx].clause_pids[2] = sub2_neg_pids[0];
        expanded[di.second_idx].clause_pids[3] = sub2_neg_pids[1];
    }

    // Phase 1b: Introduce sub-XOR clauses for degree-5+ chain decompositions
    for (auto& gd : gen_decomps) {
        int k = (int)gd.orig_vars.size();
        int nsubs = k - 2;
        int nfresh = k - 3;
        int oi = gd.orig_idx;

        // Build orig clause map: assignment mask -> 1-based clause ID
        // mask bit j = 1 means orig_vars[j] is negated
        unordered_map<int,int> orig_map;
        for (int ci : clause_groups[oi]) {
            auto& clause = all_clauses[ci];
            int mask = 0;
            for (int l : clause) {
                int v = abs(l);
                for (int j = 0; j < k; j++)
                    if (v == gd.orig_vars[j] && l < 0) mask |= (1 << j);
            }
            orig_map[mask] = ci + 1; // 1-based PID
        }

        // For each sub, compute clause variable array and fresh_var position
        // sub_j vars: [a, b, c] sorted. We need to know which is the "new" fresh var.
        struct SubInfo {
            int vars_unsorted[3]; // [left, middle, right] before sort
            int vars_sorted[3];   // sorted
            int sort_perm[3];     // sort_perm[sorted_pos] = unsorted_pos
            int fresh_var;        // the NEW fresh variable introduced by this sub (-1 for last)
            int reused_var;       // the OLD fresh variable reused by this sub (-1 for sub_0)
            int parity;
        };
        vector<SubInfo> sub_infos(nsubs);

        for (int j = 0; j < nsubs; j++) {
            SubInfo& si = sub_infos[j];
            if (j == 0) {
                si.vars_unsorted[0] = gd.orig_vars[0];
                si.vars_unsorted[1] = gd.orig_vars[1];
                si.vars_unsorted[2] = gd.fresh_vars[0];
                si.fresh_var = gd.fresh_vars[0];
                si.reused_var = -1;
            } else if (j < nsubs - 1) {
                si.vars_unsorted[0] = gd.fresh_vars[j-1];
                si.vars_unsorted[1] = gd.orig_vars[j+1];
                si.vars_unsorted[2] = gd.fresh_vars[j];
                si.fresh_var = gd.fresh_vars[j];
                si.reused_var = gd.fresh_vars[j-1];
            } else {
                si.vars_unsorted[0] = gd.fresh_vars[j-1];
                si.vars_unsorted[1] = gd.orig_vars[k-2];
                si.vars_unsorted[2] = gd.orig_vars[k-1];
                si.fresh_var = -1; // no new fresh var
                si.reused_var = gd.fresh_vars[j-1];
            }
            si.parity = (j == 0) ? gd.parity : 0;

            // Compute sort permutation
            int idx[3] = {0, 1, 2};
            sort(idx, idx+3, [&](int a, int b) {
                return si.vars_unsorted[a] < si.vars_unsorted[b];
            });
            for (int q = 0; q < 3; q++) {
                si.vars_sorted[q] = si.vars_unsorted[idx[q]];
                si.sort_perm[q] = idx[q];
            }
        }

        // Get clause masks for XOR(a,b,c) = parity
        // masks[m]: 3 bits, bit j = 1 means vars_sorted[j] is negated
        auto get_masks_sorted = [](SubInfo& si) -> vector<int> {
            vector<int> masks;
            for (int m = 0; m < 8; m++) {
                int nc = __builtin_popcount(m);
                if (nc % 2 != si.parity) masks.push_back(m);
            }
            return masks;
        };

        // For each sub, find the position of fresh_var and reused_var in sorted order
        auto find_var_pos = [](SubInfo& si, int var) -> int {
            for (int j = 0; j < 3; j++)
                if (si.vars_sorted[j] == var) return j;
            return -1;
        };

        // Store clause PIDs for each sub: [pos0, pos1, neg0, neg1]
        // where pos = fresh_var positive (or reused_var for last sub)
        // neg = fresh_var negative (or reused_var for last sub)
        vector<array<int,4>> sub_pids(nsubs);

        // Introduce subs 0 through nsubs-2 (single-variable witness on NEW fresh var)
        for (int j = 0; j < nsubs - 1; j++) {
            SubInfo& si = sub_infos[j];
            int fv = si.fresh_var;
            int fv_pos = find_var_pos(si, fv);
            auto masks = get_masks_sorted(si);

            // Separate masks by fresh_var polarity
            vector<int> pos_masks, neg_masks;
            for (int m : masks) {
                if ((m >> fv_pos) & 1) neg_masks.push_back(m);
                else pos_masks.push_back(m);
            }

            // Introduce positive-fresh clauses (witness fv->1, auto-prove since fv is brand new)
            int pos_pids[2];
            for (int ci = 0; ci < 2; ci++) {
                int mask = pos_masks[ci];
                fprintf(pf, "red ");
                for (int q = 0; q < 3; q++)
                    fprintf(pf, "1 %sx%d ", (mask >> q) & 1 ? "~" : "", si.vars_sorted[q]);
                fprintf(pf, ">= 1 : x%d -> 1 ;\n", fv);
                pos_pids[ci] = next_pid++;
            }

            // Introduce negative-fresh clauses (witness fv->0)
            int neg_pids[2];
            for (int ci = 0; ci < 2; ci++) {
                int mask = neg_masks[ci];
                fprintf(pf, "red ");
                for (int q = 0; q < 3; q++)
                    fprintf(pf, "1 %sx%d ", (mask >> q) & 1 ? "~" : "", si.vars_sorted[q]);
                fprintf(pf, ">= 1 : x%d -> 0 : subproof\n", fv);
                int neg_red = next_pid++;

                // proofgoal #1: ~fv in clause, fv=0 → ~fv=1, trivially true
                fprintf(pf, "  proofgoal #1\n");
                next_pid++;
                fprintf(pf, "  qed ;\n");

                // proofgoals for pos-fresh clauses of same sub
                for (int pi = 0; pi < 2; pi++) {
                    fprintf(pf, "  proofgoal %d\n", pos_pids[pi]);
                    int neg_pg = next_pid++;
                    // pos has fv positive, fv=0 → fv=0, clause reduces to 2 lits
                    // neg_pg forces those 2 lits to specific values
                    // neg_red forces all 3 lits (including ~fv=1)
                    // pol neg_pg + neg_red → contradiction (conflicting forced values)
                    fprintf(pf, "    pol %d %d + ;\n", neg_pg, neg_red);
                    next_pid++;
                    fprintf(pf, "  qed : %d ;\n", next_pid - 1);
                }

                // proofgoal for earlier neg-fresh clause (if ci == 1)
                if (ci == 1) {
                    fprintf(pf, "  proofgoal %d\n", neg_pids[0]);
                    next_pid++;
                    // ~fv in clause, fv=0 → ~fv=1, trivially true
                    fprintf(pf, "  qed ;\n");
                }

                fprintf(pf, "qed ;\n");
                neg_pids[ci] = next_pid++;
            }

            sub_pids[j] = {pos_pids[0], pos_pids[1], neg_pids[0], neg_pids[1]};
            expanded[gd.expanded_indices[j]].clause_pids[0] = pos_pids[0];
            expanded[gd.expanded_indices[j]].clause_pids[1] = pos_pids[1];
            expanded[gd.expanded_indices[j]].clause_pids[2] = neg_pids[0];
            expanded[gd.expanded_indices[j]].clause_pids[3] = neg_pids[1];
        }

        // Introduce last sub (nsubs-1) with witness on reused_var (y_{k-4})
        // Cross-sub proofgoals from previous sub (nsubs-2) that mentions the reused_var
        {
            int last_j = nsubs - 1;
            int prev_j = nsubs - 2;
            SubInfo& si_last = sub_infos[last_j];
            SubInfo& si_prev = sub_infos[prev_j];
            int rv = si_last.reused_var; // y_{k-4}, shared between last and prev subs
            int rv_pos_last = find_var_pos(si_last, rv);
            int rv_pos_prev = find_var_pos(si_prev, rv);

            auto masks_last = get_masks_sorted(si_last);
            auto masks_prev = get_masks_sorted(si_prev);

            // Separate masks by reused_var polarity
            vector<int> pos_masks_last, neg_masks_last;
            for (int m : masks_last) {
                if ((m >> rv_pos_last) & 1) neg_masks_last.push_back(m);
                else pos_masks_last.push_back(m);
            }
            vector<int> pos_masks_prev, neg_masks_prev;
            for (int m : masks_prev) {
                if ((m >> rv_pos_prev) & 1) neg_masks_prev.push_back(m);
                else pos_masks_prev.push_back(m);
            }

            // Determine free extension vars and v0 for cross-sub proofgoal rup+pol chain
            // In cross-sub proofgoal:
            // neg_pg forces 2 vars from prev sub (the non-rv vars)
            // neg_red forces 3 vars from last sub (including rv)
            // Free: y0, ..., y_{prev_j-2} (extension vars not determined) and v0
            // For k=5: prev_j=1, free ext = y0..y_{-1} = none, free = {v0}
            // For k=6: prev_j=2, free ext = y0..y_0 = {y0}, free = {y0, v0}
            // For k=7: prev_j=3, free ext = y0..y_1 = {y0,y1}, free = {y0, y1, v0}
            int num_free_ext = prev_j - 1; // max(0, prev_j-1)
            if (num_free_ext < 0) num_free_ext = 0;
            int v0_var = gd.orig_vars[0]; // v0 is always free (for k>=5)

            // Collect free extension vars
            vector<int> free_ext_vars;
            for (int fi = 0; fi < num_free_ext; fi++)
                free_ext_vars.push_back(gd.fresh_vars[fi]);

            // Total free vars for rup: free_ext_vars + v0
            int num_free = (int)free_ext_vars.size() + 1; // includes v0
            int num_rup = 1 << num_free; // 2^num_free rup clauses needed

            // Helper: generate cross-sub proofgoal proof (rup + pol chain)
            // neg_pg_id and neg_red_id are the constraint IDs for ¬(proofgoal) and ¬(clause)
            auto gen_cross_sub_proof = [&](int neg_pg_id, int neg_red_id) {
                if (num_free == 0) {
                    // k=4 case: all vars determined, use orig clause
                    // (shouldn't happen for k>=5)
                    fprintf(pf, "    pol %d %d + ;\n", neg_pg_id, neg_red_id);
                    next_pid++;
                    fprintf(pf, "  qed : %d ;\n", next_pid - 1);
                    return;
                }

                // Generate 2^num_free rup clauses
                // Each rup clause has num_free literals: one for each free var
                // Polarity determined by sign combination
                // The rup negate forces all free vars to specific values,
                // BCP propagates through chain and original clauses to conflict
                vector<int> rup_ids(num_rup);
                for (int combo = 0; combo < num_rup; combo++) {
                    fprintf(pf, "    rup");
                    // Bits 0..num_free_ext-1 = ext vars, bit num_free_ext = v0
                    for (int fi = 0; fi < (int)free_ext_vars.size(); fi++) {
                        int pol_bit = (combo >> fi) & 1;
                        fprintf(pf, " 1 %sx%d", pol_bit ? "~" : "", free_ext_vars[fi]);
                    }
                    int v0_pol = (combo >> num_free_ext) & 1;
                    fprintf(pf, " 1 %sx%d", v0_pol ? "~" : "", v0_var);
                    fprintf(pf, " >= 1 ;\n");
                    rup_ids[combo] = next_pid++;
                }

                // Eliminate free extension vars one at a time using pol add + div
                // After eliminating ext var fi, we have 2^(num_free-1-fi) constraints
                vector<int> current_ids = rup_ids;
                for (int fi = (int)free_ext_vars.size() - 1; fi >= 0; fi--) {
                    // Pair up constraints that differ only in bit fi
                    int half = (int)current_ids.size() / 2;
                    vector<int> new_ids(half);
                    for (int ci2 = 0; ci2 < (int)current_ids.size(); ci2++) {
                        if ((ci2 >> fi) & 1) continue; // skip odd partner
                        int partner = ci2 | (1 << fi);
                        int pair_idx = 0;
                        // Compute index in new_ids: remove bit fi from ci2
                        int idx = 0;
                        for (int b = 0, bpos = 0; b < num_free; b++) {
                            if (b == fi) continue;
                            if ((ci2 >> b) & 1) idx |= (1 << bpos);
                            bpos++;
                        }
                        // pol add
                        fprintf(pf, "    pol %d %d + ;\n", current_ids[ci2], current_ids[partner]);
                        int sum_id = next_pid++;
                        // pol div 2
                        fprintf(pf, "    pol %d 2 d ;\n", sum_id);
                        new_ids[idx] = next_pid++;
                    }
                    current_ids = new_ids;
                }

                // Now current_ids has 2 entries: one for v0 positive, one for v0 negative
                // current_ids[0] = v0 >= 1, current_ids[1] = ~v0 >= 1
                // Final pol add for contradiction
                fprintf(pf, "    pol %d %d + ;\n", current_ids[0], current_ids[1]);
                next_pid++;
                fprintf(pf, "  qed : %d ;\n", next_pid - 1);
            };

            // Introduce positive-rv clauses (witness rv->1)
            int last_pos_pids[2];
            for (int ci = 0; ci < 2; ci++) {
                int mask = pos_masks_last[ci];
                fprintf(pf, "red ");
                for (int q = 0; q < 3; q++)
                    fprintf(pf, "1 %sx%d ", (mask >> q) & 1 ? "~" : "", si_last.vars_sorted[q]);
                fprintf(pf, ">= 1 : x%d -> 1 : subproof\n", rv);
                int neg_red = next_pid++;

                // proofgoal #1: rv positive, rv=1 → trivially true
                fprintf(pf, "  proofgoal #1\n");
                next_pid++;
                fprintf(pf, "  qed ;\n");

                // Cross-sub proofgoals: prev sub's neg-rv clauses
                // (rv negative in prev sub, under rv=1: ~rv=0, reduces to 2 lits)
                for (int pi = 0; pi < 2; pi++) {
                    fprintf(pf, "  proofgoal %d\n", sub_pids[prev_j][2+pi]); // neg-rv pids
                    int neg_pg = next_pid++;
                    gen_cross_sub_proof(neg_pg, neg_red);
                }

                // Prev sub's pos-rv clauses: rv=1, rv positive → trivially true
                for (int pi = 0; pi < 2; pi++) {
                    fprintf(pf, "  proofgoal %d\n", sub_pids[prev_j][pi]); // pos-rv pids
                    next_pid++;
                    fprintf(pf, "  qed ;\n");
                }

                // Earlier same-sub pos-rv clause (if ci == 1)
                if (ci == 1) {
                    fprintf(pf, "  proofgoal %d\n", last_pos_pids[0]);
                    next_pid++;
                    // rv positive, rv=1 → trivially true
                    fprintf(pf, "  qed ;\n");
                }

                fprintf(pf, "qed ;\n");
                last_pos_pids[ci] = next_pid++;
            }

            // Introduce negative-rv clauses (witness rv->0)
            int last_neg_pids[2];
            for (int ci = 0; ci < 2; ci++) {
                int mask = neg_masks_last[ci];
                fprintf(pf, "red ");
                for (int q = 0; q < 3; q++)
                    fprintf(pf, "1 %sx%d ", (mask >> q) & 1 ? "~" : "", si_last.vars_sorted[q]);
                fprintf(pf, ">= 1 : x%d -> 0 : subproof\n", rv);
                int neg_red = next_pid++;

                // proofgoal #1: rv negative, rv=0 → ~rv=1, trivially true
                fprintf(pf, "  proofgoal #1\n");
                next_pid++;
                fprintf(pf, "  qed ;\n");

                // Cross-sub proofgoals: prev sub's pos-rv clauses
                // (rv positive in prev sub, under rv=0: rv=0, reduces to 2 lits)
                for (int pi = 0; pi < 2; pi++) {
                    fprintf(pf, "  proofgoal %d\n", sub_pids[prev_j][pi]); // pos-rv pids
                    int neg_pg = next_pid++;
                    gen_cross_sub_proof(neg_pg, neg_red);
                }

                // Prev sub's neg-rv clauses: rv=0, rv negative → ~rv=1 → trivially true
                for (int pi = 0; pi < 2; pi++) {
                    fprintf(pf, "  proofgoal %d\n", sub_pids[prev_j][2+pi]); // neg-rv pids
                    next_pid++;
                    fprintf(pf, "  qed ;\n");
                }

                // Same-sub pos-rv clauses: rv=0, rv positive → need proof
                for (int pi = 0; pi < 2; pi++) {
                    fprintf(pf, "  proofgoal %d\n", last_pos_pids[pi]);
                    int neg_pg = next_pid++;
                    // Same-sub: pos clause under rv=0 → 2 lits. neg_pg + neg_red → contradiction
                    fprintf(pf, "    pol %d %d + ;\n", neg_pg, neg_red);
                    next_pid++;
                    fprintf(pf, "  qed : %d ;\n", next_pid - 1);
                }

                // Earlier same-sub neg-rv clause (if ci == 1)
                if (ci == 1) {
                    fprintf(pf, "  proofgoal %d\n", last_neg_pids[0]);
                    next_pid++;
                    // rv negative, rv=0 → ~rv=1 → trivially true
                    fprintf(pf, "  qed ;\n");
                }

                fprintf(pf, "qed ;\n");
                last_neg_pids[ci] = next_pid++;
            }

            // Store clause PIDs: [pos0, pos1, neg0, neg1]
            // pos = rv positive in clause (sorted), neg = rv negative
            sub_pids[last_j] = {last_pos_pids[0], last_pos_pids[1],
                                last_neg_pids[0], last_neg_pids[1]};
            int ei = gd.expanded_indices[last_j];
            expanded[ei].clause_pids[0] = last_pos_pids[0];
            expanded[ei].clause_pids[1] = last_pos_pids[1];
            expanded[ei].clause_pids[2] = last_neg_pids[0];
            expanded[ei].clause_pids[3] = last_neg_pids[1];
        }
    }

    // Phase 2: Find isolation clauses for all expanded XORs
    struct IsoData {
        unordered_map<int,int> iso; // var -> clause_pid
        int all_neg; // for p=0 (clause with all 3 vars negated)
        IsoData() : all_neg(-1) {}
    };
    vector<IsoData> iso_data(nexp);

    for (int ei = 0; ei < nexp; ei++) {
        auto& ex = expanded[ei];
        int k = (int)ex.vars.size();
        int p = ex.parity;

        if (k == 2) {
            // For degree-2, find and store the clause PIDs directly
            // Need to find the original XOR index
            int oi = -1;
            if (ex.is_original) {
                for (int i = 0; i < nxors; i++) {
                    vector<int> sv = xors[i].vars; sort(sv.begin(), sv.end());
                    vector<int> ev = ex.vars; sort(ev.begin(), ev.end());
                    if (sv == ev && xors[i].parity == ex.parity) { oi = i; break; }
                }
                if (oi < 0) { fclose(pf); return false; }
                for (int ci : clause_groups[oi]) {
                    auto& clause = all_clauses[ci];
                    int mask = 0;
                    for (int l : clause) {
                        int v = abs(l);
                        for (int j = 0; j < 2; j++)
                            if (v == ex.vars[j] && l < 0) mask |= (1 << j);
                    }
                    int nc = __builtin_popcount(mask);
                    // For p=0: isolation clause has 1 neg var
                    if (p == 0 && nc == 1) {
                        for (int j = 0; j < 2; j++)
                            if ((mask >> j) & 1) iso_data[ei].iso[ex.vars[j]] = ci + 1;
                    }
                    // For p=1: store all-neg clause (both negated)
                    if (p == 1 && nc == 2) iso_data[ei].all_neg = ci + 1;
                    // For p=1: no sole_pos exists for k=2, but we don't need it
                }
            }
            continue;
        }

        if (k == 4) continue; // degree-4 p=1 uses pair clauses (Phase 3), not isolation

        auto analyze = [&](int pid, int mask, int vv[3]) {
            int nc = __builtin_popcount(mask);
            int sole_pos = -1, sole_neg = -1;
            int pos_count = 0, neg_count = 0;
            for (int j = 0; j < 3; j++) {
                if ((mask >> j) & 1) { neg_count++; sole_neg = vv[j]; }
                else { pos_count++; sole_pos = vv[j]; }
            }
            if (p == 1 && pos_count == 1) iso_data[ei].iso[sole_pos] = pid;
            if (p == 0 && neg_count == 1) iso_data[ei].iso[sole_neg] = pid;
            if (p == 0 && neg_count == 3) iso_data[ei].all_neg = pid;
        };

        if (ex.is_original) {
            int oi = -1;
            // Find original index
            for (int i = 0; i < nxors; i++) {
                if (xors[i].vars == ex.vars && xors[i].parity == ex.parity) { oi = i; break; }
            }
            if (oi < 0) {
                // try matching by sorted vars
                for (int i = 0; i < nxors; i++) {
                    vector<int> sv = xors[i].vars;
                    sort(sv.begin(), sv.end());
                    vector<int> ev = ex.vars;
                    sort(ev.begin(), ev.end());
                    if (sv == ev && xors[i].parity == ex.parity) { oi = i; break; }
                }
            }
            if (oi < 0) { fclose(pf); return false; }

            for (int ci : clause_groups[oi]) {
                auto& clause = all_clauses[ci];
                int mask = 0;
                int vv[3];
                for (int j = 0; j < k; j++) vv[j] = ex.vars[j];
                for (int l : clause) {
                    int v = abs(l);
                    for (int j = 0; j < k; j++)
                        if (v == vv[j] && l < 0) mask |= (1 << j);
                }
                analyze(ci + 1, mask, vv);
            }
        } else {
            // Introduced sub-XOR
            int vv[3] = {ex.vars[0], ex.vars[1], ex.vars[2]};
            // Find the fresh variable's position
            int fresh_pos = -1;
            for (int j = 0; j < 3; j++) if (vv[j] > nvars) fresh_pos = j;

            auto masks = get_xor3_masks(p);
            vector<int> pos_fresh, neg_fresh;
            for (int m : masks) {
                if (fresh_pos >= 0 && ((m >> fresh_pos) & 1)) neg_fresh.push_back(m);
                else pos_fresh.push_back(m);
            }
            // PIDs: [pos0, pos1, neg0, neg1]
            for (int j = 0; j < 2; j++) analyze(ex.clause_pids[j], pos_fresh[j], vv);
            for (int j = 0; j < 2; j++) analyze(ex.clause_pids[2+j], neg_fresh[j], vv);
        }

        // Verify (k==3 only; k==4 is handled by pair clauses, k==2 by direct subproof)
        if (k == 3) {
            for (int v : ex.vars) {
                if (iso_data[ei].iso.find(v) == iso_data[ei].iso.end()) {
                    fprintf(stderr, "c Parity proof: missing iso var %d in expanded %d (p=%d)\n", v, ei, p);
                    fclose(pf); return false;
                }
            }
            if (p == 0 && iso_data[ei].all_neg < 0) {
                fprintf(stderr, "c Parity proof: missing all-neg for expanded %d\n", ei);
                fclose(pf); return false;
            }
        }
    }

    // Phase 3: For degree-4 p=1 XORs, find pair clauses and all-neg clause
    struct PairInfo {
        map<pair<int,int>,int> pair_clause;
        int all_neg_pid; // clause with all 4 vars negated (for proofgoal #1)
        PairInfo() : all_neg_pid(-1) {}
    };
    vector<PairInfo> pair_info(nexp);
    for (int ei = 0; ei < nexp; ei++) {
        auto& ex = expanded[ei];
        if ((int)ex.vars.size() != 4 || ex.parity != 1) continue;
        // Find original index
        int oi = -1;
        for (int i = 0; i < nxors; i++) {
            if (xors[i].vars == ex.vars && xors[i].parity == ex.parity) { oi = i; break; }
        }
        if (oi < 0) { fclose(pf); return false; }
        for (int ci : clause_groups[oi]) {
            auto& clause = all_clauses[ci];
            vector<int> neg_vars;
            int neg_count = 0;
            for (int l : clause) if (l < 0) { neg_vars.push_back(-l); neg_count++; }
            if (neg_count == 2) {
                int a = min(neg_vars[0], neg_vars[1]);
                int b = max(neg_vars[0], neg_vars[1]);
                pair_info[ei].pair_clause[{a,b}] = ci + 1;
            }
            if (neg_count == 4) {
                pair_info[ei].all_neg_pid = ci + 1;
            }
        }
    }

    // Phase 4: A/B constraints with subproofs
    vector<int> A_pids(nexp), B_pids(nexp);
    for (int ei = 0; ei < nexp; ei++) {
        auto& ex = expanded[ei];
        int k = (int)ex.vars.size();
        int p = ex.parity;
        int z = next_fresh++;

        fprintf(pf, "red ");
        for (int v : ex.vars) fprintf(pf, "1 x%d ", v);
        fprintf(pf, "2 ~x%d >= %d : x%d -> 0 ;\n", z, p+2, z);
        A_pids[ei] = next_pid++;

        fprintf(pf, "red ");
        for (int v : ex.vars) fprintf(pf, "1 ~x%d ", v);
        fprintf(pf, "2 x%d >= %d : x%d -> 1", z, k-p, z);

        if (k == 2) {
            fprintf(pf, " : subproof\n");
            int neg_b = next_pid++;
            if (p == 0) {
                // B|_{z=1} = ~a+~b >= 0 → trivially true
                fprintf(pf, "  proofgoal #1\n");
                next_pid++;
                fprintf(pf, "  qed ;\n");
                // Proofgoal A: A|_{z=1} = a+b >= 2. Derive from ¬B + iso clauses.
                fprintf(pf, "  proofgoal %d\n", A_pids[ei]);
                int neg_pg = next_pid++;
                (void)neg_pg;
                // For each var, derive it >= 1
                vector<int> vids;
                for (int v : ex.vars) {
                    auto it = iso_data[ei].iso.find(v);
                    if (it == iso_data[ei].iso.end()) {
                        fprintf(stderr, "c Parity proof: missing iso for var %d in deg-2 p=0\n", v);
                        fclose(pf); return false;
                    }
                    // pol ¬B + iso_clause → 2*v_target + complementary + 2~z >= 3+1=4
                    // After div 2: v_target + ~z >= 2
                    // Then rup: v_target >= 1
                    fprintf(pf, "    pol %d %d + ;\n", neg_b, it->second);
                    next_pid++;
                    fprintf(pf, "    pol %d 2 d ;\n", next_pid-1);
                    next_pid++;
                    fprintf(pf, "    rup 1 x%d >= 1 ;\n", v);
                    vids.push_back(next_pid++);
                }
                // Sum the rup constraints and add neg_pg for contradiction
                fprintf(pf, "    pol %d", vids[0]);
                for (size_t j = 1; j < vids.size(); j++) fprintf(pf, " %d +", vids[j]);
                fprintf(pf, " ;\n"); next_pid++;
                // Add neg_pg: ¬(a+b >= 2) = ~a+~b >= 1
                // sum(vars >= 1) = a+b >= 2, plus ~a+~b >= 1 → 2+1 >= 3 → contradiction
                fprintf(pf, "    pol %d %d + ;\n", next_pid-1, neg_pg);
                fprintf(pf, "  qed : %d ;\n", next_pid++);
            } else {
                // p=1: B|_{z=1} = ~a+~b+2 >= 1 → trivially true
                fprintf(pf, "  proofgoal #1\n");
                next_pid++;
                fprintf(pf, "  qed ;\n");
                // Proofgoal A: A|_{z=1} = a+b >= 3 → contradiction
                fprintf(pf, "  proofgoal %d\n", A_pids[ei]);
                int neg_pg = next_pid++;
                // ¬B = a+b+2~z >= 4. pol ¬B + all_neg_clause(~a+~b >= 1):
                // a+b+2~z + ~a+~b >= 5 → 2+2~z >= 5 → 2~z >= 3. div 2: ~z >= 2 → 0>=1
                int all_neg_pid = iso_data[ei].all_neg;
                if (all_neg_pid < 0) {
                    fprintf(stderr, "c Parity proof: missing all-neg for deg-2 p=1\n");
                    fclose(pf); return false;
                }
                fprintf(pf, "    pol %d %d + ;\n", neg_b, all_neg_pid);
                next_pid++;
                fprintf(pf, "    pol %d 2 d ;\n", next_pid-1);
                fprintf(pf, "  qed : %d ;\n", next_pid++);
            }
            fprintf(pf, "qed ;\n");
            B_pids[ei] = next_pid++;
        } else if (k == 3) {
            fprintf(pf, " : subproof\n");
            int neg_b = next_pid++;
            fprintf(pf, "  proofgoal #1\n");
            int neg_s1 = next_pid++;
            if (p == 0) {
                fprintf(pf, "    pol %d %d + ;\n", neg_s1, iso_data[ei].all_neg);
                fprintf(pf, "  qed : %d ;\n", next_pid++);
            } else {
                fprintf(pf, "  qed ;\n");
            }
            fprintf(pf, "  proofgoal %d\n", A_pids[ei]);
            int neg_pg = next_pid++;
            if (p == 1) {
                vector<int> vids;
                for (int v : ex.vars) {
                    fprintf(pf, "    pol %d %d + ;\n", neg_b, iso_data[ei].iso[v]);
                    next_pid++;
                    fprintf(pf, "    pol %d 2 d ;\n", next_pid-1);
                    next_pid++;
                    fprintf(pf, "    rup 1 x%d >= 1 ;\n", v);
                    vids.push_back(next_pid++);
                }
                fprintf(pf, "    pol %d", vids[0]);
                for (size_t j = 1; j < vids.size(); j++) fprintf(pf, " %d +", vids[j]);
                fprintf(pf, " ;\n"); next_pid++;
                fprintf(pf, "    pol %d %d + ;\n", next_pid-1, neg_pg);
                fprintf(pf, "  qed : %d ;\n", next_pid++);
            } else {
                vector<int> vids;
                for (int v : ex.vars) {
                    fprintf(pf, "    pol %d %d + ;\n", neg_pg, iso_data[ei].iso[v]);
                    next_pid++;
                    fprintf(pf, "    pol %d 2 d ;\n", next_pid-1);
                    next_pid++;
                    fprintf(pf, "    rup 1 ~x%d >= 1 ;\n", v);
                    vids.push_back(next_pid++);
                }
                fprintf(pf, "    pol %d", neg_b);
                for (int vid : vids) fprintf(pf, " %d +", vid);
                fprintf(pf, " ;\n");
                fprintf(pf, "  qed : %d ;\n", next_pid++);
            }
            fprintf(pf, "qed ;\n");
            B_pids[ei] = next_pid++;
        } else {
            // k=4, p=1
            fprintf(pf, " : subproof\n");
            int neg_b = next_pid++;
            // proofgoal #1: B|_{z=1} = ~v1+~v2+~v3+~v4 >= 1
            // Need the all-negative clause (all vars negated) to derive this
            fprintf(pf, "  proofgoal #1\n");
            int neg_s1 = next_pid++;
            // ¬(B|_{z=1}) = v1+v2+v3+v4 >= 4 (all vars must be 1)
            // pol neg_s1 + all_neg_clause (which is ~v1+~v2+~v3+~v4 >= 1)
            // → v1+v2+v3+v4 + ~v1+~v2+~v3+~v4 >= 5 → 4 >= 5 → 0 >= 1
            {
                int anp = pair_info[ei].all_neg_pid;
                if (anp < 0) {
                    fprintf(stderr, "c Parity proof: missing all-neg for k=4 p=1\n");
                    fclose(pf); return false;
                }
                fprintf(pf, "    pol %d %d + ;\n", neg_s1, anp);
                fprintf(pf, "  qed : %d ;\n", next_pid++);
            }
            fprintf(pf, "  proofgoal %d\n", A_pids[ei]);
            int neg_pg = next_pid++;
            // Derive pair constraints from neg_pg + pair_clause, div by 2
            // neg_pg = ~v1+~v2+~v3+~v4 >= 2
            // pair_clause has 2 negatives: e.g. ~vi+~vj+vk+vl >= 1
            // Sum: 2~vi+2~vj+(~vk+vk)+(~vl+vl) >= 3 → 2~vi+2~vj+2 >= 3 → 2~vi+2~vj >= 1
            // Div 2 (ceil): ~vi+~vj >= 1
            for (auto& [pr,cid] : pair_info[ei].pair_clause) {
                fprintf(pf, "    pol %d %d + ;\n", neg_pg, cid);
                next_pid++;
                fprintf(pf, "    pol %d 2 d ;\n", next_pid-1);
                next_pid++;
            }
            // Use RUP to derive ~vi >= 1 for each variable
            // RUP: negate (~vi >= 1) → vi=1. Then pair constraints for vi force
            // all other vars to 0. Then neg_b gives contradiction.
            vector<int> neg_var_pids;
            for (int v : ex.vars) {
                fprintf(pf, "    rup 1 ~x%d >= 1 ;\n", v);
                neg_var_pids.push_back(next_pid++);
            }
            // Sum all ~vi >= 1
            fprintf(pf, "    pol %d", neg_var_pids[0]);
            for (size_t j = 1; j < neg_var_pids.size(); j++) fprintf(pf, " %d +", neg_var_pids[j]);
            fprintf(pf, " ;\n"); next_pid++;
            // Add neg_b: sum(~vi >= 4) + (v1+v2+v3+v4+2~z >= 4) = 4+2~z >= 8 → contradiction
            fprintf(pf, "    pol %d %d + ;\n", next_pid-1, neg_b);
            fprintf(pf, "  qed : %d ;\n", next_pid++);
            fprintf(pf, "qed ;\n");
            B_pids[ei] = next_pid++;
        }
    }

    // Phase 5: Parity sum
    fprintf(pf, "pol %d", A_pids[0]);
    for (int i = 1; i < nexp; i++) fprintf(pf, " %d +", A_pids[i]);
    fprintf(pf, " ;\n"); next_pid++;
    fprintf(pf, "pol %d 2 d ;\n", next_pid-1); int da = next_pid++;
    fprintf(pf, "pol %d", B_pids[0]);
    for (int i = 1; i < nexp; i++) fprintf(pf, " %d +", B_pids[i]);
    fprintf(pf, " ;\n"); next_pid++;
    fprintf(pf, "pol %d 2 d ;\n", next_pid-1); int db = next_pid++;
    fprintf(pf, "pol %d %d + ;\n", da, db);
    int fp = next_pid++;
    fprintf(pf, "output NONE ;\n");
    fprintf(pf, "conclusion UNSAT : %d ;\n", fp);
    fprintf(pf, "end pseudo-Boolean proof ;\n");
    fclose(pf);
    fprintf(stderr, "c Parity proof complete: %d constraints\n", next_pid-1);
    return true;
}
// ---- Graph-based proof generation ----
bool gen_proof(const char* pf, const vector<XORConstraint>& xors) {
    int nxors = xors.size();

    // Group clauses by variable set -> XOR index
    struct VH { size_t operator()(const vector<int>&v) const {
        size_t h=v.size(); for(int x:v) h=h*1000003+x; return h; }};
    unordered_map<vector<int>,int,VH> vs2xi;
    vector<vector<int>> xor_clause_indices(nxors);
    for (int i = 0; i < nxors; i++) vs2xi[xors[i].vars] = i;

    for (int ci = 0; ci < (int)all_clauses.size(); ci++) {
        vector<int> vs;
        for (int l : all_clauses[ci]) vs.push_back(abs(l));
        sort(vs.begin(), vs.end());
        auto it = vs2xi.find(vs);
        if (it != vs2xi.end()) xor_clause_indices[it->second].push_back(ci);
    }

    // Build Tseitin graph: var -> list of XOR constraint indices
    unordered_map<int, vector<int>> var2xors;
    for (int i = 0; i < nxors; i++)
        for (int v : xors[i].vars) var2xors[v].push_back(i);

    // Build adjacency graph of XOR constraints for ordering
    vector<vector<int>> xor_adj(nxors);
    vector<int> xor_degree(nxors, 0);
    for (auto& [v, xis] : var2xors) {
        if ((int)xis.size() == 2) {
            xor_adj[xis[0]].push_back(xis[1]);
            xor_adj[xis[1]].push_back(xis[0]);
            xor_degree[xis[0]]++;
            xor_degree[xis[1]]++;
        }
    }

    // Find pseudo-peripheral nodes using double BFS
    auto find_peripheral = [&](int start) -> int {
        vector<int> dist(nxors, -1);
        queue<int> q;
        dist[start] = 0;
        q.push(start);
        int farthest = start;
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : xor_adj[u]) {
                if (dist[v] < 0) {
                    dist[v] = dist[u] + 1;
                    q.push(v);
                    if (dist[v] > dist[farthest] ||
                        (dist[v] == dist[farthest] && xor_degree[v] < xor_degree[farthest]))
                        farthest = v;
                }
            }
        }
        return farthest;
    };

    // Collect good starting nodes
    vector<int> good_starts;

    // 1. Nodes with minimum degree
    int min_deg = INT_MAX;
    for (int i = 0; i < nxors; i++) min_deg = min(min_deg, xor_degree[i]);
    for (int i = 0; i < nxors; i++)
        if (xor_degree[i] <= min_deg + 1) good_starts.push_back(i);

    // 2. Pseudo-peripheral nodes (double BFS from a few starting points)
    for (int tries = 0; tries < min(5, nxors); tries++) {
        int s = tries * nxors / max(5, nxors);
        int p1 = find_peripheral(s);
        int p2 = find_peripheral(p1);
        good_starts.push_back(p1);
        good_starts.push_back(p2);
    }

    // 3. First few nodes (original approach)
    for (int i = 0; i < min(50, nxors); i++) good_starts.push_back(i);

    // 4. Random nodes for more coverage
    srand(42);
    for (int i = 0; i < 200; i++) good_starts.push_back(rand() % nxors);

    // Remove duplicates
    sort(good_starts.begin(), good_starts.end());
    good_starts.erase(unique(good_starts.begin(), good_starts.end()), good_starts.end());

    OrderResult best_order;
    best_order.max_combine_width = INT_MAX;
    best_order.estimated_lines = (long long)1e18;

    // Try DFS post-order from peripheral nodes (excellent for trees, fast)
    {
        // Find peripheral nodes via double-BFS
        auto bfs_far = [&](int start) -> int {
            vector<int> dist(nxors, -1);
            queue<int> q;
            dist[start] = 0; q.push(start);
            int farthest = start;
            while (!q.empty()) {
                int u = q.front(); q.pop();
                for (int v : xor_adj[u]) {
                    if (dist[v] < 0) {
                        dist[v] = dist[u] + 1;
                        q.push(v);
                        if (dist[v] > dist[farthest]) farthest = v;
                    }
                }
            }
            return farthest;
        };

        // Try DFS from several peripheral nodes
        vector<int> dfs_starts;
        for (int t = 0; t < min(3, nxors); t++) {
            int s = t * nxors / max(3, nxors);
            int p1 = bfs_far(s);
            int p2 = bfs_far(p1);
            dfs_starts.push_back(p1);
            dfs_starts.push_back(p2);
        }
        // Also try min-degree nodes
        int md = INT_MAX;
        for (int i = 0; i < nxors; i++) md = min(md, (int)xor_adj[i].size());
        for (int i = 0; i < nxors && (int)dfs_starts.size() < 20; i++)
            if ((int)xor_adj[i].size() == md) dfs_starts.push_back(i);

        sort(dfs_starts.begin(), dfs_starts.end());
        dfs_starts.erase(unique(dfs_starts.begin(), dfs_starts.end()), dfs_starts.end());

        for (int s : dfs_starts) {
            auto res = dfs_postorder(xors, var2xors, xor_adj, s);
            if (res.max_combine_width < best_order.max_combine_width ||
                (res.max_combine_width == best_order.max_combine_width &&
                 res.estimated_lines < best_order.estimated_lines)) {
                best_order = res;
            }
        }
        fprintf(stderr, "c DFS ordering (from %d starts): best max_combine_width=%d, estimated_lines=%lld\n",
                (int)dfs_starts.size(), best_order.max_combine_width, best_order.estimated_lines);
    }

    // Try spectral-like ordering (best for grid-like graphs)
    {
        auto res = spectral_ordering(xors, var2xors, xor_adj);
        fprintf(stderr, "c Spectral ordering: max_combine_width=%d, estimated_lines=%lld\n",
                res.max_combine_width, res.estimated_lines);
        if (res.max_combine_width < best_order.max_combine_width ||
            (res.max_combine_width == best_order.max_combine_width &&
             res.estimated_lines < best_order.estimated_lines)) {
            best_order = res;
        }
        // Also try reversed spectral ordering
        auto rres = res;
        reverse(rres.order.begin(), rres.order.end());
        compute_order_stats(rres, xors);
        if (rres.max_combine_width < best_order.max_combine_width ||
            (rres.max_combine_width == best_order.max_combine_width &&
             rres.estimated_lines < best_order.estimated_lines)) {
            best_order = rres;
        }
    }

    double order_time_start = get_time();
    int actual_starts = 0;

    // Skip greedy search for very large instances - DFS/spectral is sufficient
    // For medium instances (up to ~20K XORs), greedy is still feasible within time budget
    if (nxors <= 20000) {
        for (int si = 0; si < (int)good_starts.size(); si++) {
            if (get_time() - order_time_start > 30.0) break;
            if (si >= 10 && best_order.max_combine_width > 26) break;
            actual_starts = si + 1;
            int s = good_starts[si];
            for (int h = 0; h < 2; h++) {
                auto res = compute_ordering(xors, var2xors, s, h);
                if (res.max_combine_width < best_order.max_combine_width ||
                    (res.max_combine_width == best_order.max_combine_width &&
                     res.estimated_lines < best_order.estimated_lines)) {
                    best_order = res;
                }
            }
        }
    } else {
        fprintf(stderr, "c Skipping greedy ordering for large instance (%d XORs), using DFS/spectral\n", nxors);
    }

    fprintf(stderr, "c Best ordering: max_combine_width=%d, estimated_lines=%lld (tried %d starts + min-degree)\n",
            best_order.max_combine_width, best_order.estimated_lines, actual_starts);

    // Budget: max 300M lines, max combine width 26
    if (best_order.max_combine_width > 26 || best_order.estimated_lines > 300000000LL) {
        fprintf(stderr, "c Proof too large (width=%d, est_lines=%lld)\n",
                best_order.max_combine_width, best_order.estimated_lines);
        return false;
    }

    // Now generate the proof with the best ordering
    vector<int>& order = best_order.order;

    FILE* f = fopen(pf, "w");
    if (!f) return false;
    fprintf(f, "pseudo-Boolean proof version 2.0\n");

    // Derive virtual clauses (from XOR repair) via RUP
    int original_nclauses = nclauses; // Original clause count before virtual additions
    int next_id = original_nclauses + 1;
    long long total_lines = 0;

    for (auto& vc : virtual_clauses) {
        fprintf(f, "rup");
        for (int l : vc.lits) {
            fprintf(f, " 1 %sx%d", l < 0 ? "~" : "", abs(l));
        }
        fprintf(f, " >= 1 ;\n");
        next_id++;
        total_lines++;
    }
    // Virtual clause i gets proof ID = original_nclauses + 1 + i
    // In all_clauses, virtual clause i is at index original_nclauses + i (0-based)
    // build_live uses ci+1 as proof ID, so virtual clause at index original_nclauses+i
    // gets ID original_nclauses+i+1, which should match next_id after the RUP loop.

    // Build initial LiveXOR for first constraint
    LiveXOR accumulated = build_live(xors[order[0]], xor_clause_indices[order[0]]);

    for (int step = 1; step < nxors; step++) {
        int xi = order[step];
        LiveXOR current = build_live(xors[xi], xor_clause_indices[xi]);

        // Check if they share any variables
        vector<int> shared;
        {
            int ai=0, bi=0;
            while (ai<(int)accumulated.vars.size() && bi<(int)current.vars.size()) {
                if (accumulated.vars[ai]==current.vars[bi]) { shared.push_back(accumulated.vars[ai]); ai++; bi++; }
                else if (accumulated.vars[ai]<current.vars[bi]) ai++; else bi++;
            }
        }

        if (shared.empty()) {
            // Check if accumulated is already a contradiction
            if (accumulated.vars.empty() && accumulated.parity == 1 && accumulated.pat2id.count(0)) {
                fprintf(f, "output NONE\n");
                fprintf(f, "conclusion UNSAT : %d\n", accumulated.pat2id[0]);
                fprintf(f, "end pseudo-Boolean proof\n");
                fclose(f);
                return true;
            }
            continue;
        }

        LiveXOR combined;
        bool ok = combine_live(accumulated, current, combined, f, next_id, total_lines);

        if (!ok) {
            fprintf(stderr, "c Combine failed at step %d/%d (acc_width=%d, cur_width=%d, shared=%d)\n",
                    step, nxors, (int)accumulated.vars.size(), (int)current.vars.size(), (int)shared.size());
            fclose(f);
            return false;
        }

        accumulated = combined;

        // Check for contradiction
        if (accumulated.vars.empty() && accumulated.parity == 1 && accumulated.pat2id.count(0)) {
            fprintf(f, "output NONE\n");
            fprintf(f, "conclusion UNSAT : %d\n", accumulated.pat2id[0]);
            fprintf(f, "end pseudo-Boolean proof\n");
            fclose(f);
            fprintf(stderr, "c Proof complete at step %d/%d, %lld lines\n", step, nxors, total_lines);
            return true;
        }

        if (get_time() - start_time > TIME_LIMIT) {
            fprintf(stderr, "c Time limit reached at step %d/%d\n", step, nxors);
            fclose(f);
            return false;
        }
        if (total_lines > 300000000LL) {
            fprintf(stderr, "c Line budget exceeded at step %d/%d (%lld lines)\n", step, nxors, total_lines);
            fclose(f);
            return false;
        }
    }

    // If we processed all constraints but no contradiction, something is wrong
    if (accumulated.vars.empty() && accumulated.parity == 1 && accumulated.pat2id.count(0)) {
        fprintf(f, "output NONE\n");
        fprintf(f, "conclusion UNSAT : %d\n", accumulated.pat2id[0]);
        fprintf(f, "end pseudo-Boolean proof\n");
        fclose(f);
        return true;
    }

    fprintf(stderr, "c No contradiction found after processing all constraints\n");
    fprintf(stderr, "c Remaining vars: %d, parity: %d\n", (int)accumulated.vars.size(), accumulated.parity);
    fclose(f);
    return false;
}

// ---- Polynomial UNSAT proof generation ----
// For mod2c UNSAT: introduce product variables via VeriPB 'red' rule,
// derive linearized XOR clauses via 'rup', then combine XORs to derive empty clause.

struct PolyConstraint {
    vector<int> linear_vars; // variable IDs with coefficient 1
    vector<pair<int,int>> products; // product terms (v1,v2)
    int constant; // constant term (constraint: linear + products = constant)
};

// Forward declarations
bool extract_polynomial(const vector<int>& vs, const vector<int>& clause_indices, PolyConstraint& poly);

// poly_clause_groups[i] = list of original clause indices (0-based) for polynomial i
bool gen_poly_proof(const char* proof_path, const vector<PolyConstraint>& polys,
                    const vector<vector<int>>& poly_clause_groups) {
    // Collect all unique product pairs
    map<pair<int,int>, int> prod2aux;
    for (auto& p : polys)
        for (auto& pr : p.products)
            if (!prod2aux.count(pr)) {
                int id = nvars + (int)prod2aux.size() + 1;
                prod2aux[pr] = id;
            }

    int n_aux = prod2aux.size();

    // Build linearized XOR constraints
    vector<XORConstraint> lin_xors;
    for (auto& p : polys) {
        XORConstraint xc;
        xc.vars = p.linear_vars;
        for (auto& pr : p.products) xc.vars.push_back(prod2aux[pr]);
        sort(xc.vars.begin(), xc.vars.end());
        xc.parity = p.constant;
        lin_xors.push_back(xc);
    }

    int nxors = lin_xors.size();

    // Build var2xors and adjacency graph
    unordered_map<int, vector<int>> var2xors;
    for (int i = 0; i < nxors; i++)
        for (int v : lin_xors[i].vars) var2xors[v].push_back(i);

    vector<vector<int>> xor_adj(nxors);
    vector<int> xor_degree(nxors, 0);
    for (auto& [v, xis] : var2xors) {
        if ((int)xis.size() == 2) {
            xor_adj[xis[0]].push_back(xis[1]);
            xor_adj[xis[1]].push_back(xis[0]);
            xor_degree[xis[0]]++;
            xor_degree[xis[1]]++;
        }
    }

    // Find best ordering using DFS, spectral, and greedy approaches
    OrderResult best_order;
    best_order.max_combine_width = INT_MAX;
    best_order.estimated_lines = (long long)1e18;

    // DFS orderings
    {
        auto bfs_far = [&](int start) -> int {
            vector<int> dist(nxors, -1);
            queue<int> q;
            dist[start] = 0; q.push(start);
            int farthest = start;
            while (!q.empty()) {
                int u = q.front(); q.pop();
                for (int v : xor_adj[u]) {
                    if (dist[v] < 0) {
                        dist[v] = dist[u] + 1;
                        q.push(v);
                        if (dist[v] > dist[farthest]) farthest = v;
                    }
                }
            }
            return farthest;
        };

        vector<int> dfs_starts;
        for (int t = 0; t < min(3, nxors); t++) {
            int s = t * nxors / max(3, nxors);
            int p1 = bfs_far(s);
            int p2 = bfs_far(p1);
            dfs_starts.push_back(p1);
            dfs_starts.push_back(p2);
        }
        sort(dfs_starts.begin(), dfs_starts.end());
        dfs_starts.erase(unique(dfs_starts.begin(), dfs_starts.end()), dfs_starts.end());

        for (int s : dfs_starts) {
            auto res = dfs_postorder(lin_xors, var2xors, xor_adj, s);
            if (res.max_combine_width < best_order.max_combine_width ||
                (res.max_combine_width == best_order.max_combine_width &&
                 res.estimated_lines < best_order.estimated_lines)) {
                best_order = res;
            }
        }
    }

    // Spectral ordering
    {
        auto res = spectral_ordering(lin_xors, var2xors, xor_adj);
        if (res.max_combine_width < best_order.max_combine_width ||
            (res.max_combine_width == best_order.max_combine_width &&
             res.estimated_lines < best_order.estimated_lines)) {
            best_order = res;
        }
        auto rres = res;
        reverse(rres.order.begin(), rres.order.end());
        compute_order_stats(rres, lin_xors);
        if (rres.max_combine_width < best_order.max_combine_width ||
            (rres.max_combine_width == best_order.max_combine_width &&
             rres.estimated_lines < best_order.estimated_lines)) {
            best_order = rres;
        }
    }

    // Greedy orderings (for small enough instances)
    if (nxors <= 20000) {
        vector<int> good_starts;
        int min_deg = INT_MAX;
        for (int i = 0; i < nxors; i++) min_deg = min(min_deg, xor_degree[i]);
        for (int i = 0; i < nxors; i++)
            if (xor_degree[i] <= min_deg + 1) good_starts.push_back(i);

        auto find_peripheral = [&](int start) -> int {
            vector<int> dist(nxors, -1);
            queue<int> q;
            dist[start] = 0; q.push(start);
            int farthest = start;
            while (!q.empty()) {
                int u = q.front(); q.pop();
                for (int v : xor_adj[u]) {
                    if (dist[v] < 0) {
                        dist[v] = dist[u] + 1;
                        q.push(v);
                        if (dist[v] > dist[farthest] ||
                            (dist[v] == dist[farthest] && xor_degree[v] < xor_degree[farthest]))
                            farthest = v;
                    }
                }
            }
            return farthest;
        };

        for (int tries = 0; tries < min(5, nxors); tries++) {
            int s = tries * nxors / max(5, nxors);
            int p1 = find_peripheral(s);
            int p2 = find_peripheral(p1);
            good_starts.push_back(p1);
            good_starts.push_back(p2);
        }

        srand(42);
        for (int i = 0; i < 200; i++) good_starts.push_back(rand() % nxors);
        sort(good_starts.begin(), good_starts.end());
        good_starts.erase(unique(good_starts.begin(), good_starts.end()), good_starts.end());

        double t0 = get_time();
        for (int si = 0; si < (int)good_starts.size(); si++) {
            if (get_time() - t0 > 30.0) break;
            if (si >= 10 && best_order.max_combine_width > 26) break;
            int s = good_starts[si];
            for (int h = 0; h < 2; h++) {
                auto res = compute_ordering(lin_xors, var2xors, s, h);
                if (res.max_combine_width < best_order.max_combine_width ||
                    (res.max_combine_width == best_order.max_combine_width &&
                     res.estimated_lines < best_order.estimated_lines)) {
                    best_order = res;
                }
            }
        }
    }

    fprintf(stderr, "c Poly proof: best ordering max_combine_width=%d, estimated_lines=%lld\n",
            best_order.max_combine_width, best_order.estimated_lines);

    // Check budget
    if (best_order.max_combine_width > 26 || best_order.estimated_lines > 300000000LL) {
        fprintf(stderr, "c Poly proof too large (width=%d, est_lines=%lld)\n",
                best_order.max_combine_width, best_order.estimated_lines);
        return false;
    }

    // Open proof file
    FILE* pf = fopen(proof_path, "w");
    if (!pf) return false;
    static char pbuf[1 << 20];
    setvbuf(pf, pbuf, _IOFBF, sizeof(pbuf));

    fprintf(pf, "pseudo-Boolean proof version 2.0\n");
    int next_pid = nclauses + 1; // IDs after original clauses

    // Step 1: Introduce product variables using 'red' rule
    fprintf(stderr, "c Introducing %d product variables...\n", n_aux);
    // For each product z = a*b, add three clauses:
    // P1: z ∨ ~a ∨ ~b  (a∧b → z)
    // P2: ~z ∨ a        (z → a)
    // P3: ~z ∨ b        (z → b)

    struct ProdDef {
        int z, a, b;
        int pid_p1, pid_p2, pid_p3;  // PIDs of the three definition clauses
    };
    map<int, ProdDef> prod_defs;  // product var -> definition

    for (auto& [pr, zvar] : prod2aux) {
        int a = pr.first, b = pr.second;
        ProdDef pd;
        pd.z = zvar; pd.a = a; pd.b = b;

        // P1: z ∨ ~a ∨ ~b: under substitution z→1, becomes TRUE
        fprintf(pf, "red 1 x%d 1 ~x%d 1 ~x%d >= 1 ; x%d -> 1\n", zvar, a, b, zvar);
        pd.pid_p1 = next_pid++;

        // P2: ~z ∨ a: under substitution z→0, becomes TRUE
        fprintf(pf, "red 1 ~x%d 1 x%d >= 1 ; x%d -> 0\n", zvar, a, zvar);
        pd.pid_p2 = next_pid++;

        // P3: ~z ∨ b: under substitution z→0, becomes TRUE
        fprintf(pf, "red 1 ~x%d 1 x%d >= 1 ; x%d -> 0\n", zvar, b, zvar);
        pd.pid_p3 = next_pid++;

        prod_defs[zvar] = pd;
    }

    // Step 2: Derive linearized XOR clauses using pol (cutting planes)
    // For each polynomial with products, we derive each linearized XOR clause from
    // original clauses + product definitions using resolution-via-pol.
    fprintf(stderr, "c Deriving linearized XOR clauses via pol...\n");

    // For each linearized XOR, build its clause set
    vector<vector<int>> xor_clause_pids(nxors);

    for (int xi = 0; xi < nxors; xi++) {
        auto& xc = lin_xors[xi];
        auto& pc = polys[xi];

        if (pc.products.empty()) {
            // Pure XOR - clauses are directly in the original formula
            for (int ci : poly_clause_groups[xi])
                xor_clause_pids[xi].push_back(ci + 1);  // 1-based PID
        } else if (pc.products.size() == 1) {
            // Single product: derive linearized XOR clauses
            auto& prod = pc.products[0];
            int c_var = prod.first, d_var = prod.second;
            int z_var = prod2aux[prod];
            auto& pd = prod_defs[z_var];

            // Check if c, d are among linear vars
            bool c_is_lin = find(pc.linear_vars.begin(), pc.linear_vars.end(), c_var) != pc.linear_vars.end();
            bool d_is_lin = find(pc.linear_vars.begin(), pc.linear_vars.end(), d_var) != pc.linear_vars.end();

            // Get original variables and build position map
            vector<int> orig_vars;
            for (int ci : poly_clause_groups[xi])
                for (int l : all_clauses[ci]) {
                    int v = abs(l);
                    if (find(orig_vars.begin(), orig_vars.end(), v) == orig_vars.end())
                        orig_vars.push_back(v);
                }
            sort(orig_vars.begin(), orig_vars.end());

            map<int, int> var2pos;
            for (int j = 0; j < (int)orig_vars.size(); j++) var2pos[orig_vars[j]] = j;
            int c_pos = var2pos[c_var], d_pos = var2pos[d_var];

            // Index original clauses by blocked assignment pattern
            map<uint32_t, int> orig_by_pattern;
            for (int ci : poly_clause_groups[xi]) {
                uint32_t blocked = 0;
                for (int l : all_clauses[ci]) {
                    int v = abs(l);
                    if (l < 0) blocked |= (1u << var2pos[v]);
                }
                orig_by_pattern[blocked] = ci + 1;
            }

            // Map from linearized var to position in xc.vars
            int k = (int)xc.vars.size();
            map<int, int> lin_var2pos;
            for (int j = 0; j < k; j++) lin_var2pos[xc.vars[j]] = j;
            int z_pos_lin = lin_var2pos[z_var];

            // For each linearized XOR clause (correct parity: par != parity)
            for (int mask = 0; mask < (1 << k); mask++) {
                int par = __builtin_popcount(mask) & 1;
                if (par == xc.parity) continue; // keep only par != parity (blocking clauses)

                bool z_neg = (mask >> z_pos_lin) & 1;

                // Build blocked assignment for ALL original vars from the mask
                // For vars that are linearized: get value from mask
                // For vars that are NOT linearized (product pair vars): determined by z and other vals
                uint32_t base_blocked = 0;
                for (int v : orig_vars) {
                    if (v == c_var || v == d_var) continue; // handle separately
                    auto it = lin_var2pos.find(v);
                    if (it != lin_var2pos.end() && (mask & (1 << it->second)))
                        base_blocked |= (1u << var2pos[v]);
                }
                // Add c_var value if it's a linearized var
                int c_val = -1, d_val = -1;
                if (c_is_lin) {
                    auto it = lin_var2pos.find(c_var);
                    c_val = (mask >> it->second) & 1;
                    if (c_val) base_blocked |= (1u << c_pos);
                }
                if (d_is_lin) {
                    auto it = lin_var2pos.find(d_var);
                    d_val = (mask >> it->second) & 1;
                    if (d_val) base_blocked |= (1u << d_pos);
                }

                if (z_neg) {
                    // Blocking z=1 → c=1, d=1
                    // Check for contradiction: if c is linear and mask sets c=0,
                    // that contradicts z=1 requiring c=1. Similarly for d.
                    bool c_contradicts = (c_is_lin && c_val == 0);
                    bool d_contradicts = (d_is_lin && d_val == 0);

                    if (c_contradicts || d_contradicts) {
                        // Clause is implied by product definitions alone (RUP from P2 or P3)
                        // P2: ~z ∨ c  (z=1 → c=1)
                        // P3: ~z ∨ d  (z=1 → d=1)
                        // The linearized clause contains ~z (since z_neg) and either c or d
                        // which is a superset of P2 or P3. So it's RUP.
                        fprintf(pf, "rup ");
                        for (int i = 0; i < k; i++)
                            fprintf(pf, "1 %sx%d ", (mask & (1<<i)) ? "~" : "", xc.vars[i]);
                        fprintf(pf, ">= 1 ;\n");
                        xor_clause_pids[xi].push_back(next_pid++);
                    } else {
                        uint32_t blocked_orig = base_blocked | (1u << c_pos) | (1u << d_pos);
                        auto it = orig_by_pattern.find(blocked_orig);
                        if (it == orig_by_pattern.end()) {
                            fprintf(stderr, "c ERROR: missing orig clause z-neg poly %d mask=%d c_val=%d d_val=%d\n", xi, mask, c_val, d_val);
                            fclose(pf); return false;
                        }

                        if (c_is_lin && d_is_lin) {
                            // Both c,d are linearized (and both =1, no contradiction) → just weaken with ~z (RUP)
                            fprintf(pf, "rup ");
                            for (int i = 0; i < k; i++)
                                fprintf(pf, "1 %sx%d ", (mask & (1<<i)) ? "~" : "", xc.vars[i]);
                            fprintf(pf, ">= 1 ;\n");
                            xor_clause_pids[xi].push_back(next_pid++);
                        } else if (c_is_lin && !d_is_lin) {
                            // c is linearized (c=1), d is not → resolve with P3 on d
                            fprintf(pf, "pol %d %d + s\n", it->second, pd.pid_p3);
                            xor_clause_pids[xi].push_back(next_pid++);
                        } else if (!c_is_lin && d_is_lin) {
                            // d is linearized (d=1), c is not → resolve with P2 on c
                            fprintf(pf, "pol %d %d + s\n", it->second, pd.pid_p2);
                            xor_clause_pids[xi].push_back(next_pid++);
                        } else {
                            // Neither linearized → 2-step: resolve with P2 on c, then P3 on d
                            fprintf(pf, "pol %d %d + s\n", it->second, pd.pid_p2);
                            int s1 = next_pid++;
                            fprintf(pf, "pol %d %d + s\n", s1, pd.pid_p3);
                            xor_clause_pids[xi].push_back(next_pid++);
                        }
                    }
                } else {
                    // Blocking z=0 → c*d=0
                    // Check for contradiction: if c_lin=1 and d_lin=1 but z=0,
                    // contradicts P1: z ∨ ~c ∨ ~d (c=1,d=1 → z=1)
                    if (c_is_lin && d_is_lin && c_val == 1 && d_val == 1) {
                        // Clause contains z (positive, since z_neg=false) and ~c and ~d
                        // which is superset of P1: z ∨ ~c ∨ ~d. RUP.
                        fprintf(pf, "rup ");
                        for (int i = 0; i < k; i++)
                            fprintf(pf, "1 %sx%d ", (mask & (1<<i)) ? "~" : "", xc.vars[i]);
                        fprintf(pf, ">= 1 ;\n");
                        xor_clause_pids[xi].push_back(next_pid++);
                        continue;
                    }

                    // Find consistent (c,d) pairs based on which are linearized
                    vector<pair<int,int>> cd_pairs; // (c_val, d_val) consistent with c*d=0
                    if (c_val >= 0 && d_val >= 0) {
                        // Both determined
                        if (c_val * d_val == 0) cd_pairs.push_back({c_val, d_val});
                    } else if (c_val >= 0) {
                        // c determined
                        if (c_val == 0) { cd_pairs.push_back({0,0}); cd_pairs.push_back({0,1}); }
                        else { cd_pairs.push_back({1,0}); }
                    } else if (d_val >= 0) {
                        // d determined
                        if (d_val == 0) { cd_pairs.push_back({0,0}); cd_pairs.push_back({1,0}); }
                        else { cd_pairs.push_back({0,1}); }
                    } else {
                        // Neither determined
                        cd_pairs = {{0,0}, {0,1}, {1,0}};
                    }

                    // Find original clauses for each (c,d) pair
                    vector<int> orig_pids;
                    for (auto [cv, dv] : cd_pairs) {
                        uint32_t bl = base_blocked;
                        if (cv) bl |= (1u << c_pos);
                        if (dv) bl |= (1u << d_pos);
                        auto it = orig_by_pattern.find(bl);
                        if (it == orig_by_pattern.end()) {
                            fprintf(stderr, "c ERROR: missing orig clause z-pos poly %d cd=(%d,%d) bl=%u\n", xi, cv, dv, bl);
                            fclose(pf); return false;
                        }
                        orig_pids.push_back(it->second);
                    }

                    if (c_is_lin && d_is_lin) {
                        // Both linearized, 1 original → weaken with z (RUP)
                        fprintf(pf, "rup ");
                        for (int i = 0; i < k; i++)
                            fprintf(pf, "1 %sx%d ", (mask & (1<<i)) ? "~" : "", xc.vars[i]);
                        fprintf(pf, ">= 1 ;\n");
                        xor_clause_pids[xi].push_back(next_pid++);
                    } else if (cd_pairs.size() == 1) {
                        // 1 original clause. Need to remove non-lin vars and add z.
                        int pid = orig_pids[0];
                        auto [cv, dv] = cd_pairs[0];
                        if (!c_is_lin && !d_is_lin) {
                            // Neither c nor d is lin → resolve with P1 (z∨~c∨~d) on c or d
                            // But only 1 pair, so either c=0 (resolve with something?) or...
                            // Actually with 1 pair and neither lin, this shouldn't happen
                            // (3 pairs when neither lin). So this case is impossible.
                            fprintf(stderr, "c INTERNAL ERROR: unexpected case\n");
                            fclose(pf); return false;
                        }
                        if (c_is_lin && !d_is_lin) {
                            // c is lin, d is not. d_val is fixed by cv: if cv=1 → dv=0; if cv=0 → could be 0 or 1
                            // But cd_pairs has size 1, so cv=1, dv=0
                            // Original has d positive (d=0 blocked). Resolve with P1 (z∨~c∨~d) on d.
                            fprintf(pf, "pol %d %d + s\n", pid, pd.pid_p1);
                            xor_clause_pids[xi].push_back(next_pid++);
                        } else if (!c_is_lin && d_is_lin) {
                            // d is lin, c is not. c_val fixed. If dv=1 → cv=0; if dv=0 → could be 0 or 1
                            // cd_pairs has size 1, so dv=1, cv=0
                            // Original has c positive (c=0 blocked).
                            // Need to eliminate c and add z.
                            // P1 is z∨~c∨~d. With d=1 (dv=1), d is neg in original. P1 has ~d.
                            // Resolve original (has c positive) with P1 on... hmm, both have ~d? Let me think.
                            // Original blocks (c=0, d=1): clause has c (pos) and ~d (neg).
                            // P1: z∨~c∨~d. Has ~c and ~d.
                            // Resolve on c: original has c (pos), P1 has ~c (neg). YES!
                            // pol: (other+c+~d) + (z+~c+~d) = other + (c+~c) + 2~d + z ≥ 2
                            // = other + 1 + 2~d + z ≥ 2 = other + 2~d + z ≥ 1 → sat: other + ~d + z ≥ 1
                            // But ~d is still there! We want to eliminate d!
                            // Hmm, d is linearized, so ~d is part of the linearized clause. Actually wait,
                            // d is linearized and dv=1 means d is negated in the linearized clause.
                            // So ~d IS supposed to be in the linearized clause!
                            // But wait, d is NOT z. d = d_var, z = prod aux var. The linearized clause
                            // should have z instead of d. But d is a linearized var, so it's also in the clause.
                            //
                            // Actually, the linearized XOR vars include both d_var (as linear) and z_var.
                            // So the clause has d_var AND z_var. We need both.
                            // After pol: other + ~d + z ≥ 1. This has d and z but not c. ✓
                            // We just need c eliminated, which the pol did. ✓
                            fprintf(pf, "pol %d %d + s\n", pid, pd.pid_p1);
                            xor_clause_pids[xi].push_back(next_pid++);
                        }
                    } else if (cd_pairs.size() == 2) {
                        // 2 original clauses. The pairs differ in one variable.
                        auto [c0, d0] = cd_pairs[0];
                        auto [c1, d1] = cd_pairs[1];
                        // Determine which variable differs
                        int pid0 = orig_pids[0], pid1 = orig_pids[1];

                        if (c0 == c1) {
                            // d differs: resolve on d, then weaken with z (rup)
                            fprintf(pf, "pol %d %d + 2 d s\n", pid0, pid1);
                            int s1 = next_pid++;
                            // Now we have clause without d. Weaken with z via rup.
                            fprintf(pf, "rup ");
                            for (int i = 0; i < k; i++)
                                fprintf(pf, "1 %sx%d ", (mask & (1<<i)) ? "~" : "", xc.vars[i]);
                            fprintf(pf, ">= 1 ;\n");
                            xor_clause_pids[xi].push_back(next_pid++);
                        } else {
                            // c differs: resolve on c, then weaken with z (rup)
                            fprintf(pf, "pol %d %d + 2 d s\n", pid0, pid1);
                            int s1 = next_pid++;
                            fprintf(pf, "rup ");
                            for (int i = 0; i < k; i++)
                                fprintf(pf, "1 %sx%d ", (mask & (1<<i)) ? "~" : "", xc.vars[i]);
                            fprintf(pf, ">= 1 ;\n");
                            xor_clause_pids[xi].push_back(next_pid++);
                        }
                    } else {
                        // 3 original clauses. Standard 3-step derivation.
                        // (0,0), (0,1), (1,0) → b00, b01, b10
                        int pid_00 = orig_pids[0], pid_01 = orig_pids[1], pid_10 = orig_pids[2];
                        // Step 1: C_10 + P1 on d
                        fprintf(pf, "pol %d %d + s\n", pid_10, pd.pid_p1);
                        int s1 = next_pid++;
                        // Step 2: C_00 + C_01 on d
                        fprintf(pf, "pol %d %d + 2 d s\n", pid_00, pid_01);
                        int s2 = next_pid++;
                        // Step 3: step1 + step2 on c
                        fprintf(pf, "pol %d %d + s\n", s1, s2);
                        xor_clause_pids[xi].push_back(next_pid++);
                    }
                }
            }
        } else {
            // Multiple products - not yet supported
            fprintf(stderr, "c WARNING: poly %d has %d products, unsupported\n", xi, (int)pc.products.size());
            fclose(pf); return false;
        }
    }

    fprintf(stderr, "c Linearized XOR clauses derived (%d pol steps), starting combine proof...\n",
            next_pid - nclauses - 1 - 3 * n_aux);

    // Step 3: Build LiveXOR objects for the linearized constraints
    vector<LiveXOR> live_xors(nxors);
    for (int xi = 0; xi < nxors; xi++) {
        auto& xc = lin_xors[xi];
        auto& pids = xor_clause_pids[xi];
        live_xors[xi].vars = xc.vars;
        live_xors[xi].parity = xc.parity;
        int k = (int)xc.vars.size();
        // Build pat2id: for each clause, compute sign pattern
        // Correct parity: blocking clauses have par != parity
        int ci_idx = 0;
        for (int mask = 0; mask < (1 << k); mask++) {
            int par = __builtin_popcount(mask) & 1;
            if (par == xc.parity) continue; // keep only par != parity
            uint32_t pat = (uint32_t)mask;
            if (ci_idx < (int)pids.size())
                live_xors[xi].pat2id[pat] = pids[ci_idx++];
        }
    }

    // Step 4: Combine linearized XOR constraints
    vector<int>& order = best_order.order;
    long long proof_lines = next_pid - nclauses - 1;

    LiveXOR accumulated = live_xors[order[0]];
    int step_count = 0;

    for (int oi = 1; oi < (int)order.size(); oi++) {
        if (get_time() - start_time > TIME_LIMIT - 30) {
            fprintf(stderr, "c Poly proof: timeout at step %d/%d\n", oi, (int)order.size());
            fclose(pf);
            return false;
        }

        int xi = order[oi];
        LiveXOR& B = live_xors[xi];

        // Combine using combine_live
        LiveXOR result;
        if (!combine_live(accumulated, B, result, pf, next_pid, proof_lines)) {
            fprintf(stderr, "c Poly proof: combine failed at step %d/%d\n", oi, (int)order.size());
            fclose(pf);
            return false;
        }
        accumulated = move(result);

        step_count++;
        if (step_count % 10 == 0 || oi == (int)order.size() - 1) {
            fprintf(stderr, "c Poly proof step %d/%d, acc_vars=%d\n",
                    oi, (int)order.size() - 1, (int)accumulated.vars.size());
        }
    }

    // Final: accumulated should be empty (parity 1 = contradiction)
    if (!accumulated.vars.empty()) {
        fprintf(stderr, "c Poly proof: accumulated vars remain: %d, parity: %d\n",
                (int)accumulated.vars.size(), accumulated.parity);
        fclose(pf);
        return false;
    }

    if (accumulated.parity != 1) {
        fprintf(stderr, "c Poly proof: parity sum is 0 (SAT?), not UNSAT\n");
        fclose(pf);
        return false;
    }

    // The last combine step should have produced a constraint ID for the contradiction
    if (accumulated.pat2id.empty()) {
        fprintf(stderr, "c Poly proof: no final constraint!\n");
        fclose(pf);
        return false;
    }
    int final_pid = accumulated.pat2id.begin()->second;

    fprintf(pf, "output NONE\nconclusion UNSAT : %d\nend pseudo-Boolean proof\n", final_pid);
    fclose(pf);

    fprintf(stderr, "c Poly proof complete (%d steps)\n", step_count);
    return true;
}

bool extract_polynomial(const vector<int>& vs, const vector<int>& clause_indices, PolyConstraint& poly) {
    int n = vs.size();
    if (n > 6) return false;
    int na = 1 << n;

    // Compute blocked assignments
    uint64_t blocked = 0;
    for (int ci : clause_indices) {
        uint32_t asgn = 0;
        for (int l : all_clauses[ci]) {
            int v = abs(l);
            for (int j = 0; j < n; j++)
                if (vs[j] == v) { if (l < 0) asgn |= (1u << j); break; }
        }
        blocked |= (1ULL << asgn);
    }

    // Build degree-2 monomial system
    // Monomials: const, x0..xn-1, x0x1..x(n-2)x(n-1)
    int nm = 1 + n + n*(n-1)/2;

    // Build matrix [na x (nm+1)]
    vector<vector<int>> mat(na, vector<int>(nm + 1, 0));
    for (int a = 0; a < na; a++) {
        int col = 0;
        mat[a][col++] = 1; // constant
        for (int i = 0; i < n; i++) mat[a][col++] = (a >> i) & 1;
        for (int i = 0; i < n; i++)
            for (int j = i+1; j < n; j++)
                mat[a][col++] = ((a >> i) & 1) & ((a >> j) & 1);
        mat[a][nm] = ((blocked >> a) & 1) ? 0 : 1; // allowed = 1
    }

    // GF(2) elimination
    int rank = 0;
    for (int col = 0; col < nm; col++) {
        int piv = -1;
        for (int r = rank; r < na; r++) if (mat[r][col]) { piv = r; break; }
        if (piv < 0) continue;
        swap(mat[rank], mat[piv]);
        for (int r = 0; r < na; r++) {
            if (r == rank || !mat[r][col]) continue;
            for (int j = 0; j < nm + 1; j++) mat[r][j] ^= mat[rank][j];
        }
        rank++;
    }

    // Check consistency
    for (int r = rank; r < na; r++) if (mat[r][nm]) return false;

    // Extract coefficients
    vector<int> coeffs(nm, 0);
    for (int r = 0; r < rank; r++)
        for (int c = 0; c < nm; c++)
            if (mat[r][c]) { coeffs[c] = mat[r][nm]; break; }

    // polynomial says allowed(x) = coeffs. Constraint: poly(x) = 1 (must be allowed)
    // Rewrite: (1 ^ coeffs[0]) + linear_terms + product_terms = 0
    poly.constant = 1 ^ coeffs[0];
    poly.linear_vars.clear();
    poly.products.clear();

    int col = 1;
    for (int i = 0; i < n; i++) {
        if (coeffs[col]) poly.linear_vars.push_back(vs[i]);
        col++;
    }
    for (int i = 0; i < n; i++)
        for (int j = i+1; j < n; j++) {
            if (coeffs[col]) poly.products.push_back({vs[i], vs[j]});
            col++;
        }

    return true;
}

// Solve a system of polynomial constraints using linearization + GF(2) + search
int solve_poly_system(vector<PolyConstraint>& polys) {
    // Collect all unique product pairs
    map<pair<int,int>, int> prod2aux; // product -> aux variable ID
    for (auto& p : polys)
        for (auto& pr : p.products)
            if (!prod2aux.count(pr)) {
                int id = nvars + (int)prod2aux.size() + 1; // 1-based
                prod2aux[pr] = id;
            }

    int n_aux = prod2aux.size();
    int total = nvars + n_aux;
    fprintf(stderr, "c Poly system: %d polys, %d orig vars, %d products, %d total vars\n",
            (int)polys.size(), nvars, n_aux, total);

    // Build GF(2) system
    GF2System sys;
    sys.init(total);
    for (auto& p : polys) {
        vector<int> vars = p.linear_vars;
        for (auto& pr : p.products) vars.push_back(prod2aux[pr]);
        sys.add_row(vars, p.constant);
    }

    int rank = sys.eliminate();
    if (rank < 0) {
        fprintf(stderr, "c GF(2) linearized system: UNSAT\n");
        // Can't generate proof easily for mod2c UNSAT
        // Return 2 = detected UNSAT but no proof
        return 2;
    }
    fprintf(stderr, "c GF(2) linearized system: rank=%d, free=%d\n", rank, total - rank);

    // Find pivot columns
    vector<int> pivot_col(sys.nr, -1);
    vector<bool> is_pivot(total, false);
    for (int r = 0; r < sys.nr; r++)
        for (int c = 0; c < total; c++)
            if (sys.get_bit(r, c)) { pivot_col[r] = c; is_pivot[c] = true; break; }

    // Back-substitute to get solution for given free variable assignment
    auto back_sub = [&](vector<int>& sol) {
        for (int r = sys.nr - 1; r >= 0; r--) {
            if (pivot_col[r] < 0) continue;
            int v = sys.parities[r];
            for (int c = pivot_col[r]+1; c < total; c++)
                if (sys.get_bit(r, c)) v ^= sol[c];
            sol[pivot_col[r]] = v;
        }
    };

    // Collect AND constraints: aux_var = var1 & var2
    struct ANDConstraint {
        int z; // aux variable (0-based)
        int x, y; // factor variables (0-based)
    };
    vector<ANDConstraint> ands;
    for (auto& [pr, aux] : prod2aux) {
        ands.push_back({aux - 1, pr.first - 1, pr.second - 1}); // convert to 0-based
    }

    // GF(2)-aware search with AND constraint propagation
    // 1. Use GF(2) linearized system to determine variables
    // 2. When a product's both factors are known, add unit constraint for the aux var
    // 3. Search over remaining variables with GF(2) + AND propagation

    // Build product info
    struct ProdInfo {
        int aux_0based; // 0-based column for aux var
        int x_0based, y_0based; // 0-based columns for factors
    };
    vector<ProdInfo> prods;
    for (auto& [pr, aux] : prod2aux)
        prods.push_back({aux - 1, pr.first - 1, pr.second - 1});

    // Clone the GF(2) system for search (we'll modify it during search)
    // Use a simpler representation: for each row, track vars + parity
    int nw = sys.nw;
    struct GF2State {
        vector<vector<uint64_t>> rows;
        vector<int> parities;
        int nr;
        int nv;
        int nw;
        vector<int> val; // -1=unset, 0/1
        vector<int> pivot_of_col; // col -> row that pivots on it (-1 if none)

        void init_from(GF2System& s, int total_vars) {
            nr = s.nr; nv = total_vars; nw = s.nw;
            rows = s.rows;
            parities = s.parities;
            val.assign(nv, -1);
            pivot_of_col.assign(nv, -1);
            for (int r = 0; r < nr; r++)
                for (int c = 0; c < nv; c++)
                    if (get_bit(r, c)) { pivot_of_col[c] = r; break; }
        }

        bool get_bit(int r, int c) const { return (rows[r][c/64]>>(c%64))&1; }

        // Assign variable c to value v. Update system.
        // Returns false on conflict.
        bool assign(int c, int v, vector<pair<int,int>>& trail) {
            if (val[c] >= 0) return val[c] == v;
            val[c] = v;
            trail.push_back({c, -1});

            // If v=1, XOR parity of all rows containing c
            // If v=0, just remove c from all rows
            for (int r = 0; r < nr; r++) {
                if (!get_bit(r, c)) continue;
                // Remove c from this row
                rows[r][c/64] ^= (1ULL << (c%64));
                if (v == 1) parities[r] ^= 1;

                // Check if row became empty
                bool empty = true;
                int first_col = -1;
                for (int w = 0; w < nw && first_col < 0; w++) {
                    if (rows[r][w]) {
                        first_col = w * 64 + __builtin_ctzll(rows[r][w]);
                        empty = false;
                    }
                }
                if (empty) {
                    if (parities[r]) return false; // contradiction
                    continue;
                }

                // Update pivot_of_col if needed
                if (pivot_of_col[c] == r) {
                    pivot_of_col[c] = -1;
                    pivot_of_col[first_col] = r;
                }
            }
            return true;
        }

        // Find unit rows (rows with exactly one variable)
        int find_unit(int& forced_var, int& forced_val) {
            for (int r = 0; r < nr; r++) {
                int cnt = 0, col = -1;
                for (int w = 0; w < nw; w++) {
                    if (rows[r][w]) {
                        cnt += __builtin_popcountll(rows[r][w]);
                        if (cnt == 1 && col < 0) col = w * 64 + __builtin_ctzll(rows[r][w]);
                        if (cnt > 1) break;
                    }
                }
                if (cnt == 0 && parities[r]) return -1; // contradiction
                if (cnt == 1) {
                    forced_var = col;
                    forced_val = parities[r];
                    return 1;
                }
            }
            return 0;
        }
    };

    GF2State state;
    state.init_from(sys, total);

    long long nodes = 0;
    vector<int> solution;

    function<bool(vector<pair<int,int>>&)> search = [&](vector<pair<int,int>>& trail) -> bool {
        nodes++;
        if (nodes % 100000 == 0)
            fprintf(stderr, "c GF2Search nodes: %lld, time: %.1fs\n", nodes, get_time() - start_time);
        if (get_time() - start_time > TIME_LIMIT) return false;

        // Propagate: unit rows + AND constraints
        bool changed = true;
        while (changed) {
            changed = false;

            // GF(2) unit propagation
            int fv, fval;
            int res = state.find_unit(fv, fval);
            if (res < 0) return false; // conflict
            if (res > 0) {
                if (!state.assign(fv, fval, trail)) return false;
                changed = true;
                continue;
            }

            // AND propagation
            for (auto& p : prods) {
                // z = x & y
                if (state.val[p.aux_0based] >= 0 && state.val[p.x_0based] >= 0 && state.val[p.y_0based] >= 0) {
                    // All set, check consistency
                    if (state.val[p.aux_0based] != (state.val[p.x_0based] & state.val[p.y_0based])) return false;
                    continue;
                }
                // z known = 1 → x=1, y=1
                if (state.val[p.aux_0based] == 1) {
                    if (state.val[p.x_0based] < 0) { if (!state.assign(p.x_0based, 1, trail)) return false; changed = true; }
                    if (state.val[p.y_0based] < 0) { if (!state.assign(p.y_0based, 1, trail)) return false; changed = true; }
                }
                // x known = 0 → z=0
                if (state.val[p.x_0based] == 0 && state.val[p.aux_0based] < 0) {
                    if (!state.assign(p.aux_0based, 0, trail)) return false; changed = true;
                }
                // y known = 0 → z=0
                if (state.val[p.y_0based] == 0 && state.val[p.aux_0based] < 0) {
                    if (!state.assign(p.aux_0based, 0, trail)) return false; changed = true;
                }
                // x=1, y=1 → z=1
                if (state.val[p.x_0based] == 1 && state.val[p.y_0based] == 1 && state.val[p.aux_0based] < 0) {
                    if (!state.assign(p.aux_0based, 1, trail)) return false; changed = true;
                }
                // x=1, z=0 → y=0
                if (state.val[p.x_0based] == 1 && state.val[p.aux_0based] == 0 && state.val[p.y_0based] < 0) {
                    if (!state.assign(p.y_0based, 0, trail)) return false; changed = true;
                }
                // y=1, z=0 → x=0
                if (state.val[p.y_0based] == 1 && state.val[p.aux_0based] == 0 && state.val[p.x_0based] < 0) {
                    if (!state.assign(p.x_0based, 0, trail)) return false; changed = true;
                }
            }
        }

        // Find branching variable (prefer original variables)
        int best = -1;
        for (int c = 0; c < nvars; c++) {
            if (state.val[c] < 0) { best = c; break; }
        }
        if (best < 0) {
            // All original vars set. Check if all aux vars can be determined.
            for (int c = nvars; c < total; c++) {
                if (state.val[c] < 0) { best = c; break; }
            }
        }
        if (best < 0) {
            // All variables assigned - save solution
            solution.resize(nvars);
            for (int i = 0; i < nvars; i++) solution[i] = state.val[i];
            return true;
        }

        // Branch on best
        for (int try_val = 0; try_val <= 1; try_val++) {
            // Save state
            GF2State saved = state;
            int save_trail = trail.size();

            vector<pair<int,int>> branch_trail;
            if (state.assign(best, try_val, branch_trail) && search(branch_trail)) {
                return true;
            }

            // Restore
            state = saved;
            trail.resize(save_trail);
        }

        return false;
    };

    vector<pair<int,int>> main_trail;
    if (search(main_trail) && !solution.empty()) {
        if (verify_model(solution)) {
            fprintf(stderr, "c GF2Search: SAT (nodes=%lld, time=%.3fs)\n", nodes, get_time() - start_time);
            printf("s SATISFIABLE\nv ");
            for (int i = 0; i < nvars; i++) printf("%d ", solution[i]?(i+1):-(i+1));
            printf("0\n");
            return 1;
        }
        fprintf(stderr, "c GF2Search found solution but verification failed!\n");
    }

    fprintf(stderr, "c Table DPLL: no solution (nodes=%lld, time=%.1fs)\n", nodes, get_time() - start_time);
    return 0; // UNKNOWN
}


// ---- CDCL with RUP proof logging ----
// Returns: 20 for UNSAT (proof written), 10 for SAT (model printed), 0 for unknown/timeout
int cdcl_solve(const char* proof_path) {
    int nc = (int)all_clauses.size();
    fprintf(stderr, "c CDCL+proof: starting on %d vars, %d clauses\n", nvars, nc);

    FILE* pf = fopen(proof_path, "w");
    if (!pf) return 0;
    // Large I/O buffer for proof output (1MB)
    static char proof_buf[1 << 20];
    setvbuf(pf, proof_buf, _IOFBF, sizeof(proof_buf));
    fprintf(pf, "pseudo-Boolean proof version 2.0\n");
    int next_proof_id = nc + 1;
    long long proof_lines = 1;

    int nlits = 2 * (nvars + 1);

    struct CDB {
        vector<vector<int>> cls;
        vector<int> lbd;
        vector<float> activity;
        vector<int> proof_id;  // VeriPB constraint ID for each clause
        int n_orig;

        struct Watch { int ci, blk; };
        vector<vector<Watch>> watches;
        int nv;

        void init(int nvars, int nc, int nlits_) {
            nv = nvars; n_orig = nc;
            watches.resize(nlits_);
        }

        int add_clause(const vector<int>& lits, bool learnt, int lbd_val, int pid) {
            int ci = (int)cls.size();
            cls.push_back(lits);
            lbd.push_back(lbd_val);
            activity.push_back(0);
            proof_id.push_back(pid);
            if ((int)lits.size() >= 2) {
                watches[lits[0]].push_back({ci, lits[1]});
                watches[lits[1]].push_back({ci, lits[0]});
            }
            return ci;
        }

        void rebuild_watches() {
            for (auto& w : watches) w.clear();
            for (int ci = 0; ci < (int)cls.size(); ci++) {
                auto& c = cls[ci];
                if ((int)c.size() >= 2) {
                    watches[c[0]].push_back({ci, c[1]});
                    watches[c[1]].push_back({ci, c[0]});
                }
            }
        }

        // Compact: remove learned clauses not in keep_set
        // Returns old->new index mapping, and fills deleted_pids with proof IDs to delete
        vector<int> compact(const vector<bool>& keep, vector<int>& deleted_pids) {
            vector<int> mapping(cls.size(), -1);
            vector<vector<int>> new_cls;
            vector<int> new_lbd, new_pid;
            vector<float> new_act;

            for (int ci = 0; ci < (int)cls.size(); ci++) {
                if (ci < n_orig || keep[ci]) {
                    mapping[ci] = (int)new_cls.size();
                    new_cls.push_back(move(cls[ci]));
                    new_lbd.push_back(lbd[ci]);
                    new_act.push_back(activity[ci]);
                    new_pid.push_back(proof_id[ci]);
                } else {
                    if (proof_id[ci] > 0) deleted_pids.push_back(proof_id[ci]);
                }
            }
            cls = move(new_cls);
            lbd = move(new_lbd);
            activity = move(new_act);
            proof_id = move(new_pid);
            rebuild_watches();
            return mapping;
        }
    } cdb;

    cdb.init(nvars, nc, nlits);

    // Add original clauses
    for (int ci = 0; ci < nc; ci++) {
        vector<int> lits;
        for (int l : all_clauses[ci])
            lits.push_back(l > 0 ? 2*l : 2*(-l)+1);
        cdb.add_clause(lits, false, 0, ci + 1); // proof IDs 1..nc
    }

    // Assignment
    vector<int8_t> val(nvars+1, -1);
    vector<int> lev(nvars+1, -1), rsn(nvars+1, -1);
    vector<int> trail;
    vector<int> trail_lim;
    int qhead = 0, dlevel = 0;
    int xor_qhead = 0;

    // XOR propagation data (populated later)
    struct CdclXor {
        vector<int> vars;
        int parity;
        int n_unset;
        int parity_sum;
        vector<int> clause_indices;
    };
    vector<CdclXor> cdcl_xors;
    vector<vector<int>> var2cdcl_xor(nvars + 1);

    // VSIDS heap
    vector<double> act(nvars+1, 0.0);
    double vinc = 1.0;
    vector<int> heap;
    vector<int> hpos(nvars+1, -1);
    auto hp_swap = [&](int i, int j) {
        swap(heap[i], heap[j]); hpos[heap[i]] = i; hpos[heap[j]] = j;
    };
    auto hp_up = [&](int i) {
        while (i > 0 && act[heap[i]] > act[heap[(i-1)/2]]) {
            hp_swap(i, (i-1)/2); i = (i-1)/2;
        }
    };
    auto hp_down = [&](int i) {
        int sz = (int)heap.size();
        while (true) {
            int b = i, l = 2*i+1, r = 2*i+2;
            if (l < sz && act[heap[l]] > act[heap[b]]) b = l;
            if (r < sz && act[heap[r]] > act[heap[b]]) b = r;
            if (b == i) break;
            hp_swap(i, b); i = b;
        }
    };
    auto hp_insert = [&](int v) {
        if (hpos[v] >= 0) return;
        hpos[v] = (int)heap.size(); heap.push_back(v); hp_up((int)heap.size()-1);
    };
    auto hp_pop = [&]() -> int {
        int v = heap[0]; hpos[v] = -1;
        if ((int)heap.size() > 1) { heap[0] = heap.back(); heap.pop_back(); hpos[heap[0]] = 0; hp_down(0); }
        else heap.pop_back();
        return v;
    };
    auto hp_update = [&](int v) { if (hpos[v] >= 0) hp_up(hpos[v]); };

    for (int ci = 0; ci < nc; ci++)
        for (int lit : cdb.cls[ci]) act[lit>>1] += 1.0;
    for (int v = 1; v <= nvars; v++) hp_insert(v);

    vector<int> phase(nvars+1, -1);
    vector<bool> seen(nvars+1, false);

    auto litval = [&](int lit) -> int8_t {
        int v = lit>>1; if (val[v]<0) return -1;
        return (lit&1) ? (1-val[v]) : val[v];
    };
    auto enqueue = [&](int lit, int r) {
        int v = lit>>1; val[v] = (lit&1)?0:1; lev[v] = dlevel; rsn[v] = r;
        trail.push_back(lit);
    };
    auto bt_to = [&](int tgt) {
        while ((int)trail.size() > trail_lim[tgt]) {
            int v = trail.back()>>1; trail.pop_back();
            // Restore XOR state
            for (int xi : var2cdcl_xor[v]) {
                cdcl_xors[xi].n_unset++;
                cdcl_xors[xi].parity_sum ^= val[v];
            }
            phase[v] = val[v]; val[v] = -1; lev[v] = -1; rsn[v] = -1;
            hp_insert(v);
        }
        trail_lim.resize(tgt); qhead = (int)trail.size(); xor_qhead = qhead; dlevel = tgt;
    };

    // --- XOR propagation setup ---
    // Detect XOR constraints for CDCL XOR propagation
    {
        // Group clauses by variable set (same as XOR detection)
        map<vector<int>, vector<int>> groups;
        for (int ci = 0; ci < nc; ci++) {
            vector<int> vars;
            for (int l : all_clauses[ci]) vars.push_back(abs(l));
            sort(vars.begin(), vars.end());
            groups[vars].push_back(ci);
        }
        for (auto& [vars, cls_idx] : groups) {
            int k = (int)vars.size();
            if (k < 3 || k > 12) continue; // Include 3-var XOR for Tseitin d3
            if ((int)cls_idx.size() != (1 << (k-1))) continue;
            // Verify XOR structure
            int par = -1;
            bool valid = true;
            set<uint32_t> seen_masks;
            for (int ci : cls_idx) {
                uint32_t mask = 0;
                for (int l : all_clauses[ci]) {
                    int v = abs(l);
                    int pos = lower_bound(vars.begin(), vars.end(), v) - vars.begin();
                    if (l < 0) mask |= (1u << pos);
                }
                int p = __builtin_popcount(mask) & 1;
                if (par < 0) par = p;
                else if (p != par) { valid = false; break; }
                seen_masks.insert(mask);
            }
            if (!valid || (int)seen_masks.size() != (1<<(k-1))) continue;
            // Determine parity: blocking clauses have popcount(mask) parity = 1-parity
            // So if par=0, blocking clauses have even popcount → parity = 1
            // If par=1, blocking clauses have odd popcount → parity = 0
            int xor_parity = 1 - par;

            CdclXor cx;
            cx.vars = vars;
            cx.parity = xor_parity;
            cx.n_unset = k;
            cx.parity_sum = 0;
            // Sort clauses by blocking pattern for fast lookup
            cx.clause_indices.resize(1 << (k-1));
            int idx = 0;
            for (uint32_t m = 0; m < (1u << k); m++) {
                if ((__builtin_popcount(m) & 1) == par) {
                    // Find the clause with this mask
                    for (int ci : cls_idx) {
                        uint32_t cm = 0;
                        for (int l : all_clauses[ci]) {
                            int v = abs(l);
                            int pos = lower_bound(vars.begin(), vars.end(), v) - vars.begin();
                            if (l < 0) cm |= (1u << pos);
                        }
                        if (cm == m) {
                            cx.clause_indices[idx++] = ci;
                            break;
                        }
                    }
                }
            }
            int xi = (int)cdcl_xors.size();
            for (int v : vars) var2cdcl_xor[v].push_back(xi);
            cdcl_xors.push_back(move(cx));
        }
        if (!cdcl_xors.empty())
            fprintf(stderr, "c CDCL: detected %d XOR constraints for propagation\n", (int)cdcl_xors.size());
    }

    // XOR propagation: find the reason clause for an XOR unit propagation
    // Given XOR with vars and current assignment, compute the blocking pattern
    // and look up the clause index
    auto xor_reason_clause = [&](int xi) -> int {
        auto& cx = cdcl_xors[xi];
        int k = (int)cx.vars.size();
        uint32_t mask = 0;
        for (int i = 0; i < k; i++) {
            int v = cx.vars[i];
            if (val[v] == 1) mask |= (1u << i);
        }
        // The blocking clause has this mask (negated assignment)
        // We need to find the clause index in our sorted list
        int par = __builtin_popcount(mask) & 1;
        int target_par = 1 - cx.parity; // blocking clauses have this popcount parity
        if (par != target_par) {
            // This shouldn't happen - means the assignment satisfies the XOR
            return -1;
        }
        // Find index in sorted blocking patterns
        int idx = 0;
        for (uint32_t m = 0; m < mask; m++) {
            if ((__builtin_popcount(m) & 1) == target_par)
                idx++;
        }
        return cx.clause_indices[idx];
    };

    // BCP + XOR propagation
    auto propagate = [&]() -> int {
        while (true) {
            // Standard BCP
            while (qhead < (int)trail.size()) {
                int fp = trail[qhead++] ^ 1;
                auto& ws = cdb.watches[fp];
                int i = 0, j = 0, sz = (int)ws.size();
                while (i < sz) {
                    auto w = ws[i];
                    if (litval(w.blk) == 1) { ws[j++] = ws[i++]; continue; }
                    auto& c = cdb.cls[w.ci];
                    if (c[0] == fp) swap(c[0], c[1]);
                    if (litval(c[0]) == 1) { ws[i].blk = c[0]; ws[j++] = ws[i++]; continue; }
                    bool found = false;
                    for (int k = 2; k < (int)c.size(); k++) {
                        if (litval(c[k]) != 0) {
                            swap(c[1], c[k]);
                            cdb.watches[c[1]].push_back({w.ci, c[0]});
                            i++; found = true; break;
                        }
                    }
                    if (found) continue;
                    ws[j++] = ws[i++];
                    if (litval(c[0]) == 0) {
                        while (i < sz) ws[j++] = ws[i++];
                        ws.resize(j); return w.ci;
                    }
                    enqueue(c[0], w.ci);
                }
                ws.resize(j);
            }
            // XOR propagation: process newly assigned variables
            bool xor_propagated = false;
            while (xor_qhead < (int)trail.size()) {
                int v = trail[xor_qhead++] >> 1;
                for (int xi : var2cdcl_xor[v]) {
                    auto& cx = cdcl_xors[xi];
                    cx.n_unset--;
                    cx.parity_sum ^= val[v];
                    if (cx.n_unset == 1) {
                        // Find the one unassigned variable
                        int uv = -1;
                        for (int u : cx.vars) {
                            if (val[u] < 0) { uv = u; break; }
                        }
                        if (uv < 0) continue; // shouldn't happen
                        // Required value: parity ^ parity_sum
                        int req = cx.parity ^ cx.parity_sum;
                        int lit = (req == 1) ? (2*uv) : (2*uv+1);
                        if (val[uv] >= 0) continue; // already assigned (by BCP or earlier XOR)
                        // Find reason clause
                        // Temporarily set the variable to compute reason
                        val[uv] = req;
                        int reason_ci = xor_reason_clause(xi);
                        val[uv] = -1;
                        if (reason_ci >= 0) {
                            enqueue(lit, reason_ci);
                            xor_propagated = true;
                        }
                    } else if (cx.n_unset == 0) {
                        if (cx.parity_sum != cx.parity) {
                            // Conflict!
                            int conflict_ci = xor_reason_clause(xi);
                            if (conflict_ci >= 0) return conflict_ci;
                        }
                    }
                }
            }
            if (!xor_propagated) break;
            // If XOR propagated something, go back to BCP
        }
        return -1;
    };

    auto bump_var = [&](int v) {
        act[v] += vinc;
        if (act[v] > 1e100) {
            for (int i = 1; i <= nvars; i++) act[i] *= 1e-100;
            vinc *= 1e-100;
        }
        hp_update(v);
    };

    float cl_inc = 1.0f;
    auto bump_cl = [&](int ci) {
        cdb.activity[ci] += cl_inc;
    };

    vector<int> learnt;
    int learnt_lbd;

    // MiniSat-style self-subsumption minimization
    // Uses 'seen' array which is already set during analysis
    vector<int> min_toclear;

    auto lit_removable = [&](int lit) -> bool {
        // Check if literal is redundant via implication graph traversal
        // seen[v] is already set for all variables in the learned clause
        vector<int> stack;
        stack.push_back(lit);
        int top = (int)min_toclear.size();
        while (!stack.empty()) {
            int l = stack.back(); stack.pop_back();
            int v = l >> 1;
            int r = rsn[v];
            if (r < 0) {
                // Clean up and return false
                for (int j = top; j < (int)min_toclear.size(); j++)
                    seen[min_toclear[j]] = false;
                min_toclear.resize(top);
                return false;
            }
            auto& c = cdb.cls[r];
            for (int cl : c) {
                int cv = cl >> 1;
                if (cv == v) continue;
                if (seen[cv] || lev[cv] <= 0) continue;
                if (rsn[cv] < 0) {
                    // Decision variable not in clause - can't remove
                    for (int j = top; j < (int)min_toclear.size(); j++)
                        seen[min_toclear[j]] = false;
                    min_toclear.resize(top);
                    return false;
                }
                seen[cv] = true;
                min_toclear.push_back(cv);
                stack.push_back(cl);
            }
        }
        return true;
    };

    auto analyze = [&](int confl) -> int {
        learnt.clear(); learnt.push_back(0);
        int cnt = 0, p = -1, idx = (int)trail.size()-1;
        vector<int> lvls;
        do {
            auto& c = cdb.cls[confl];
            if (confl >= nc) bump_cl(confl);
            for (int lit : c) {
                int v = lit>>1;
                if (p >= 0 && v == (p>>1)) continue;
                if (seen[v] || lev[v] <= 0) continue;
                seen[v] = true;
                bump_var(v);
                if (lev[v] >= dlevel) cnt++;
                else { learnt.push_back(lit); lvls.push_back(lev[v]); }
            }
            while (!seen[trail[idx]>>1]) idx--;
            p = trail[idx--];
            confl = rsn[p>>1];
            cnt--;
            seen[p>>1] = false;
        } while (cnt > 0);
        learnt[0] = p^1;

        // Self-subsumption minimization (compatible with RUP proofs)
        // At this point, seen[] is TRUE for lower-level vars in learnt[1..k]
        // and FALSE for UIP. Re-mark UIP too.
        seen[learnt[0]>>1] = true;
        min_toclear.clear();
        {
            int j = 1; // keep UIP at position 0
            for (int i = 1; i < (int)learnt.size(); i++) {
                if (rsn[learnt[i]>>1] >= 0 && lit_removable(learnt[i])) {
                    // This literal is redundant, track for seen cleanup
                    min_toclear.push_back(learnt[i]>>1);
                } else {
                    learnt[j++] = learnt[i];
                }
            }
            learnt.resize(j);
            // Recompute lvls
            lvls.clear();
            for (int i = 1; i < (int)learnt.size(); i++) lvls.push_back(lev[learnt[i]>>1]);
        }
        // Clean up ALL seen marks: remaining learnt + removed + BFS-visited
        for (int lit : learnt) seen[lit>>1] = false;
        for (int v : min_toclear) seen[v] = false;

        // Compute LBD
        sort(lvls.begin(), lvls.end());
        lvls.erase(unique(lvls.begin(), lvls.end()), lvls.end());
        learnt_lbd = (int)lvls.size() + 1;

        int bt = 0;
        if ((int)learnt.size() > 1) {
            int mx = 1;
            for (int i = 2; i < (int)learnt.size(); i++)
                if (lev[learnt[i]>>1] > lev[learnt[mx]>>1]) mx = i;
            swap(learnt[1], learnt[mx]);
            bt = lev[learnt[1]>>1];
        }
        vinc /= 0.95;
        cl_inc /= 0.999f;
        if (cl_inc > 1e20f) {
            for (int i = nc; i < (int)cdb.activity.size(); i++) cdb.activity[i] *= 1e-20f;
            cl_inc *= 1e-20f;
        }
        return bt;
    };

    // Fast integer-to-string for proof output
    char proof_line_buf[65536];
    auto fast_itoa = [](char* p, int v) -> char* {
        if (v == 0) { *p++ = '0'; return p; }
        char tmp[12]; int i = 0;
        while (v > 0) { tmp[i++] = '0' + v % 10; v /= 10; }
        while (i > 0) *p++ = tmp[--i];
        return p;
    };

    auto emit_rup = [&](const vector<int>& lits) -> int {
        char* p = proof_line_buf;
        *p++ = 'r'; *p++ = 'u'; *p++ = 'p';
        for (int lit : lits) {
            *p++ = ' '; *p++ = '1'; *p++ = ' ';
            if (lit & 1) *p++ = '~';
            *p++ = 'x';
            p = fast_itoa(p, lit >> 1);
        }
        *p++ = ' '; *p++ = '>'; *p++ = '='; *p++ = ' '; *p++ = '1';
        *p++ = ' '; *p++ = ';'; *p++ = '\n';
        fwrite(proof_line_buf, 1, p - proof_line_buf, pf);
        int pid = next_proof_id++;
        proof_lines++;
        return pid;
    };

    // Emit batch deletion to proof file
    auto emit_del = [&](const vector<int>& pids) {
        if (pids.empty()) return;
        for (int i = 0; i < (int)pids.size(); i += 1000) {
            char* p = proof_line_buf;
            *p++ = 'd'; *p++ = 'e'; *p++ = 'l'; *p++ = ' ';
            *p++ = 'i'; *p++ = 'd';
            int end = min(i + 1000, (int)pids.size());
            for (int j = i; j < end; j++) {
                *p++ = ' ';
                p = fast_itoa(p, pids[j]);
            }
            *p++ = ' '; *p++ = ';'; *p++ = '\n';
            fwrite(proof_line_buf, 1, p - proof_line_buf, pf);
            proof_lines++;
        }
    };

    // Compact clause DB: backtrack to 0, remove low-quality learned clauses, emit deletions
    auto reduce_and_compact = [&]() {
        if (dlevel > 0) bt_to(0);
        int n_total = (int)cdb.cls.size();
        int n_learned = n_total - nc;
        if (n_learned <= 100) return;

        vector<bool> keep(n_total, true);
        vector<bool> is_reason(n_total, false);
        for (int lit : trail) { int r = rsn[lit>>1]; if (r >= 0) is_reason[r] = true; }

        vector<pair<float,int>> candidates;
        for (int ci = nc; ci < n_total; ci++) {
            if (cdb.cls[ci].empty()) continue;
            if ((int)cdb.cls[ci].size() <= 2) continue;
            if (is_reason[ci]) continue;
            if (cdb.lbd[ci] <= 2) continue;
            candidates.push_back({cdb.activity[ci], ci});
        }
        sort(candidates.begin(), candidates.end());
        int del = (int)candidates.size() * 2 / 3;
        for (int i = 0; i < del; i++) keep[candidates[i].second] = false;

        vector<int> deleted_pids;
        auto mapping = cdb.compact(keep, deleted_pids);

        // Emit deletions to proof
        emit_del(deleted_pids);

        for (int lit : trail) {
            int v = lit>>1;
            if (rsn[v] >= 0) rsn[v] = mapping[rsn[v]];
        }

        fprintf(stderr, "c CDCL: compacted %d -> %d clauses, deleted %d proof constraints\n",
                n_total, (int)cdb.cls.size(), (int)deleted_pids.size());
    };

    // Initial unit clauses
    for (int ci = 0; ci < nc; ci++) {
        auto& c = cdb.cls[ci];
        if (c.empty()) {
            fprintf(pf, "rup >= 1 ;\n"); next_proof_id++; proof_lines++;
            fprintf(pf, "output NONE\nconclusion UNSAT : %d\nend pseudo-Boolean proof\n", next_proof_id-1);
            fclose(pf); return 20;
        }
        if ((int)c.size() == 1) {
            if (litval(c[0]) < 0) enqueue(c[0], ci);
            else if (litval(c[0]) == 0) {
                fprintf(pf, "rup >= 1 ;\n"); next_proof_id++; proof_lines++;
                fprintf(pf, "output NONE\nconclusion UNSAT : %d\nend pseudo-Boolean proof\n", next_proof_id-1);
                fclose(pf); return 20;
            }
        }
    }

    // Failed Literal Detection (FLP)
    {
        int confl0 = propagate();
        if (confl0 >= 0) {
            fprintf(pf, "rup >= 1 ;\n"); next_proof_id++; proof_lines++;
            fprintf(pf, "output NONE\nconclusion UNSAT : %d\nend pseudo-Boolean proof\n", next_proof_id-1);
            fclose(pf);
            fprintf(stderr, "c CDCL: UNSAT at level 0 by initial BCP\n");
            return 20;
        }
        bool progress = true;
        int flp_rounds = 0;
        while (progress && flp_rounds < 20) {
            progress = false;
            flp_rounds++;
            int n_failed = 0;
            for (int v = 1; v <= nvars; v++) {
                if (val[v] >= 0) continue;
                for (int pol = 0; pol < 2; pol++) {
                    int lit = 2*v + pol;
                    int save_trail = (int)trail.size();
                    dlevel = 1;
                    trail_lim.push_back(save_trail);
                    enqueue(lit, -1);
                    int c = propagate();
                    if (c >= 0) {
                        bt_to(0);
                        int unit = lit ^ 1;
                        fprintf(pf, "rup 1 %sx%d >= 1 ;\n", (unit&1)?"~":"", unit>>1);
                        int pid = next_proof_id++; proof_lines++;
                        int ci = cdb.add_clause({unit}, true, 1, pid);
                        enqueue(unit, ci);
                        n_failed++;
                        progress = true;
                        int c2 = propagate();
                        if (c2 >= 0) {
                            fprintf(pf, "rup >= 1 ;\n"); next_proof_id++; proof_lines++;
                            fprintf(pf, "output NONE\nconclusion UNSAT : %d\nend pseudo-Boolean proof\n", next_proof_id-1);
                            fclose(pf);
                            fprintf(stderr, "c FLP: UNSAT by failed literal at round %d\n", flp_rounds);
                            return 20;
                        }
                        break;
                    } else {
                        bt_to(0);
                    }
                }
            }
            fprintf(stderr, "c FLP round %d: %d failed literals, %d assigned\n",
                    flp_rounds, n_failed, (int)trail.size());
            if (n_failed == 0) break;
        }
    }

    long long conflicts = 0, decs = 0;
    int max_learnt = max(nc * 10, 10000);
    int restart_cnt = 0;
    long long next_restart = 100;

    // Glucose-style moving averages for restarts
    double lbd_ma_fast = 0, lbd_ma_slow = 0;
    bool glucose_restart_pending = false;
    long long glucose_cooldown = 0;
    const double lbd_fast_alpha = 0.03;  // fast EMA
    const double lbd_slow_alpha = 1e-5;  // slow EMA

    while (true) {
        if (get_time() - start_time > TIME_LIMIT - 10) {
            fprintf(stderr, "c CDCL: timeout (confl=%lld t=%.1fs)\n", conflicts, get_time()-start_time);
            fclose(pf); return 0;
        }
        int confl = propagate();
        if (confl >= 0) {
            conflicts++;
            if (conflicts % 50000 == 0)
                fprintf(stderr, "c CDCL: confl=%lld decs=%lld cls=%d proof=%lld t=%.1fs\n",
                        conflicts, decs, (int)cdb.cls.size(), proof_lines, get_time()-start_time);
            if (dlevel == 0) {
                fprintf(pf, "rup >= 1 ;\n"); next_proof_id++; proof_lines++;
                fprintf(pf, "output NONE\nconclusion UNSAT : %d\nend pseudo-Boolean proof\n", next_proof_id-1);
                fclose(pf);
                fprintf(stderr, "c CDCL: UNSAT (confl=%lld proof=%lld t=%.3fs)\n",
                        conflicts, proof_lines, get_time()-start_time);
                return 20;
            }
            int bt = analyze(confl);
            // Update LBD moving averages
            lbd_ma_fast += lbd_fast_alpha * (learnt_lbd - lbd_ma_fast);
            lbd_ma_slow += lbd_slow_alpha * (learnt_lbd - lbd_ma_slow);

            int pid = emit_rup(learnt);
            bt_to(bt);
            int ci = cdb.add_clause(learnt, true, learnt_lbd, pid);
            if ((int)learnt.size() == 1) enqueue(learnt[0], ci);
            else enqueue(cdb.cls[ci][0], ci);

            // Glucose-style restart: mark for restart if LBD quality degrades
            if (conflicts > 100 && conflicts > glucose_cooldown &&
                lbd_ma_fast > lbd_ma_slow * 1.25) {
                glucose_restart_pending = true;
            }

            // Reduce at level 0 when too many learned clauses
            int n_learned = (int)cdb.cls.size() - nc;
            if (n_learned > max_learnt && dlevel == 0) {
                reduce_and_compact();
                max_learnt = max(max_learnt + 500, (int)(cdb.cls.size() - nc) + 3000);
            }
        } else {
            // Restart check (Luby + Glucose)
            bool do_restart = false;
            if (conflicts >= next_restart) {
                do_restart = true;
                restart_cnt++;
                int sz=1, seq=0, ii=restart_cnt;
                while (sz<ii+1) { sz=2*sz+1; seq++; }
                while (sz-1!=ii) { sz=(sz-1)>>1; seq--; ii%=sz; }
                next_restart = conflicts + 100LL * (1LL<<seq);
            }
            if (glucose_restart_pending) {
                do_restart = true;
                glucose_restart_pending = false;
                glucose_cooldown = conflicts + 50; // cooldown: at least 50 conflicts between Glucose restarts
            }
            if (do_restart && dlevel > 0) {
                bt_to(0);
                int n_learned = (int)cdb.cls.size() - nc;
                if (n_learned > max_learnt) {
                    reduce_and_compact();
                    max_learnt = max(max_learnt + 500, (int)(cdb.cls.size() - nc) + 3000);
                }
            }
            int best = -1;
            while (!heap.empty()) {
                int v = hp_pop();
                if (val[v] < 0) { best = v; break; }
            }
            if (best < 0) {
                fclose(pf);
                vector<int> sol(nvars, 0);
                for (int v = 1; v <= nvars; v++) sol[v-1] = val[v];
                if (verify_model(sol)) {
                    fprintf(stderr, "c CDCL: SAT (decs=%lld confl=%lld t=%.3fs)\n",
                            decs, conflicts, get_time()-start_time);
                    printf("s SATISFIABLE\nv ");
                    for (int i = 0; i < nvars; i++) printf("%d ", sol[i]?(i+1):-(i+1));
                    printf("0\n");
                    return 10;
                }
                fprintf(stderr, "c CDCL: model verification failed!\n");
                return 0;
            }
            decs++; dlevel++; trail_lim.push_back((int)trail.size());
            enqueue(2*best + (phase[best]==0?1:0), -1);
        }
    }
}

#if 0
// Dead code - DPLL on original CNF (too slow without watched literals)
void dead_code_dpll() {
    int nc = all_clauses.size();

    // For each variable, track which clauses contain it positively/negatively
    vector<vector<int>> pos_in(nvars + 1), neg_in(nvars + 1);
    for (int ci = 0; ci < nc; ci++)
        for (int l : all_clauses[ci]) {
            if (l > 0) pos_in[l].push_back(ci);
            else neg_in[-l].push_back(ci);
        }

    // Per-clause: count of satisfied literals, count of unset literals
    vector<int> sat_count(nc, 0), unset_count(nc);
    for (int ci = 0; ci < nc; ci++) unset_count[ci] = all_clauses[ci].size();

    vector<int> val(nvars + 1, -1); // -1=unassigned
    vector<int> trail;
    vector<int> prop_queue;

    // Assign and propagate
    auto assign = [&](int v, int value) {
        if (val[v] >= 0) return;
        val[v] = value;
        trail.push_back(v);
        // Update clauses
        auto& pos = pos_in[v];
        auto& neg = neg_in[v];
        if (value == 1) {
            for (int ci : pos) { sat_count[ci]++; unset_count[ci]--; }
            for (int ci : neg) { unset_count[ci]--; }
        } else {
            for (int ci : neg) { sat_count[ci]++; unset_count[ci]--; }
            for (int ci : pos) { unset_count[ci]--; }
        }
        prop_queue.push_back(v);
    };

    auto unassign = [&](int v) {
        int value = val[v];
        val[v] = -1;
        auto& pos = pos_in[v];
        auto& neg = neg_in[v];
        if (value == 1) {
            for (int ci : pos) { sat_count[ci]--; unset_count[ci]++; }
            for (int ci : neg) { unset_count[ci]++; }
        } else {
            for (int ci : neg) { sat_count[ci]--; unset_count[ci]++; }
            for (int ci : pos) { unset_count[ci]++; }
        }
    };

    // BCP: process propagation queue. Returns false on conflict.
    auto bcp = [&]() -> bool {
        while (!prop_queue.empty()) {
            int v = prop_queue.back();
            prop_queue.pop_back();
            int value = val[v];
            // Check clauses where this variable appears with opposite polarity (might become unit/empty)
            auto& opp = (value == 1) ? neg_in[v] : pos_in[v];
            for (int ci : opp) {
                if (sat_count[ci] > 0) continue; // already satisfied
                if (unset_count[ci] == 0) return false; // conflict
                if (unset_count[ci] == 1) {
                    // Unit clause: find the unset literal
                    for (int l : all_clauses[ci]) {
                        int u = abs(l);
                        if (val[u] < 0) {
                            assign(u, l > 0 ? 1 : 0);
                            break;
                        }
                    }
                }
            }
        }
        return true;
    };

    // Variable selection: highest occurrence count
    vector<double> activity(nvars + 1, 0.0);
    for (int v = 1; v <= nvars; v++)
        activity[v] = pos_in[v].size() + neg_in[v].size();

    long long nodes = 0;

    function<bool()> dpll = [&]() -> bool {
        nodes++;
        if (nodes % 1000000 == 0)
            fprintf(stderr, "c DPLL nodes: %lld, time: %.1fs\n", nodes, get_time() - start_time);
        if (get_time() - start_time > TIME_LIMIT) return false;

        int save_trail = trail.size();

        if (!bcp()) {
            while ((int)trail.size() > save_trail) { unassign(trail.back()); trail.pop_back(); }
            return false;
        }

        // Check if all clauses satisfied
        int best = -1;
        double best_act = -1;
        for (int v = 1; v <= nvars; v++) {
            if (val[v] >= 0) continue;
            if (activity[v] > best_act) { best_act = activity[v]; best = v; }
        }

        if (best < 0) return true;

        // Decide: try positive first if more positive occurrences
        int first_val = (pos_in[best].size() >= neg_in[best].size()) ? 1 : 0;

        for (int attempt = 0; attempt < 2; attempt++) {
            int try_val = (attempt == 0) ? first_val : (1 - first_val);
            int save2 = trail.size();

            assign(best, try_val);

            if (dpll()) return true;

            while ((int)trail.size() > save2) { unassign(trail.back()); trail.pop_back(); }
        }

        while ((int)trail.size() > save_trail) { unassign(trail.back()); trail.pop_back(); }
        return false;
    };

    if (dpll()) {
        vector<int> sol(nvars, 0);
        for (int i = 1; i <= nvars; i++) sol[i-1] = (val[i] == 1) ? 1 : 0;
        if (verify_model(sol)) {
            fprintf(stderr, "c DPLL: SAT (nodes=%lld, time=%.3fs)\n", nodes, get_time() - start_time);
            printf("s SATISFIABLE\nv ");
            for (int i = 0; i < nvars; i++) printf("%d ", sol[i]?(i+1):-(i+1));
            printf("0\n");
            return 1;
        }
    }

    fprintf(stderr, "c DPLL: no solution\n");
}
#endif

// ---- Main ----
int main(int argc, char* argv[]) {
    if (argc < 3) { fprintf(stderr, "Usage: %s <cnf> <pbp>\n", argv[0]); return 0; }
    start_time = get_time();

    fprintf(stderr, "c Tseitin/XOR Solver\n");
    if (!parse_cnf(argv[1])) { printf("s UNKNOWN\n"); return 0; }
    fprintf(stderr, "c Parsed: %d vars, %d clauses\n", nvars, (int)all_clauses.size());

    int rem;
    auto xors = detect_xors(rem);
    fprintf(stderr, "c Detected %d XOR constraints, %d remaining clauses\n", (int)xors.size(), rem);

    // Try to repair incomplete XOR groups using subsumption from shorter clauses
    if (rem > 0) repair_xors(xors, rem);

    // Quick parity sum check for pure XOR formulas
    if (rem == 0 && !xors.empty()) {
        int parity_sum = 0;
        for (auto& x : xors) parity_sum ^= x.parity;
        fprintf(stderr, "c Pure XOR formula, parity sum: %d\n", parity_sum);
    }

    if (rem > 0) {
        // Try to handle as polynomial GF(2) system (mod2c and similar)
        struct VH { size_t operator()(const vector<int>&v) const {
            size_t h=v.size(); for(int x:v) h=h*1000003+x; return h; }};
        unordered_map<vector<int>,vector<int>,VH> groups;
        for (int i = 0; i < (int)all_clauses.size(); i++) {
            vector<int> vs;
            for (int l : all_clauses[i]) vs.push_back(abs(l));
            sort(vs.begin(), vs.end());
            groups[vs].push_back(i);
        }

        vector<PolyConstraint> polys;
        vector<vector<int>> poly_clause_groups;
        int handled = 0;
        bool all_covered = true;

        for (auto& [vs, idx] : groups) {
            // Check if this is a standard XOR group
            int n = vs.size();
            if (n <= 20 && (int)idx.size() == (1 << (n-1))) {
                int ec=0, oc=0;
                for (int i : idx) {
                    int ng=0; for (int l : all_clauses[i]) if (l<0) ng++;
                    if (ng%2==0) ec++; else oc++;
                }
                int exp = 1<<(n-1);
                if (ec == exp || oc == exp) {
                    // Standard XOR - add as linear poly constraint
                    PolyConstraint pc;
                    pc.constant = (ec == exp) ? 1 : 0; // parity
                    pc.linear_vars.assign(vs.begin(), vs.end());
                    polys.push_back(pc);
                    poly_clause_groups.push_back(idx);
                    handled += idx.size();
                    continue;
                }
            }

            // Try degree-2 polynomial extraction
            if (n <= 6) {
                PolyConstraint pc;
                if (extract_polynomial(vs, idx, pc)) {
                    polys.push_back(pc);
                    poly_clause_groups.push_back(idx);
                    handled += idx.size();
                    continue;
                }
            }

            all_covered = false;
        }

        fprintf(stderr, "c Poly system: %d constraints, covered %d/%d clauses\n",
                (int)polys.size(), handled, (int)all_clauses.size());

        if (all_covered && !polys.empty()) {
            int result = solve_poly_system(polys);
            if (result == 1) return 10; // SAT
            if (result == 2) {
                // Detected UNSAT via GF(2) - try polynomial proof generation first
                fprintf(stderr, "c GF(2) says UNSAT, attempting polynomial XOR proof...\n");
                if (gen_poly_proof(argv[2], polys, poly_clause_groups)) {
                    printf("s UNSATISFIABLE\n");
                    return 20;
                }
                // Fall back to CDCL if proof generation fails
                fprintf(stderr, "c Poly proof failed, attempting CDCL proof...\n");
                { int r = cdcl_solve(argv[2]); if (r == 20) { printf("s UNSATISFIABLE\n"); return 20; } }
                printf("s UNKNOWN\n");
                return 0;
            }
        }

        // For instances where poly detection is incomplete, try CDCL
        {
            fprintf(stderr, "c Trying CDCL on instance (%d vars, %d clauses)...\n", nvars, (int)all_clauses.size());
            int r = cdcl_solve(argv[2]);
            if (r == 10) return 10; // SAT already printed
            if (r == 20) { printf("s UNSATISFIABLE\n"); return 20; }
        }

        printf("s UNKNOWN\n");
        return 0;
    }

    // Check parity sum first (fast UNSAT detection for Tseitin formulas)
    int parity_sum = 0;
    for (auto& x : xors) parity_sum ^= x.parity;
    fprintf(stderr, "c Parity sum: %d (0=SAT candidate, 1=UNSAT)\n", parity_sum);

    // Graph-based SAT solver for Tseitin formulas
    // Build the underlying graph: each variable appears in exactly 2 XOR constraints
    {
        int nxors = xors.size();
        vector<vector<int>> var2xor(nvars + 1);
        for (int i = 0; i < nxors; i++)
            for (int v : xors[i].vars) var2xor[v].push_back(i);

        // Check if this is a valid Tseitin structure (each var in exactly 2 XORs)
        bool is_tseitin = true;
        for (int v = 1; v <= nvars; v++) {
            if (var2xor[v].size() != 2) { is_tseitin = false; break; }
        }

        if (is_tseitin && parity_sum == 0 && nxors > 0 && nvars > 0) {
            // SAT: use spanning tree approach
            fprintf(stderr, "c Using graph-based Tseitin SAT solver\n");

            // Build adjacency list: XOR constraint -> [(neighbor XOR, variable)]
            vector<vector<pair<int,int>>> adj(nxors);
            for (int v = 1; v <= nvars; v++) {
                int a = var2xor[v][0], b = var2xor[v][1];
                adj[a].push_back({b, v});
                adj[b].push_back({a, v});
            }

            // BFS to find spanning forest
            vector<int> parent_node(nxors, -1), parent_var(nxors, -1);
            vector<bool> visited(nxors, false);
            vector<int> bfs_order;

            for (int root = 0; root < nxors; root++) {
                if (visited[root]) continue;
                visited[root] = true;
                bfs_order.push_back(root);
                queue<int> q;
                q.push(root);
                while (!q.empty()) {
                    int u = q.front(); q.pop();
                    for (auto [v, var] : adj[u]) {
                        if (!visited[v]) {
                            visited[v] = true;
                            parent_node[v] = u;
                            parent_var[v] = var;
                            bfs_order.push_back(v);
                            q.push(v);
                        }
                    }
                }
            }

            // Set non-tree variables to 0
            vector<int> sol(nvars, 0);
            set<int> tree_vars;
            for (int i = 0; i < nxors; i++)
                if (parent_var[i] >= 0) tree_vars.insert(parent_var[i]);

            // Process from leaves to root: determine tree-edge variables
            for (int i = (int)bfs_order.size() - 1; i >= 0; i--) {
                int u = bfs_order[i];
                if (parent_var[u] < 0) continue; // root - skip
                // XOR constraint for u: xors[u].vars XOR = xors[u].parity
                int xor_val = xors[u].parity;
                for (int v : xors[u].vars) {
                    if (v != parent_var[u]) xor_val ^= sol[v - 1];
                }
                sol[parent_var[u] - 1] = xor_val;
            }

            if (verify_model(sol)) {
                fprintf(stderr, "c Graph-based solver: SAT\n");
                printf("s SATISFIABLE\nv ");
                for (int i = 0; i < nvars; i++) printf("%d ", sol[i] ? (i+1) : -(i+1));
                printf("0\n");
                return 10;
            }
            fprintf(stderr, "c Graph-based solver: model verification FAILED\n");
        }

        if (is_tseitin && parity_sum == 1) {
            fprintf(stderr, "c Tseitin UNSAT detected by parity sum\n");
            fprintf(stderr, "c Generating UNSAT proof...\n");
            // Try polynomial parity-sum proof first (works for any treewidth)
            if (gen_parity_sum_proof(argv[2], xors)) {
                printf("s UNSATISFIABLE\n");
                return 20;
            }
            // Fall back to Gaussian elimination proof
            if (gen_proof(argv[2], xors)) {
                printf("s UNSATISFIABLE\n");
                return 20;
            }
            fprintf(stderr, "c Proof generation failed (treewidth too high), trying CDCL...\n");
            { int r = cdcl_solve(argv[2]); if (r == 20) { printf("s UNSATISFIABLE\n"); return 20; } if (r == 10) return 10; }
            printf("s UNKNOWN\n");
            return 0;
        }
    }

    // For large instances where dense GF(2) is too expensive
    if ((long long)nvars * nvars / 64 > 300000000LL) {
        fprintf(stderr, "c Instance too large for dense GF(2) (%d vars)\n", nvars);
        fprintf(stderr, "c Trying CDCL on large instance...\n");
        { int r = cdcl_solve(argv[2]); if (r == 20) { printf("s UNSATISFIABLE\n"); return 20; } if (r == 10) return 10; }
        printf("s UNKNOWN\n");
        return 0;
    }

    GF2System sys;
    sys.init(nvars);
    for (auto& x : xors) sys.add_row(x.vars, x.parity);
    int rank = sys.eliminate();
    fprintf(stderr, "c GF(2): %s (rank=%d)\n", rank<0?"UNSAT":"SAT", rank<0?-1:rank);

    if (rank >= 0) {
        auto sol = sys.get_solution();
        if (!verify_model(sol)) {
            fprintf(stderr, "c Model verification FAILED\n");
            printf("s UNKNOWN\n");
            return 0;
        }
        printf("s SATISFIABLE\nv ");
        for (int i=0; i<nvars; i++) printf("%d ", sol[i]?(i+1):-(i+1));
        printf("0\n");
        return 10;
    }

    // UNSAT
    fprintf(stderr, "c Generating UNSAT proof...\n");
    // Try polynomial parity-sum proof first
    if (gen_parity_sum_proof(argv[2], xors)) {
        printf("s UNSATISFIABLE\n");
        return 20;
    }
    // Fall back to Gaussian elimination proof
    if (gen_proof(argv[2], xors)) {
        printf("s UNSATISFIABLE\n");
        return 20;
    }
    fprintf(stderr, "c gen_proof failed, trying CDCL...\n");
    { int r = cdcl_solve(argv[2]); if (r == 20) { printf("s UNSATISFIABLE\n"); return 20; } if (r == 10) return 10; }
    fprintf(stderr, "c CDCL also failed\n");
    printf("s UNKNOWN\n");
    return 0;
}
