[BZOJ3812][UOJ#37][清华集训2014]主旋律(状压 DP + 容斥原理)

Address

Solution

  • 考虑状压 DP ,定义状态
  • f [ S ] f[S] f[S] 表示原图的点集 S S S 的导出子图中,有多少个边子集能强连通点集 S S S
  • g [ S ] g[S] g[S] 表示将原图的点集 S S S 分割成一些非空子集,每个点子集分别用其内部的边将其强连通的方案数,分成的子集个数为奇数时计入 g [ S ] g[S] g[S] ,子集个数为偶数时从 g [ S ] g[S] g[S] 里扣掉
  • 如果没有强连通的要求,那么显然 f [ S ] = 2 c n t [ S ] f[S]=2^{cnt[S]} f[S]=2cnt[S] c n t [ S ] cnt[S] cnt[S] 为点集 S S S 的导出子图边数)
  • 对一个状态 S S S 处理时,先完成 g g g 的部分转移
  • g [ S ] = − ∑ T ⊂ S , 0 &lt; ∣ T ∣ &lt; ∣ S ∣ , i d ∈ T f [ T ] g [ S − T ] g[S]=-\sum_{T\subset S,0&lt;|T|&lt;|S|,id\in T}f[T]g[S-T] g[S]=TS,0<T<S,idTf[T]g[ST]
  • 注意上面的 i d id id S S S 内首个元素,限制 i d ∈ T id\in T idT 是为了避免重复计数
  • 这时候 g [ S ] g[S] g[S] 还没记录 S 只分成一个子集即本身的方案
  • 然后考虑 f [ S ] f[S] f[S] ,对于满足 T ⊂ S , ∣ T ∣ &gt; 0 T\subset S,|T|&gt;0 TS,T>0 的任意集合 T T T ,我们需要扣除掉入度为 0 0 0 的强连通分量包含的点集为 T T T 的方案数
  • 这扣掉的方案数好像就是
  • 点 集 T 划 分 成 互 相 独 立 的 强 连 通 图 的 方 案 数 × 2 c n t 2 [ S ] [ T ] 点集T划分成互相独立的强连通图的方案数\times2^{cnt_2[S][T]} T×2cnt2[S][T]
  • 其中 c n t 2 [ S ] [ T ] cnt_2[S][T] cnt2[S][T] 表示两端都在点集 S S S 中且指向点集 T T T 外的边数 ,怎么求各凭本事
  • 然后我们发现上面其实是有问题的,因为我们没办法知道点集 S − T S-T ST 内是否有入度为 0 0 0 的强连通分量 ,会算重复
  • 于是我们考虑容斥
  • T T T 划分成奇数个互相独立的强连通图时有正贡献,否则负贡献
  • 转移方程出来了
  • f [ S ] = 2 c n t [ i ] − ∑ T ∈ S , ∣ T ∣ &gt; 0 2 c n t 2 [ S ] [ T ] g [ T ] f[S]=2^{cnt[i]}-\sum_{T\in S,|T|&gt;0}2^{cnt_2[S][T]}g[T] f[S]=2cnt[i]TS,T>02cnt2[S][T]g[T]
  • 注意,这时候 g [ S ] g[S] g[S] 还没记录 S S S 只分成一个子集即本身的方案,所以这时还要将 g [ S ] g[S] g[S] 加上 f [ S ] f[S] f[S]
  • 最后答案就是 f [ 全 集 ] f[全集] f[]

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

inline int read()
{
	int res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	return bo ? ~res + 1 : res;
}

const int N = 19, M = N * N, C = (1 << 15) + 5, ZZQ = 1e9 + 7;

int n, m, Cm, pw[M], f[C], g[C], cnt[C], cnt2[N][C], sze[C], tot, st[C], rp[C];
bool G[N][N];

int main()
{
	n = read(); m = read();
	pw[0] = 1;
	for (int i = 1; i <= m; i++)
		pw[i] = 2 * pw[i - 1] % ZZQ;
	while (m--) G[read()][read()] = 1;
	Cm = 1 << n;
	for (int S = 1; S < Cm; S++)
		for (int u = 1; u <= n; u++) if ((S >> u - 1) & 1)
			for (int v = 1; v <= n; v++)
				if (((S >> v - 1) & 1) && G[u][v])
					cnt[S]++, cnt2[v][S]++;
	for (int i = 1; i <= n; i++) rp[1 << i - 1] = i;
	for (int S = 1; S < Cm; S++)
	{
		if (S == (S & -S))
		{
			f[S] = g[S] = 1;
			continue;
		}
		tot = 0;
		int id = rp[S & -S];
		for (int T = (S - 1) & S; T; T = (T - 1) & S)
			st[++tot] = T;
		for (int i = tot; i >= 1; i--)
		{
			int s = st[i], t = s & -s;
			sze[s] = sze[s ^ t] + cnt2[rp[t]][S];
		}
		f[S] = pw[cnt[S]];
		for (int T = (S - 1) & S; T; T = (T - 1) & S) if ((T >> id - 1) & 1)
			g[S] = (g[S] - 1ll * f[T] * g[S ^ T] % ZZQ + ZZQ) % ZZQ;
		for (int T = S; T; T = (T - 1) & S)
			f[S] = (f[S] - 1ll * g[T] * pw[sze[S ^ T]] % ZZQ + ZZQ) % ZZQ;
		g[S] = (g[S] + f[S]) % ZZQ;
	}
	std::cout << f[Cm - 1] << std::endl;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值