Gifts from Knowledge

Gifts from Knowledge

异或带权并查集

问题简述

给定一个 \(r\) 行 \(c\) 列的二进制矩阵。每一行可以选择翻转或不翻转。翻转一行表示将该行的元素左右颠倒。

要求选择一组行进行翻转,使得翻转后每一列中至多有一个 \(1\)。

求方案数,对 \(10^9+7\) 取模。两个方案不同当且仅当存在某一行在一个方案中翻转、在另一个方案中不翻转。


核心观察

翻转第 \(i\) 行会交换第 \(j\) 列和第 \(c-1-j\) 列。因此可以把矩阵的列按对称位置成对处理:

\[(j,\ c-1-j) \]

当 \(c\) 为奇数时,最中间一列单独处理,因为翻转不会改变该列。

对于每一行,我们只关心它在一对对称列 \((L,R)\) 上的两个字符:

  • 00:翻转前后都没有 \(1\),不影响任何列;
  • 11:翻转前后两列都有 \(1\),这一行固定占用两列;
  • 01 或 10:翻转会决定这个 \(1\) 最终落在左列还是右列,称为“异位行”。

每个列对最多容纳两个 \(1\):左列一个、右列一个。


足迹统计

设 \(m=c\)。定义

\[\text{colcnt}[p] = \#\{\text{第 }p\text{ 列中的 }1\} + \#\{\text{第 }m-1-p\text{ 列中的 }1\} \]

也就是说,\(\text{colcnt}[p]\) 统计的是对称列对 \((p,\ m-1-p)\) 上的 \(1\) 的总数。

在代码中,每读到一个 \(1\),同时加到它所在的列和它的对称列:

\[\text{colcnt}[j] \mathrel{+}= 1,\qquad \text{colcnt}[m-1-j] \mathrel{+}= 1 \]

因为每一列最多只能有一个 \(1\),所以每个对称列对最多容纳两个 \(1\)。于是有:

  • 若存在 \(p\) 使得 \(\text{colcnt}[p]\ge 3\),则无解;
  • 若总 \(1\) 的个数 \(w>m\),由鸽笼原理也无解。

另外,当 \(c\) 为奇数时,中间列满足 \(m-1-j=j\),所以中间列的 \(1\) 会被加两次:

  • 中间列有 \(1\) 个 \(1\):\(\text{colcnt}[j]=2\),合法;
  • 中间列有 \(2\) 个 \(1\):\(\text{colcnt}[j]=4\ge 3\),无解。

异位行之间的异或约束

对于每个对称列对,记录第一次在该列对上出现 \(1\) 的行号:

pos[j] = pos[m - j - 1] = i;

当另一行 \(i\) 也在同一个列对上有 \(1\) 时,建立约束:

\[x_i \oplus x_{\text{pos}[j]} = y \]

其中

\[y = g[\text{pos}[j]][j] \]

这里 \(x_i\in\{0,1\}\) 表示第 \(i\) 行是否翻转。

这个约束的含义如下:

  • 若 \(y=1\),说明两行在同一个位置 \(j\) 上都有 \(1\)。翻转后它们仍然在同一列冲突,因此两行必须一翻一不翻,即 \(x_i\oplus x_{\text{pos}[j]}=1\);
  • 若 \(y=0\),说明两行在对称列的两侧各有一个 \(1\)。翻转后必须保持一左一右,因此两行必须同翻或同不翻,即 \(x_i\oplus x_{\text{pos}[j]}=0\)。

所有这样的约束都可以用带权并查集维护。


特殊情况的覆盖

1. 一个 11 行

11 行会在两个对称列上各产生一个 \(1\)。扫描到它时,pos[L] 和 pos[R] 都被设为同一行。扫描到第二个 \(1\) 时,pos 已经等于当前行,因此不会建立自约束。

此时该列对的 \(\text{colcnt}=2\),合法。若同一列对还有其他 \(1\),则 \(\text{colcnt}\ge 3\),会被无解条件拦住。

2. 两个 11 行

同一列对上有两个 11 行时,\(\text{colcnt}=4\ge 3\),直接无解。

3. 中间列

中间列翻转后不变,最多只能有一个 \(1\)。若有两个 \(1\),则 \(\text{colcnt}=4\ge 3\),无解。

因此,11 行、00 行、中间列的情况都被“\(\text{colcnt}\le 2\)”这个条件自动覆盖。


带权并查集

并查集中维护每个变量到根节点的异或值。unite(x, y, lim) 表示加入约束:

\[x \oplus y = lim \]

若 \(x\) 和 \(y\) 已经在同一连通块中,则检查当前异或值是否等于 \(lim\);若不等,说明矛盾,无解。

若不在同一连通块,则合并两个连通块,并维护新的异或关系。

所有约束处理完后,若没有矛盾,设并查集中连通块的数量为 \(cnt\)。每个连通块有一个自由变量,可以任选 \(0\) 或 \(1\),因此答案为:

\[2^{cnt}\pmod{10^9+7} \]


复杂度

设矩阵大小为 \(r\times c\)。

  • 扫描矩阵:\(O(rc)\);
  • 并查集操作:\(O(r\alpha(r))\)。

总时间复杂度为:

\[O(rc + r\alpha(r)) \]

题目保证所有测试用例的 \(\sum r\times c\le 10^6\),因此该复杂度足够。


#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;

struct DSU {
    int n; 
    vector<int> pa, sz, d;
    DSU(int _n) {init(_n);}
    void init(int _n) {
        n = _n;
        pa.assign(_n + 1, 0);
        sz.assign(_n + 1, 1);
        d.assign(_n + 1, 0);
        iota(pa.begin(), pa.end(), 0);
    }
    int findu(int x) {
        if(pa[x] == x) return pa[x];
        int r = findu(pa[x]);
        d[x] ^= d[pa[x]];
        pa[x] = r;
        return r;
    }
    bool unite(int x, int y, int lim) {//lim约束,x与y的翻转关系
        int tx = findu(x), ty = findu(y);
        if(tx == ty) return d[x] ^ d[y] == lim;
        int k = d[x] ^ d[y] ^ lim;
        if(sz[tx] >= sz[ty]) {
            sz[tx] += sz[ty];
            pa[ty] = tx;
            d[ty] = k;
        }
        else {
            sz[ty] += sz[tx];
            pa[tx] = ty;
            d[tx] = k;
        }
        return true;
    }
};

ll modpow(ll a, ll b) {
    ll r = 1;
    while(b) {
        if(b & 1) r = r * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return r;
}

void solve() {
    int n, m; cin >> n >> m;
    vector<string> g(n + 1);
    for(int i = 0; i < n; i ++) cin >> g[i];
    vector<int> colcnt(m + 1);
    int w = 0;
    for(int i = 0; i < n; i ++) {
        for(int j = 0; j < m; j ++) {
            colcnt[j] += g[i][j] - '0';
            colcnt[m - j - 1] += g[i][j] - '0';
            w += g[i][j] - '0';
        }
    }
    if(w > m) {cout << "0\n"; return;}
    for(int j = 0; j < m; j ++) {
        if(colcnt[j] >= 3) {cout << "0\n"; return;}
    }
    
    DSU dsu(n);
    vector<int> pos(m + 1, -1);
    for(int i = 0; i < n; i ++) {
        for(int j = 0; j < m; j ++) {
            if(g[i][j] == '0') continue;
            if(pos[j] == -1) pos[j] = pos[m - j - 1] = i;
            else if(pos[j] != i) {
                int y = (g[pos[j]][j] == '1');
                if(!dsu.unite(i, pos[j], y)) {cout << "0\n"; return;}
            }
        }
    }
    
    int cnt = 0;
    for(int i = 0; i < n; i ++) {
        if(dsu.findu(i) == i) cnt ++;
    }
    ll ans = modpow(2, cnt) % MOD;
    cout << ans << "\n";
}

int main() {  
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int _ = 1; cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-11-03 22:05  thenrify  阅读(3)  评论(0)    收藏  举报