容斥、染色类计数问题

染色相关

染色问题是一类NPC问题。

它的一般形式是给定一个无向联通图 G < n , m > G_{<n,m>} G<n,m>,要求用 k k k种颜色对其染色。使得每一条边所连的两个端点不同色。

这一类问题通常需要很高的时间复杂度。但在特殊的图中,这一类问题能得到很优秀的解法。

例1 jzoj6079

Problem

给定无向联通图 G < n , m > G_{<n,m>} G<n,m>,要求 k k k染色的方案数

n ≤ 1 0 5 , m ≤ n + 5 , k ≤ 1 0 5 n\le 10^5, m\le n+5, k\le 10^5 n105,mn+5,k105

Solution

很经典的一道题目。

首先注意到, m ≤ n + 5 m\le n + 5 mn+5这个有用的性质。

再观察一下 k k k染色图的一些性质。

注意到,度数为 1 1 1的点是可以直接缩掉的。

然后转化一下,把一条边看做两个边权组成,一个边权表示两个点颜色相同时的方案,另一个则表示两个点颜色不同时的方案,显然一开始边权的组成是 ( 0 , 1 ) (0,1) (0,1)

然后考虑缩一下度数为 2 2 2的点。

事实上是可以通过推式子得出的。然后会发现缩完之后最终图的大小是 n ≤ 10 , m ≤ 15 n\le 10,m\le15 n10,m15的。

于是直接搜索就好了。

例2 jzoj

Problem

给定 m m m条原始边,并按照以下规则加边,得到的最终图求 n n n染色方案。

a < b < c a\lt b\lt c a<b<c,且 ( a , b ) , ( a , c ) (a,b),(a,c) (a,b),(a,c)有边,则连 ( a , c ) (a,c) (a,c)

n , m ≤ 1 e 6 n,m\le 1e6 n,m1e6

Solution

观察到这道题连边的特殊性质。

可以发现,如果按节点编号从大到小染色,假设一个点的度数为最终图里连向多少条比它编号大的点,那么答案很显然就是 ∏ i n − 度 数 i \prod_i n-度数_i ini

因为比它大的点颜色都互不相同!

现在关键就变成了求最终图里每个点的度数

然后发现一个很重要的性质:

如果最终图里两个点 x , y x,y x,y有边,则必然可以在原图中找到一条路径,使得这个路径上点的编号都小于 m i n ( x , y ) min(x,y) min(x,y).

于是就可以从小到大去构图,每次维护好图的连通性,每次当前点所在联通块有多少个相邻点就是当前点的度数了。

这个可以用启发式合并或者线段树合并。事实上启发式合并由于有set,map这种高级骚操作,所以时间复杂度虽然是两个 l o g log log的,但依然跑的比线段树的一个 l o g log log快。

容斥相关

计数题一般有容斥,这是计数题的一大特点。

例3 jzoj3170

Problem

给定 n n n个箱子,每个箱子里有若干种玩具,玩具编号从 1 ∼ m 1\sim m 1m,然后让你求有多少种选择箱子的方案,使得每种玩具都至少有一个。

n ≤ 1 0 6 , m ≤ 20 n\le 10^6, m\le 20 n106,m20

Solution

首先,要求至少,那肯定一波容斥了。转化为求一个都没有的方案。

然后必然是要枚举哪些至少没有,第二次容斥。

那么问题现在就转化为了, n n n个箱子中选哪些箱子,不包括给定集合。

然后考虑它的对应问题, n n n个箱子中选哪些箱子,被给定集合包括,这里的包括显然指的是真子集。

然后这个有个经典做法是分治。考虑把 m m m位从高位到低位处理。然后考虑当前这一位的贡献,可以由 f i → f i + m i d f_i\rightarrow f_{i+mid} fifi+mid

我们来考虑这样做法的正确性。

由于最终计数的 f i f_i fi表示的是 i i i的所有子集中包含了多少个箱子,换句话说,这些箱子的并集 ∈ i \in i i。由于我们依次考虑了 i i i的某一位是否为 1 1 1时的贡献,且当某一位为 0 0 0时,我们不会累加为 1 1 1的答案。换句话说,只有当某一位为 1 1 1时,我们会累加 0 , 1 0,1 0,1的答案,所以可以累加出最终答案。

Code
#include 

#define F(i, a, b) for (int i = a; i <= b; i ++)

const int T = 1e6 + 10, Mo = 1e9 + 7, S = 2e6;

using namespace std;

int n, m, tot, x, sum, N, ans;
int shl[T], f[S];

void CDQ(int l, int r) {
	if (l >= r) return;
	int m = l + r >> 1;
	CDQ(l, m), CDQ(m + 1, r);
	F(i, l, m)
		f[m + i - l + 1] += f[i];
}

int main() {
	scanf("%d%d", &n, &m), shl[0] = 1;
	F(i, 1, max(n, m)) shl[i] = (shl[i - 1] * 2LL) % Mo;

	N = shl[m] - 1;
	F(i, 1, n) {
		scanf("%d", &tot), sum = 0;
		F(j, 1, tot)
			scanf("%d", &x), sum += shl[x - 1];
		f[sum] ++;
	}

	CDQ(0, N);

	F(i, 0, N) {
		int tot = 0;
		for (int x = i; x; x -= x & (- x), tot ++);
		if (tot & 1) ans = (ans - (shl[f[N - i]] - 1)) % Mo; else ans = (ans + (shl[f[N - i]] - 1)) % Mo;
	}

	printf("%d\n", (ans + Mo) % Mo);
}

例4 jzoj5664

Problem

在这里插入图片描述

  • M x , M y , T x , T y ≤ 800 , K ≤ 50 , R ≤ 1600 Mx,My,Tx,Ty\le 800,K\le 50,R\le 1600 Mx,My,Tx,Ty800,K50,R1600
Solution

还是很经典的一道题目。

首先必然要考虑容斥了,因为有些向量不能走。令 s k s_k sk表示走了至少 k k k次不合法向量的方案数,那么答案可以写成 A n s = ∑ i = 0 r ( − 1 ) i s i Ans=\sum_{i=0}^r(-1)^is_i Ans=i=0r(1)isi

考虑如何求 S i S_i Si,这个是很经典的。

容易想到令 g i , x g_{i,x} gi,x表示只走不合法向量,到达 ( 10 x , 10 x ) (10x,10x) (10x,10x)的方案数。

f i , x , y f_{i,x,y} fi,x,y表示 i i i步,任意向量,到达 ( x , y ) (x,y) (x,y)的方案数。

那么就有 s ( k ) = ∑ i = 0 m i n ( T x 10 , T y 10 ) ( r k ) ∗ g [ k ] [ i ] ∗ f [ r − k ] [ T x − 10 i ] [ T y − 10 i ] s(k)=\sum_{i=0}^{min(\frac{T_x}{10},\frac{T_y}{10})}\binom{r}{k}*g[k][i]*f[r-k][T_x-10i][T_y-10i] s(k)=i=0min(10Tx,10Ty)(kr)g[k][i]f[rk][Tx10i][Ty10i]

然后现在就把问题变为求 f i , x , y f_{i,x,y} fi,x,y了。

这里有一个很经典的套路就是 降维。

考虑 F x i , j , F y i , j Fx_{i,j},Fy_{i,j} Fxi,j,Fyi,j分别表示走 i i i步,到 x x x轴的第 j j j个点, y y y轴的第 j j j个点的方案。

那么就有 f i , x , y = F x i , x ∗ F y i , y f_{i,x,y}=Fx_{i,x}*Fy_{i,y} fi,x,y=Fxi,xFyi,y。这个是很容易证明的。

而求 F x , F y Fx,Fy Fx,Fy则可以很快求出。

所以时间复杂度很优秀。

注意边界。

Code
#include 
#include 
#include 

#define F(i, a, b) for (int i = (a); i <= (b); i ++)
#define G(i, a, b) for (int i = (a); i >= (b); i --)
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))
#define add(a, b) ((a) = (a + b) % Mo)

const int Mo = 1e4 + 7;
const int R = 1601, N = 801, T = 51;

using namespace std;

int Answer, Tx, Ty, Mx, My, r, m, Limit, c[R][R];
int g[R][N / 10], k[T], Fx[R][N], Fy[R][N], Sx[R][N], Sy[R][N];

int S(int k) {
	int ans = 0;
	F(i, 0, Limit)
		add(ans, 1ll * c[r][k] % Mo * g[k][i] * Fx[r - k][Tx - 10 * i] * Fy[r - k][Ty - 10 * i] % Mo);
	return ans;
}

int ksm(int x, int y) {
	int ans = 1;
	for (; y ; y >>= 1, x = (1ll * x * x) % Mo)
		if (y & 1)
			ans = (1ll * ans * x) % Mo;
	return ans;
}

int main() {
	freopen("jump.in", "r", stdin);
	freopen("jump.out", "w", stdout);

	scanf("%d%d%d%d%d%d", &Tx, &Ty, &Mx, &My, &r, &m);
	F(i, 1, m) scanf("%d", &k[i]), k[i] /= 10;

	Limit = min(Tx / 10, Ty / 10), g[0][0] = 1;
	F(i, 0, r - 1)
		F(j, 0, Limit)
			if (g[i][j])
				F(p, 0, m)
					if (j + k[p] <= Limit)
						add(g[i + 1][j + k[p]], g[i][j]);

	Fx[0][0] = 1;
	F(j, 0, Tx) Sx[0][j] = 1;
	F(i, 1, r) F(j, 0, Tx) {
		if (j - Mx <= 0)
			Fx[i][j] = Sx[i - 1][j];
		else
			Fx[i][j] = Sx[i - 1][j] - Sx[i - 1][j - Mx - 1];
		if (j) Sx[i][j] = Sx[i][j - 1];
		add(Sx[i][j], Fx[i][j]);
	}
	Fy[0][0] = 1;
	F(j, 0, Ty) Sy[0][j] = 1;
	F(i, 1, r) F(j, 0, Ty) {
		if (j - My <= 0)
			Fy[i][j] = Sy[i - 1][j];
		else
			Fy[i][j] = Sy[i - 1][j] - Sy[i - 1][j - My - 1];
		if (j) Sy[i][j] = Sy[i][j - 1];
		add(Sy[i][j], Fy[i][j]);
	}

	F(i, 0, r) c[i][0] = 1;
	F(i, 1, r)
		F(j, 1, r)
			c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % Mo;

	F(i, 0, r)
		add(Answer, ((i & 1) ? ( - 1) : ( 1 )) * S(i));

	printf("%d\n", (Answer + Mo) %Mo);
}

你可能感兴趣的:(容斥原理,计数)