原题链接

挺神妙的一个题吧。自己做的时候完全没做明白,看题解也看到晕晕乎乎的。今天重新从哈集幂的视角理解了一下,才发现确实挺妙(而且典)。

面对这个题首先做一些小转化。考虑一条边,它在哪几条路径上?我们用一个掩码($k$ 位二进制数)表示这个集合。第 $i$ 位为 1 代表这条边在 $a_i$ 与 $b_i$ 间的简单路径上。

我们考虑给树赋一个根,然后把每条边和它连接的两个点中深度较大的对应起来。一条路径经过这条边,当且仅当一个端点在这个点的子树中,而另一个端点不在子树中。这样,我们对每个点维护一个掩码,第 $i$ 位代表第 $i$ 条路径在该点子树中的端点的数目的奇偶性。不难发现这个掩码其实和这个点对应的边的掩码相等。

容易预处理这个掩码。具体的,考虑对第 $i$ 条路径的一个端点 $a$,$\text{mask}_a = \text{mask}_a \text{ xor } 2^i$。然后我们做一次 dfs,对每个点,先处理它的所有子节点,然后它的掩码异或上它所有子节点的掩码的异或和。

然后,我们就注意到,挑选一些边涂色,这时被满足(即有至少一条边被涂色)的简单路径的集合就是这些边对应的掩码(集合)的并集(或者按位或)。

至此,我们就完成了所有平凡的预处理工作。问题被转化为:

给定若干集合,求最少需要选取多少集合才能使得它们的并集为全集。

这个问题是本题的核心,也是一个非常经典的 NP-hard 问题。

我们考虑将最优化问题转化为判定问题:从小到大枚举需要多少集合,再判定这是否可行。

注意到答案最大等于全集大小。因为每个集合至少贡献一位,否则直接去除它更好。

进一步转化为计数问题:选出 $x$ 个集合,有多少种方案使得它们的并集为全集?

原题在这里要求选出的集合互不相同,但选出重复集合并不影响。

考虑哈集幂。我们构造一个长度为 $2^k$ 的序列 $a_i$(数组),其中第 $s$ 个位置是选出 $i$ 个集合,其并集是掩码为 $s$ 的集合的方案数。

显然有 $a_i = (a_1)^i$,这里的乘法是并集卷积,即 $(\text{or}, \times)$ 卷积。意思是,$a\times b$ 的第 $k$ 位是所有满足 $i \text{ or } j = k$ 的 $a_i \times b_j$ 的和。

然后套用 SOS dp 和高维前缀和/FWT 模板即可。

放一个 AC 代码,里面涉及到一些实现细节。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
#include <bits/stdc++.h>

using namespace std;
int t;
array<uint32_t, 100005> mask;
array<vector<int>, 100005> e;

void dfs(int u, int fa) {
    for (int v: e[u]) {
        if (v != fa) {
            dfs(v, u);
            mask[u] ^= mask[v];
        }
    }
}

template<typename T>
constexpr uint64_t mod(T res) {
    constexpr uint64_t mod_p = (1ull << 61) - 1;
    res = (res >> 61) + (res & mod_p);
    res = (res >> 61) + (res & mod_p);

    if (res == mod_p) {
        return 0;
    }
    return res;
}

uint64_t quick_pow(uint64_t a, uint64_t b) {
    uint64_t res = 1;
    while (b) {
        if (b & 1) {
            res = mod((unsigned __int128) res * a);
        }

        a = mod((unsigned __int128) a * a);
        b >>= 1;
    }
    return res;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> t;
    for (int _ = 0; _ < t; ++_) {
        int n;
        cin >> n;

        for (int i = 1; i <= n; ++i) {
            e[i].clear();
            mask[i] = 0;
        }

        for (int i = 1; i < n; ++i) {
            int u, v;
            cin >> u >> v;
            e[u].push_back(v);
            e[v].push_back(u);
        }

        int k;
        cin >> k;
        for (int i = 0; i < k; ++i) {
            int x, y;
            cin >> x >> y;
            mask[x] ^= (1 << i);
            mask[y] ^= (1 << i);
        }

        dfs(1, 0);

        vector<uint32_t> cnt(1 << k);
        for (int i = 1; i <= n; ++i) {
            cnt[mask[i]]++;
        }

        for (int i = 0; i < k; ++i) {
            for (uint32_t j = 0; j < (1 << k); ++j) {
                if ((j >> i) & 1) {
                    cnt[j] += cnt[j ^ (1 << i)];
                }
            }
        }

        for (int ans = 1; ans <= k; ++ans) {
            uint64_t term = 0;
            for (uint32_t i = 0; i < (1 << k); ++i) {
                const uint64_t tmp = quick_pow(cnt[i], ans);
                term = mod((1ll << 61) - 1 + term - ((k - popcount(i)) & 1 ? tmp : -tmp));
            }

            if (term) {
                cout << ans << "\n";
                break;
            }
        }
    }
    return 0;
}