详细揭秘 [清华集训 2014] 主旋律

强连通子图计数。

\(1\le n\le 15, 0\le m\le n(n-1)\)

大体思路:先刻画一张图在「强连通」视角下的结构、设计状态,进行图计数。

这题的难点在于每一步的处理手法都很经典,但却不算简单,并且解题的链条拖的很长,一旦想不清楚就容易头晕。

结构

一张图在「强连通」的视角下只有两种情况:① 整张图都强连通;②若干个强连通分量构成一个 DAG。

经验告诉我要用 DP 解决这道题,由于第二种情况出现了子结构,所以设 \(f(S)\) 表示 \(S\) 点集内子图构成强连通图的方案数,用 \(2^{|E|}\) 减去情况②的方案数。

图计数

这就是 有标号 DAG 计数,只不过需要考虑怎么把强连通分量组合起来。不过还是简单复述一下 DAG 计数的手法:

对 DAG 结构的拆解可以用分层的模式划分 (一个相似的小练习 Rikka with Game Theory),把 DAG 分成:① 一开始入度为 0 的点;② 去掉 ① 后入度为 0 的点;③ 依次类推。

比较好做的只有钦定一部分点初始入度为 0,好在根据广义二项式反演推得一个容斥系数 \((-1)^{j-1}\),其中 \(j\) 是钦定的强连通分量数。这样只需要做钦定的部分,乘个 \((-1)^{j-1}\) 就完事了。需要的限制是 \(j\) 个联通分量之间没边,还需要知道 \(j\) 个连通分量向 \(S\)​ 中其它点连了多少边。这都是可以预处理的。

(这里我想了想,加深了一点对二项式反演的理解:二项式反演的正确性和贡献的计算无关,比如原本是 恰好 \(S\),现在是 钦定 \(S\),它的作用是说不管你 \(S\) 钦定的方案咋算,只要它不是恰好 \(S\),就不会被算到 恰好 \(S\) 的方案里。)

拉回注意力,我们现在需要的是组合起来若干个入度为 0 的联通分量,为了方便配合贡献计算进行预处理,考虑枚举这些强联通分量的并对应的 \(S\) 的子集 \(T\),算出所有可能的组合对应的容斥系数之和即可。

接下来来算容斥系数之和。这个手法在不少习题里都有出现,我知道一个 [ABC180F] Unbranched,核心想法就是只关注某个特殊点对应的子结构。用最高位或者 lowbit 作为特殊点,然后枚举其所在的强连通分量即可实现进一步的子结构拆分,从而完成递推。

代码里的处理细节仍然很多,而且方法各异,我只给出我的处理。

不过现在一个我不理解的点是,什么样的结构可以像 DAG 计数一样拼凑出一个容斥系数。

upd: 这个容斥系数好像并不是恰好凑出来的,实质上和那个经典的 “钦定 \(k\) 个限制不满足,容斥系数 \((-1)^k\)” 就是一个东西。

#include <bits/stdc++.h>
#define pb emplace_back
#define fir first
#define sec second

using i64 = long long;
using pii = std::pair<int, int>;

constexpr int mod = 1e9 + 7;
void add(int& x, int y) { if ((x += y) >= mod) x -= mod; return; }
void sub(int& x, int y) { if ((x -= y) < 0) x += mod; return; }

constexpr int N = 15, M = (N - 1) * N;
int out[N], in[N], E[1 << N], G[1 << N], F[1 << N], Coef[1 << N], pw[M + 5];
int n, m;

int log2(int x) {
    return 31 - __builtin_clz(x);
} 

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    std::cin >> n >> m;
    pw[0] = 1;
    for (int i = 1; i <= m; ++i) pw[i] = 2 * pw[i - 1] % mod;
    while (m--) {
        int x, y;
        std::cin >> x >> y, --x, --y;
        out[x] |= 1 << y;
        in[y] |= 1 << x;
    }
    // E[s] : s -> s 的边
    for (int s = 0; s < (1 << n); ++s)
        for (int i = 0; i < n; ++i)
            if (s >> i & 1) E[s] += __builtin_popcount(out[i] & s);
    int U = (1 << n) - 1;
    Coef[0] = mod - 1;
    for (int s = 1; s < (1 << n); ++s) {
        // G[t] : t -> s \ t 的边
        int p = log2(s), st = s ^ (1 << p);
        for (int t = st; ; t = (t - 1) & st) {
            sub(Coef[s], 1ll * Coef[st ^ t] * F[t | (1 << p)] % mod);
            if (!t) break;
        }
        G[s] = 0;
        for (int t = s; t; t = (t - 1) & s) {
            if (t < s) {
                int p = log2(s ^ t);
                G[t] = G[t | (1 << p)] - __builtin_popcount(out[p] & (s ^ t ^ (1 << p))) + __builtin_popcount(in[p] & t);
            }
            sub(F[s], 1ll * Coef[t] * pw[G[t] + E[s ^ t]] % mod);
        }
        add(F[s], pw[E[s]]);
        add(Coef[s], F[s]);
        // F[s] : 和题解保持一致
        // Coef[s] : s 对应的所有划分的容斥系数之和
    }
    std::cout << F[U] << '\n';
    return 0;
}
posted @ 2026-08-24 16:17  Kurisuki  阅读(6)  评论(0)    收藏  举报