【BZOJ3812】【UOJ37】【清华集训2014】主旋律

【题目链接】

  • BZOJ
  • UOJ

【思路要点】

  • 我们希望求出使得图强联通的边集数,这等价于求出所有边集数减去使得图不强连通的边集数。
  • 首先考虑一种非常暴力的做法,我们枚举最终的图缩点后的情况。那么,剩下的图必须是一个DAG,我们希望求出这张图在是DAG的情况下可行的边集数,并与每一个强连通分量加边方案数相乘,得到答案。后面的这个问题是原问题的一个子问题,递归对对应强联通分量的导出子图求解即可。
  • 问题在于求解一张图在是DAG的情况下可行的边集数。
  • 我们知道,DAG一定有一系列入度为0的点,这一系列点之间没有边,而它们可以向DAG的剩余部分任意连边。令\(F_S\)表示点集\(S\)的导出子图中使得子图形成一个DAG的边集数。我们很自然地想到,可以枚举\(S\)的非空子集\(T\),那么应当有:$$F_S=\sum_{T\in S,T\ne\varnothing}F_{S-T}*2^{Cnt_{T,S-T}}$$
  • 其中\(Cnt_{A,B}\)表示出点在点集\(A\)中,入点在点集\(B\)中的边数。
  • 但这是错的,因为我们不能保证每一个点都连出至少一条边,所以我们会重复统计许多情况。
  • 解决的方式是容斥原理:$$F_S=\sum_{T\in S,T\ne\varnothing}(-1)^{|T|-1}*F_{S-T}*2^{Cnt_{T,S-T}}$$
  • 这样我们就得到了一个复杂度仍然很大的非暴力算法。
  • 但如果仔细观察上述转移,我们发现某一个子集\(T\)贡献的系数只与其包含的强连通分量个数的奇偶性有关,奇数为正,偶数为负,所以枚举最终缩点的结果是没有必要的。
  • 设\(F_S\),表示点集\(S\)的导出子图中使得图强联通的边集数量。设\(G_S\)和\(H_S\)分别表示点集\(S\)的导出子图中的边集的数量,使得图中形成奇/偶数个强联通分量,并且这些强联通分量互相之间没有边。
  • 那么应当有$$F_S=2^{Cnt_{S,S}}-\sum_{T\in S,T\ne\varnothing}(G_T-H_T)*2^{Cnt_{T,S-T}+Cnt_{S-T,S-T}}$$
  • 而对于\(G_S\)和\(H_S\),显然有转移$$G_S=\sum_{T\in S,T\ne\varnothing}F_T*H_{S-T}$$$$H_S=\sum_{T\in S,T\ne\varnothing}F_T*G_{S-T}$$
  • 需要注意的是上述方程中看似\(F_S\)和\(G_S\)、\(H_S\)会互相转移,但实际上当\(T=S\),转移得到\(F_S\)时,我们用到的\(G_S\)不应该包括\(S\)本身强联通的情况,所以可以先转移得到\(F_S\),再计算\(G_S\)和\(H_S\)。
  • 通过适当地预处理,我们可以得到\(O(\frac{M*3^N}{w}+M*2^N)\)的时间复杂度,其中\(w=64\)

【代码】

#include
using namespace std;
const int MAXS = 32768;
const int MAXM = 205;
const int MAXN = 20;
const int P = 1e9 + 7;
template  void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template  void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template  void writeln(T x) {
	write(x);
	puts("");
}
bitset  in[MAXS], out[MAXS];
int n, m, u, bit[MAXN], two[MAXM];
long long f[MAXS], g[MAXS], h[MAXS];
int cnt(int s, int t) {return (out[s] & in[t]).count(); }
int main() {
	read(n), read(m); u = (1 << n) - 1;
	for (int i = 1; i <= n; i++)
		bit[i] = 1 << (i - 1);
	two[0] = 1;
	for (int i = 1; i <= m; i++)
		two[i] = two[i - 1] * 2 % P;
	for (int i = 1; i <= m; i++) {
		int x, y; read(x), read(y);
		for (int s = 0; s <= u; s++) {
			if (s & bit[x]) out[s].set(i);
			else out[s].reset(i);
			if (s & bit[y]) in[s].set(i);
			else in[s].reset(i);
		}
	}
	f[0] = 0, g[0] = 0, h[0] = 1;
	for (int s = 1; s <= u; s++) {
		f[s] = two[cnt(s, s)];
		int tmp = s & -s;
		for (int t = (s - 1) & s; t != 0; t = (t - 1) & s) {
			f[s] -= (g[t] - h[t]) * two[cnt(t, s - t) + cnt(s - t, s - t)] % P;
			if ((t & tmp) == 0) continue;
			g[s] += f[t] * h[s - t] % P;
			h[s] += f[t] * g[s - t] % P;
		}
		f[s] -= g[s] - h[s];
		f[s] = (f[s] % P + P) % P;
		g[s] += f[s];
		g[s] %= P, h[s] %= P;
	}
	writeln(f[u]);
	return 0;
}

你可能感兴趣的:(【OJ】BZOJ,【OJ】UOJ,【类型】做题记录,【算法】动态规划,【算法】容斥原理,【算法】压位,【算法】枚举子集)