【学习笔记】组合计数 & 计数DP
还没更完还没更完还没更完还没更完还没更完还没更完还没更完
组合计数
加法原理、乘法原理(分类、分步)
组合数,排列数
DP(递推)
容斥
二项式反演
钦定意义下
form 1. 至少 \(\Leftrightarrow\) 恰好:
\(f(x) = \sum_{i=x}^n C(i, x) g(i) \Leftrightarrow g(x) = \sum_{i=x}^n (-1)^{i-x} C(i, x) f(i)\);
form 2. 至多 \(\Leftrightarrow\) 恰好
\(f(x) = \sum_{i=0}^{x} C(n-i,n-x) g(i) \Leftrightarrow g(x) = \sum_{i=0}^x (-1)^{x-i} C(n-i , n-x) f(i)\) ;
details
组合意义上,是我们钦定 $x$ 个性质要满足。其余任意选择满足或不满足。 这里 $n$ 个「限制/性质/属性」互不相同的,即会产生重复($0011$ 和 $0101$ 不同)。 理解重复:$\text{e.g.}$ $\text{_11_:0111}$,$\text{__11:0111}$。
\(\text{form 1.}\) 组合意义很直白,\(\text{form 2.}\) 的证明:
左式:
\(f(x)\) 表示至多选多少
\(f'(x)\) 表示钦定 \(x\) 个不能选,满足 \(\text{form 1.}\)
\(f'(x) = \sum_{i=x}^n C(i, x) g'(i)\)
\(f(n-x) = \sum_{i=x}^n C(i, x) g(n-i)\)
\(f(x) = \sum_{i=n-x}^n C(i, n-x) g(n-i)\)
\(f(x) = \sum_{i=0}^{x} C(n-i, n-x) g(i)\)
右式:
\(g(n-x) = \sum_{i=x}^n (-1)^{i-x} C(i, x) f(n-i)\)
\(g(x) = \sum_{i=n-x}^n (-1)^{i-(n-x)} C(i, n-x) f(n-i)\)
\(g(x) = \sum_{i=0}^x (-1)^{(n-i)-(n-x)} C(n-i , n-x) f(i)\)
\(g(x) = \sum_{i=0}^x (-1)^{x-i} C(n-i , n-x) f(i)\)
选择意义下
form 2. 至多 \(\Leftrightarrow\) 恰好:
\(f(x) = \sum_{i=0}^x C(x, i) g(i) \Leftrightarrow g(x) = \sum_{i=0}^x (-1)^{x-i} C(x, i) f(i)\);
form 1. 至少 \(\Leftrightarrow\) 恰好
\(f(x) = \sum_{i=x}^{n} C(n-x,n-i) g(i) \Leftrightarrow g(x) = \sum_{i=x}^n (-1)^{i-x} C(n-x , n-i) f(i)\) ;
details
我们 $n$ 个性质是等价的,即 $0011$ 和 $0101$ 是等价的,不会产生重复。 具体一点:根据 $\text{form 2.}$,$f(2)$ 统计的是局面为 $00,01,10,11$ 的方案数之和,虽然 $01$ 和 $10$ 方案数相同,但是具体的方案不一样,我们用 $C(x,i)$ 进行了统计。
- 特殊数列(卡特兰数)
- 卡特兰数:栈的合法操作序列、合法括号序列。\(\frac{C_{2n}^{n}}{n+1} = C_{2n}^{n}-C_{2n}^{n-1}\)。
 
 
VJ大专题
stO谢拜龚神Orz
- T1:反面计数、排列组合、高精度
关键元素是老师
反面计数
\(n+2\) 个人,女生不相邻的放进去,\(C_{n+3}^{m} \times (n+2)! \times m!\)
\(n+1\) 个人,女生不相邻的放进去,\(C_{n+2}^{m} \times (n+1)! \times m!\)
\(2000\) 多的组合排列,用高精。 - T2:排列组合
抽象切割的过程:从两边向中间切割,满足左右切割点为 \(p_1,p_2,\cdots,p_k,q_k,\cdots,q_2,q_1\),\(pre_{q_i} = suf_{q_i}\)。
注意细节的构造:\(suf_i\) 表示 \(sum(i+1,\cdots,n)\)。左右一一对应,当 \(p_k = q_k\),为偶回文,否则为奇回文。
对于两个区间 \([l_1,r_1],[l_2,r_2]\),有 \(pre[i \in [l_1,r_1]] = suf[i \in [l_2,r_2]]\)。两边互不影响,方案为 \(\sum C_{r_1-l_1+1}^{p} \sum C_{r_2-l_2+1}^{p}\)。
对于 \(l_1 = l_2,r_1 = r_2\),方案数为 \(2^{r_1-l_1+1}\)。 - T3:卡特兰数、找性质
其实 \(2n\) 提醒我们这与卡特兰数有关。
我们可以的到显然的结论:一个偶数位上的数大于之前的所有数。进而,偶数位 i 上放的数必然大于 i。
进而抽象构造序列的过程:从 \(1 \sim 2n\) 放序列,每次放在最靠前的奇数位或偶数位。
模拟这一过程,可以得到整道题最重要的结论:当前序列中偶位有的个数不能大于奇数位。
故答案为 \(Catalan(n)\)。其计算:质数筛+约分+快速幂。 这里约分传递幂次的 trick: 
for(int i=2*n;i>1;--i)
    if(mp[i]<i){//如果是合数,向下传递,可以保证O(n)
    //mp[i] 为 i 的最小质因数 
    cnt[mp[i]]+=cnt[i];
    cnt[i/mp[i]]+=cnt[i];
}
容斥:
- 是要同时满足若干个条件的计数问题;
 - 若干个条件如果去掉(或者钦定不满足)以后,计数的做法不难;
 - 时间复杂度是状态个数的高次(或其他增长快于线性的)函数。
 
括号有关问题
P3058 [USACO12NOV] Balanced Cow Breeds G/S
对于括号类问题,研究其合法性时,一个重要的性质就是这一路过来都合法(和栈类似)。
套路地,将 \(\texttt{(}\) 看做 \(+1\),\(\texttt{)}\) 看做 \(-1\),那么该序列合法就是其所有前缀和都 \(\ge 0\)。设计 \(dp\),\(dp[i][j][k]\) 表示标记了前 \(i\) 个数,\(A\) 序列的前缀和为 \(j\),\(B\) 序列的前缀和为 \(k\),的方案数。
这里自然保证了 \(j,k \ge 0\)。
但是时间复杂度是 \(O(n^3)\)。
观察到 \(j+k = sum\) 这一特征,即 \(i,j\)  已知足以只 \(k\)。
去掉 \(k\) 这一维,有转移:
这里保证 \(sum-j \ge 0\) 即可。
点我展开看代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 2012;
char s[1010];
int n,a[1010],f[1010][1010];
inline int add(int x,int y){ return x + y >= mod ? x + y - mod : x + y; }
inline void toadd(int &x,int y){ x = add(x,y); }
int main(){
	scanf("%s",s+1);
	n = strlen(s+1);
	for(int i = 1;i<=n;++i){
		if(s[i] == '(')a[i] = 1;
		else a[i] = -1;
	}
	f[0][0] = 1;
	for(int i = 1,sum = 0;i<=n;++i){
		sum += a[i];
		for(int j = 0;j<=sum;++j){
			f[i][j] = f[i-1][j];
			if(sum - j >= 0)toadd(f[i][j],f[i-1][sum-j]);
		}
	}
	printf("%d",f[n][0]);
	return 0;
}
P3059 [USACO12NOV] Concurrently Balanced Strings G
同上题类似,应当满足 \(sum_{l-1} = sum_r,sum_i \le sum_r(i ∈ [l,r-1])\)。
对于第二条式子,对于每个 \(l\),找到第一个不满足的 \(r\),就可以确定位置范围,再让第一条去限定即可。
但是对每个 \(l\) 而言会有多个 \(r\) 满足条件,我们发现对于满足条件的 \(r1,r2\),两边是相互独立且都合法的,那么方案数就有了继承关系 \(g_{r2} + 1 \to g_{r1}\)。那么我们找到最左边的即可。
我们这个前缀序列的性质就是每次只会 \(+1,-1\),故第一个小于 \(sum\) 的位置,其值一定为 \(sum-1\),那么倒着扫的同时维护即可。
对于第一条式子,应当快速判断信息相同,故对每列做哈希,可以使用 map。
点我展开看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod1 = 1e9 + 7, mod2 = 1e9 + 9, bas = 50000;
int n,k;
char s[50010];
int sum[15][50010];
ll hsh1[50010],hsh2[50010];
int pos[50010 << 1], lim[50010],r[50010];
ll f[50010];
struct Donny {
	size_t operator () (pair<ll, ll> donny) const {
		return donny.first * 111ll + donny.second;
	}
};
unordered_map<pair<ll, ll>, int, Donny> mp;
int main(){
	scanf("%d%d",&k,&n);
	for(int i = 1;i<=k;++i){
		scanf("%s",s+1);
		for(int j = 1;j<=n;++j){
			if(s[j] == '(')sum[i][j] = sum[i][j-1] + 1;
			else sum[i][j] = sum[i][j-1] - 1;
			hsh1[j] = (hsh1[j]*133%mod1+sum[i][j])%mod1;
			hsh2[j] = (hsh2[j]*133%mod2+sum[i][j])%mod2;
		}
	}
	memset(lim,0x3f,sizeof lim);
	for(int i = 1;i<=k;++i){
		memset(pos,0x3f,sizeof pos);
		for(int j = n;j;--j){
			pos[sum[i][j] + bas] = j;
			lim[j] = min(lim[j],pos[sum[i][j-1] - 1 + bas]);
		}
	}
	for(int i = n;i;--i){
		r[i] = mp[{hsh1[i-1],hsh2[i-1]}];
		mp[{hsh1[i],hsh2[i]}] = i;
	}
	ll ans = 0;
	for(int i = n;i;--i){
		if(r[i] && r[i] < lim[i]){
			f[i] = f[r[i]+1] + 1;
			ans += f[i];
		}
	}
	printf("%lld",ans);
	return 0;
}
运货司机
点我展开看代码
#include<cstdio>
const int N = 55, mod = 10007;
int n,m,K;
int a[N][N],g[N][N][N],h[N][N][N],f[N][N][N];
inline int mul(const int x,const int y){ return 1ll * x * y % mod; }
inline int add(const int x,const int y){ return x + y >= mod ? x + y - mod : x + y; }
inline void toadd(int &x,const int y){ x = add(x,y); }
/*
表示从 i->j,走了 k 步的方案数
f[i][j][k] 栈的状态不变
h[i][j][k] 栈中可以留下一系列左括号
g[i][j][k] 栈中 i j 匹配 
我们这里最重要的是不重统计 
g[i][j][k] = g[a][b][k-2] (i->a b->j)
a->l 没有限制 那么我们直接转移
f[i][j][k] += f[l][j][k-1] 
a->l 有/没有限制 
h[i][j][k] += h[l][j][k-1]
即可以在左边加左括号 
枚举 p
f[i][j][k] 
i->l  l->j  
f ,h 左边严格匹配加上一段不严格
for(int p = 1;p<=k;++p){
	int q = k - p;
	toadd(f[i][j][k],mul(g[i][l][p],f[l][j][q]));
	toadd(h[i][j][k],mul(g[i][l][p],h[l][j][q]));
}
*/ 
int main(){
	scanf("%d%d%d\n",&n,&m,&K);
	for(int i = 1;i<=n;++i)
		for(int j = 1;j<=n;++j)a[i][j] = 27;
	for(int i = 1;i<=n;++i)
		h[i][i][0] = f[i][i][0] = 1;
	for(int k = 1;k<=K;++k){
		if(k >= 2){
			for(int i = 1;i<=n;++i)
			for(int l1 = 1;l1 <= n;++l1)if(0 < a[i][l1] && a[i][l1] < 27)
			for(int j = 1;j<=n;++j)
			for(int l2 = 1;l2 <= n;++l2)
				if(a[i][l1] + a[l2][j] == 0)
					toadd(g[i][j][k],f[l1][l2][k-2]);
		}
		for(int i = 1;i<=n;++i)
		for(int j = 1;j<=n;++j)
		for(int l = 1;l<=n;++l){
			if(a[i][l] >= 0 && a[i][l] < 27){
				if(!a[i][l])toadd(f[i][j][k],f[l][j][k-1]);
				toadd(h[i][j][k],h[l][j][k-1]);
			}
			for(int p = 1;p<=k;++p){
				int q = k - p;
				toadd(f[i][j][k],mul(g[i][l][p],f[l][j][q]));
				toadd(h[i][j][k],mul(g[i][l][p],h[l][j][q]));
			}
		}
	}
	int ans = 0;
	for(int i = 1;i<=K;++i)toadd(ans,h[1][n][i]);
	printf("%d",ans);
}
总结:
- 看到快速判断信息相同,可做哈希。
 - 括号序列转前缀和的一个性质:相邻值变化为 \(1\)。
 - 对于有效信息的提取有利于优化 \(dp\)。
 
二项式反演
CF1516E Baby Ehab Plays with Permutations
先转化题意,从数列无序到有序的方案数。
有些题目没有思路是因为对于规模没有把握。对于规模的扩展,我们采用增量法。
又因为与次数,数列长度有关,设计状态 \(f_{i,j}\) 表示至少弄 \(i\) 次,长度为 \(j\) 的数列有多少个。
增量,若加入第 \(j\) 位为 \(j\),那么方案数加上 \(f_{i,j-1}\);不为 \(j\),可以交换。
转移:$f_{i,j} = f_{i,j-1} + (j-1) \cdot f_{i-1,j-1} $。
瓶颈在于 \(n\)。很自然考虑到改变的数最多有 \(2*k\) 个,故将改变了的数的个数计入状态。
\(g_{i,j}\) 表示 表示至少弄 \(i\) 次,长度为 \(j\) 的数列有多少个,其中这 \(j\) 个数都改变。
其对答案的贡献:\(C_{n}^{i} \sum g_{x-2*t,i}\)
那么 \(f\) 与 \(g\) 的关系是:\(f_{x} = \sum_{i=0}^{x} C_{x}^{i} g_{i}\)。
二项式反演,\(g\) 的意义是恰好有 \(i\) 个改变,\(f\) 为至多有 \(x\) 个改变。
所以有 \(g_{i} = \sum_{j=0}^{i} (-1)^{i-j} C_{i}^{j} f_{j}\)。
这里我们发现 \(f\) 的至多意义,利用 \(g\) 的恰好的小规模与 \(ans\) 产生联系,最后使用了二项式反演解决问题。
DAG
DAG上生成树方案数统计
在 DAG 基础上多出一条 \((x,y)\),把有环的挖去,对于一个环,其方案数为 \(\frac{\prod}{\prod_{x\in cir}d_x}\)记录 \(f_u\) 表示 \(u \to y\) 的 \(\prod d\),记忆化搜索即可。
有限空间跳跃理论
转化为若干个独立集之间,删掉前面的独立集,当前独立集入度为 \(0\) 的方案数。
令 \(dp_S\) 表示 \(S\) 点集在 \(DAG\) 上的方案数
\(dp_S \times (-1)^{|T|+1} \to dp_{S ∪ T}\)
每一层之间的转移基于容斥。
对于一次转移,我们的目标是不重不漏地加入 T 这一系列集合,抵消去本质相同但顺序不同的加入操作
容斥系数归纳:
|T| = 1   1
|T| = 2  -1 
	1: -2
	2: 1
|T| = 3   1 
	1: 3
	2: -3
	3: 1
树相关
无标号有根树:LOJ6185 烷基计数
有重复组合数:\(C_{n+m-1}^m\)。(若在 \(n\) 种元素中有重复的选择 \(m\) 个元素的公式)
令 \(i \le j \le p\),
\(k = 1 + i + j + p\)
这样转移时间复杂度为 \(O(n^3)\)。
点我展开看代码
f[0] = 1;
for(int k = 1;k<=n;++k){
  for(int i = 0;i<=k;++i){
  	for(int j = i;j<=k;++j){
	  int p = k - 1 - i - j;
		if(p < j)break;
		  if(i == j && j == p)toadd(f[k],mul(mul(mul(f[i]+2,f[i]+1),f[i]),inv6));
	          else if(i == j)toadd(f[k],mul(f[p],mul(mul(f[i]+1,f[i]),inv2)));
		  else if(j == p)toadd(f[k],mul(f[i],mul(mul(f[j]+1,f[j]),inv2)));
		  else toadd(f[k],mul(f[i],mul(f[j],f[p])));
		}
	}
}
printf("%d",f[n]);
考虑优化。
\(f[i][j]\) 表示子树大小为 \(i\),根节点度数为 \(j\) 的方案数。
与之前直接枚举子树大小的方法不同,我们通过不断地插入子树来实现优化。
\(tot = ∑_{i=0}^{3} f[siz][i]\)
\(f[i][j] = f[i-siz \cdot k][j-k] \times C(tot+k-1,k)\)
点我展开看代码
f[1][0] = 1;
for(int siz = 1;siz<n;++siz){
  int now = 0;
  for(int i = 0;i<4;++i)toadd(now,f[siz][i]);
    for(int i = n;i;--i)
      for(int j = 1;j<4;++j)
      for(int k = 1;k<=j && siz*k<i;++k)
	  toadd(f[i][j],mul(f[i-siz*k][j-k],C(now+k-1,k)));
} 
int ans = 0;
for(int i = 0;i<4;++i)toadd(ans,f[n][i]);
printf("%d",ans);
时间复杂度 \(O(n^2 m)\)。
无标号无根树:BZOJ4271 烷烃计数。
对于无根树的一个套路就是将其重心作为根。
那么同上道题,直接预处理出相同的东西。
但是要限制插入的子树大小 \(\le (n+1)/2\),这样就可以保证了根节点为重心。
- 一个重心:\(∑_{i=0}^{4}f[n][i]\);
 - 两个重心:此时一定是偶数个节点,这里又要用到有重复组合数,即 \(C(sum+2-1,2)\),其中 \(sum = ∑_{i=0}^{3}f[n/2][i]\)。
 
由于数据范围中 \(n \le 500\),大致估一下:组合数 \(500^4\),三维前缀和 \((500^4)^4\),要用高精度。(这里不太清晰,多请指正)。
此处高精度代码省去。
点我展开看代码
#include<bits/stdc++.h>
#define print(a) cerr<<#a"="<<(a)<<endl
using namespace std;
const int N = 510,bas = 1e4;
int n;
NUM C(NUM n,int m){
	NUM d(1);
	int t = m;
	NUM res(1);
	while(t--){
		res = res * n;
		n = n - d;
	}
	for(int i = 1;i<=m;++i)res = res / i;
	return res;
}
void init(){
	f[1][0] = NUM(1);
	for(int siz = 1;siz<=(n-1)/2;++siz){
		NUM now;
		for(int i = 0;i<4;++i)now = now + f[siz][i];
		for(int i = n;i;--i){
			for(int j = 1;j<=4;++j)
				for(int k = 1;k<=j && siz*k<i;++k)
					f[i][j] = f[i][j] + f[i-siz*k][j-k] * C(now+(k-1),k);
		}
	}
}
int main(){
	scanf("%d",&n);
	init();
	NUM ans;
	for(int i = 0;i<=4;++i)ans = ans + f[n][i];
	if(!(n&1)){
		NUM sum(0);
		for(int i = 0;i<=3;++i)sum = sum + f[n/2][i];
		ans = ans + C(sum+1,2);
	}
	ans.output();
	return 0;
}
计数DP
[ABC248F] Keep Connect
题解区已经讲得十分清楚了。套路地搞 dp,将连通载入其中。\(dp_{i,j,0/1}\) 表示前 \(i\) 列,断了 \(j\) 条边,上下是否连通的方案数。这里我们保证所有的点都与第 \(i\) 列其中的 \(1\) 或两个点相连。然后就可以转移了。这里连通是关键信息,顺序转移是统计。
[ARC166C] LU / RD Marking
思考边与边所产生的冲突。模拟可以发现,只有每条在斜方向延伸的多条曲折的边内部才会有冲突。那么大致方向就是算出每个再相乘。考虑怎么计算,由于每条斜的没有本质区别,只是规模上的扩展。考虑由 \(len\) 条边组成,那么由增量法:选最后一条边,那么倒数第二条必选,规模变为 \(len-2\);不选,规模变为 \(len-1\)。\(f_i \gets f_{i-1} + f{i_2}\)。斐波那契数列。
[ABC207E] Mod i
这是一个很简单的 dp。\(f_{i,j}\) 表示前 \(i\) 个分为 \(j\) 段的方案数。\(f_{i,j} = \sum [pre_i \equiv pre_k \pmod j ]f_{k,j-1}\)。对于同余这一特性进行优化。辅助数组 \(g_{k}\) 记录 \(sum_x\) 模 \(j\) 余 \(k\) 的 \(f_{k,j-1}\)。将 \(j\) 作为阶段转移即可。
点我展开看代码
f[0][0] = 1;
for(int j = 1;j<=n;++j){
	memset(g,0,sizeof g);
	for(int i = 1;i<=n;++i){
		toadd(g[a[i-1]%j],f[i-1][j-1]);
		toadd(f[i][j],g[a[i]%j]);
	}
}
for(int j = 1;j<=n;++j)ans = (0ll + ans + f[n][j])%mod;
[ABC215E] Chain Contestant
设计状态 \(f_{i,st}\) 表示前 \(i\) 个位置,已经选了 \(st\) 的字母集合。我们这里强制选 \(i\),这样就避免了再记一维表示当前的末尾颜色。
那么分讨:
- \(a_i = a_j,f_{i,st} \to f_{j,st}\);
 - \(a_i \neq a_j,a_i \in st\) 不合法;
 - \(a_i \neq a_j,a_i \notin st,f_{i,st} \to f_{j,st|(1<<j)}\)。
 
[ABC312G] Avoid Straight Line
关注到三个点的交汇位置,那么不妨枚举这个位置,去统计以该点为中心的答案。
这样就有两个方向,DP 和容斥。
令以枚举的点为根,子树大小为 \(a_1,a_2,a_3,\cdots,a_k\)。
- 容斥
考虑以下几个元素:
\(s1 = ∑a_i,s2 = ∑a_{i}^2,s3 = ∑a_{i}^2\)
我们要的是 \(x \cdot y \cdot z\),我们可以构造 \(s1 \cdot s2 \cdot s3\),并去掉其中的形式 \(x \cdot x \cdot y,x \cdot x \cdot x\) 的到。其中,形式 \(x \cdot x \cdot y\) 可以通过 \(s1 \cdot s2\) 的到。在 \(s1^3 - 3 \cdot s1 \cdot s2\) 后,发现形式 \(x \cdot x \cdot x\) 多减了 \(2\) 次,因此最后答案为 \(s1^3 - 3 \cdot s1 \cdot s2 + 2 \cdot s3\)。 - DP
令 \(f_{i}\) 表示在前 \(i\) 个中选 \(3\) 个的方案数,\(g_{i}\) 表示在前 \(i\) 个中选 \(2\) 个的方案数,二者都强制选 \(i\)。
那么有转移:\(f_i = a_i \cdot \sum_{j = 2}^{i-1}g_{j},g_j = \sum_{j=1}^{i-1}a_i\)。转移即可。 
[ABC] Three Colors
模拟赛的一道题。朴素的 dp 就是 \(dp_{i,j,k}\) 表示染了前 \(i\) 个,红的总和为 \(j\),黄的总和为 \(k\) 的方案数。
优化。由于合法方案很多,不妨计算不合法的方案数,我们记其为 \(ans\)。
观察合法的判定式子:\(a + b > c,a + b + c = presum_i\),因此可以只记一个 \(a+b\)。
\(dp_{i,j}\) 表示前 \(i\) 个,小的两个结果之和为 \(j\) 的方案数。最后统计时只需要将 \(j \le \lfloor sum/2 \rfloor\) 的 \(f_{n,j}\) 加到 \(ans\) 中去。
这里只统计了三种颜色之间的相对关系,所以最后计算时要乘以 \(3\)。
有转移 \(dp_{i,j} \gets dp_{i-1,j} + dp_{i-1,j-a_i} \times 2\)。通过限定 \(j\) 的上界为 \(\lfloor sum/2 \rfloor\) 可以实现统计。
但是我们发现当 \(sum\) 为偶数时会重复统计,即 \(a + 0 = c,a = c = \lfloor sum/2 \rfloor\) 的情况。
令 \(g_{i,j}\) 表示 \(c = \lfloor sum/2 \rfloor\) 时,\(a = j\) 的方案数,这样转移过程就是模拟只选 \(a\) 或 \(c\) 的过程。
\(g_{i,j} = g_{i-1,j-1} + g_{i-1,j-a_i}\)。从不合法方案总数 \(ans\) 中减去 \(g_{\lfloor sum/2 \rfloor}\) 即可。
最后用全集 \(3^n\) 减去 \(ans \times 3\)。
对于解决方案中的 \(f\),\(g\) 的转移,可以删去一维倒序地类似 01 背包一样转移,空间更优秀。
点我展开看代码
inline int mul(int x,int y){ return 1ll * x * y % mod; }
inline int fpow(int a,int k){
	int res = 1;
	while(k){
		if(k & 1)res = mul(res,a);
		a = mul(a,a);
		k >>= 1;
	}
	return res;
}
inline int add(int x,int y){ return x + y >= mod ? x + y - mod : x + y; }
inline int sub(int x,int y){ return x - y < 0 ? x - y + mod : x - y; }
inline void toadd(int &x,int y){ x = add(x,y); }
int n,a[N],tot,f[N],g[N],ans,mid;
int main(){
	scanf("%d",&n);
	for(int i = 1;i<=n;++i)scanf("%d",&a[i]),tot += a[i];
	mid = (tot >> 1);
	f[0] = 1;
	for(int i = 1;i<=n;++i)
		for(int j = mid;j >= a[i];--j)
			toadd(f[j],mul(2,f[j-a[i]]));
	for(int i = 0;i<=mid;++i)toadd(ans,f[i]);
	if(!(tot&1)){
		g[0] = 1;
		for(int i = 1;i<=n;++i)
			for(int j = tot;j>=a[i];--j)
				toadd(g[j],g[j-a[i]]);
		ans = sub(ans,g[mid]);
	}
	printf("%d",sub(fpow(3,n),mul(3,ans)));
	return 0;
}
CF979E Kuro and Topological Parity
题意转化:将连边,设点的操作放在序列上。这样使得一切都变得有条理了。
名确目标:统计合法路径条数为奇数/偶数的方案数。那么对于类似序列的问题,增量法是一个优秀的选择。
我平时处理复杂问题可能会忽视这种方法,因为其规模之间只有 \(1\) 的差距,将思想集中于此时问题可以解决。
提取出核心的信息:合法路径条数的奇偶性。
剩下的就是转移了。
铅粉
题意简述:给定数列的长度 \(n\),值域为 \([1,k]\),要求对于所有位置都可以被一个长度为 \(k\) 的排列子串包含,求这样的数列个数 \(\mod 998244353\)。
一开始就想到了正确的状态,\(f_i\) 表示 \([i-k+1,i]\) 为合法排列,且 \([1,i]\) 满足要求的方案数。后来的一个误区就是在做转移的时候,不应该是在 \(f_j\) 中进行取舍,而是应该通过减少放的一些方式使得不会重复和遗漏。所以就有容斥系数 \(g_i\) 表示放 \(i\) 个进行转移时的系数。\(g_i = i! - \sum g_j \times (i-j)!\)。
点我展开看代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 998244353;
int n,k,fac[1010];
int f[100010],g[100010];
inline int mul(int x,int y){ return 1ll * x * y % mod; }
inline int add(int x,int y){ return x + y >= mod ? x + y - mod : x + y; }
inline void toadd(int &x,int y){ x = add(x,y); }
int main(){
	freopen("Pb.in","r",stdin);
	freopen("Pb.out","w",stdout);
	scanf("%d%d",&n,&k);
	fac[1] = 1;
	for(int i = 2;i<=k;++i)fac[i] = mul(fac[i-1],i);
	for(int i = 1;i<=k;++i){
		g[i] = fac[i];
		for(int j = 1;j<i;++j)g[i] = (g[i] + mod - mul(fac[i-j],g[j])) % mod;
	}
	f[k] = fac[k];
	for(int i = k+1;i<=n;++i)
		for(int j = 1;j<=k;++j)
			toadd(f[i],mul(f[i-j],g[j]));
	printf("%d",f[n]);
	return 0;
}
luogu。
问题描述:有 \(k\) 种颜色对一棵树的所有边进行染色,给定 \(m\) 条限制,每条限制要求 \(u,v\) 路径上的所有边至少有两种颜色,问染色的方案总数。
注意到数据范围:\(m \le 15\),明显的一个经典容斥。如何求钦定一些路径颜色全部相同的方案数?对于要求颜色相同的边用并查集并起来,剩下的联通块个数的个数为 \(cnt\),那么这次的贡献就是 \(k^{cnt}\)。
想到的过程:考试的时候有误区:路径上的边和路径外的边分开计算。这是不对的,因为里面在容斥只会把不同的边独立出来,进而限制了我们的思想空间,并没有意识到整体而言问题会变的很简单。
n 个有标号节点无向连通图计数
\(f_i\) 表示大小为 \(i\) 的答案。反面计数求非联通的,枚举 \(1\) 所在的连通块大小,\(f_i = 2^{\frac{i \times (i-1)}{2}} - \sum_{j=0}^{i-1}f[j] \times C_{i-1}^{j-1} 2^{\frac{(i-j-1)\times(i-j)}{2}}\)。
                    
                
                
            
        
浙公网安备 33010602011771号