计数专题
P9743 「KDOI-06-J」旅行
先写出来一个 \(O(n^7)\) 计数方程
复杂度过高,考虑容斥,首先不要被 \(x,y\) 所迷惑,它们是定值,而变量为 \(ca,cb\) 发现连续区间可以进行二维前缀和。写不下了,第一个式子略去了下标\((x,y)\)
还是有点小细节的,比如 \(c\) 的变化。其实就是让变量 \(la,lb\) 发生一个偏移 "\(1\)" 同时不要忘记让其他维度也发生相应变化
P8502 「CGOI-2」No cost too great
设出 \(f_{i,j,k}\) 表示从 \(i\) 到 \(j\) 经过 \(k\) 条边的方案数,列出方程
如何快速求和?观察到一个点所到达的点序号为一个区间,故可用刷表法,并用差分代替树状数组,以此去掉一个 \(\log\) 的复杂度。
至于约束条件可以运用正难则反,去掉经过 \(c\) 的情况。
注意 \(f_{i,j,k}\) 并不代表只经过一次 \(j\),因为可能从 \(j\) 出发又回到 \(j\) 再去 \(k\)。如果去掉经过 \(c\) 的情况用 \(f\) 减去 \(f\) 相乘求和这样子算的话肯定会有重复。一般解决办法是选取一个基准点,这里选择最后一次经过 \(c\) 的状态为基准点,只要最后一次经过 \(c\) 的时候状态不一样那么总的状态就不一样,这样就不会减去重复的了。此时做法都是再开一个辅助数组,设 \(g_{i,j,k}\) 约束其为最后一次经过 \(j\) 的方案数,而且一般这种辅助数组的递推要用到原数组,本题中即为在 \(f\) 的基础上将 \(g_{i,j,j}\) 设置为 \(0\) 即可。
于是在计算 \(f\) 数组的基础上修改一次即可,得到
\(f_{i,j,k}-= \sum\limits_{i=1}^k f_{i,c,i} \times g_{c,j,k-i}\)
P9745 「KDOI-06-S」树上异或
先考虑链部分分以此来引导至正解。这就是一个序列上的问题了。
设 \(f_i\) 为前 \(i\) 个数的答案,\(s_i= \bigotimes_{i=1}^n a_i\)。显然 \(f_{i}=\sum\limits_{j=0}^{i-1}(s_{i}\bigotimes s_{j}) \times f_{j}\),此时计算复杂度为 \((n^2)\)。
发现这是一个类似前缀和的东西,但是因为要异或所以不可直接前缀和统计。于是我们可以对二进制的每一位进行前缀和。设 \(g_{i,j,k}\) 为前 \(i\) 位中,满足 \(s\) 的第 \(j\) 位为 \(k\) 的 \(f\) 的前缀和。代码如下
点击查看代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=5e5+10;
const int maxlog=64;
const int mod=998244353;
long long g[maxn][maxlog][2],f[maxn];
long long a[maxn],s[maxn],pow[maxlog];
int main(){
int n; cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) s[i]=s[i-1]^a[i];
for(int i=1;i<=n;i++) f[i]=s[i];
pow[0]=1;
for(int i=1;i<maxlog;i++) pow[i]=(pow[i-1]*2)%mod;
for(int i=1;i<=n;i++){
for(int j=0;j<maxlog;j++){
int x=(s[i]>>j)&1; x^=1;
f[i]=(f[i]+g[i-1][j][x]*pow[j]%mod)%mod;
}
for(int j=0;j<maxlog;j++){
int x=(s[i]>>j)&1;
g[i][j][x]=(g[i-1][j][x]+f[i])%mod;
g[i][j][x^1]=g[i-1][j][x^1];
}
}
cout<<f[n];
return 0;
}
现在考虑正解
AT_arc144_d [ARC144D] AND OR Equation
看到题目奇怪的二进制式子,第一眼似乎要用二进制运算找规律?其实不用,可以思考式子的实际意义,即为 $ A_{i}+A_{j}-A_{i \cap j}=A_{i \cup j}$。这样就是一个集合合并的式子啦。
我们可以构造 \(p_{0 \sim i-1}\),令 \(A_S =\sum\limits_{i \in S}p_i\),考虑还有哪些式子符合给出一个偏移量则 \(a'_S=a_S+c\)。
此时可以开始计数,利用题目约束条件可得,\(\sum\limits_{p_i>0} p_i+c \le k\) 并且 \(\sum\limits_{p_i<0}p_i+c \ge 0\)。一个很显然的想法就是枚举 \(c\) 然后统计方案。但是我们会发现枚举完 \(c\) 之后是一个求和式子小于一个数不太好处理,需要枚举一个可能值再计算这样就麻烦了。那么我们可以转换一下思路,直接枚举求和式,然后 \(c\) 单变量的方案数就很显然了。
于是直接枚举正数之和,负数之和以及为 \(0\) 的情况,列出 \(c\) 的范围是 $ [ -\sum\limits_{p_i<0}p_i,k-\sum\limits_{p_i>0}p_i]$。发现区间长度正好就是 \(k+1-\sum\limits \lvert p_i \rvert\),这样又可以简化了,我们只需要枚举绝对值之和,不需要分开枚举了!
答案就是
线性求解即可。
P9753 [CSP-S 2023] 消消乐
哈希
对于每个左端点放一个栈匹配,可以做到 \(O(n^2)\) ,部分分的串是随机的,我们可以发现随机情况下合法串应该很短,对每个左端点起扫很少一段距离就行了。还是减少重复计算的思想,于是直接从头建立一个栈一直扫到尾部,然后可以发现这题满足可减性即 \([l,r]=[1,r]-[1,l-1]\)。哈希维护相同的前缀栈就行了。
dp
计数可以往 \(dp\) 方向想,\(dp_i\) 表示以 \(i\) 结尾的方案数,显然 $dp_i = dp_{j-1}+1 $。 \(j\) 是 \(i\) 的上一个匹配点,此时还是 \(O(n^2)\) 瓶颈在于如何快速找到上一个匹配点 \(j\) 也就是 \(j\) 为满足 \([j,i]\) 为合法串的最大一个。这和 KMP 很像,也可以利用类似的思路递推 \(Next\) 数组就行了。我们来分析一下复杂度,对于一种字母最多跳到某一位置一次,假设 \(i<j\),\(s_i=s_j\),如果 \(i\) 的时候枚举到了那个位置,\(j\) 的时候如果还可以枚举到那个位置的话显然就与 \(Next\) 数组定义中的最近相矛盾,因为此时 \([i,j]\) 已经可以构成一个合法区间,故复杂度为 \(O(n \lvert \sum \rvert)\)。
发现构成串的字符种类很少,可以空间换时间。做到空间 \(O(n \lvert S \rvert)\),但是时间线性。每个位置新增一个字符所以只需要一次修改,记录一次快速匹配,具体操作可以见题解。
UVA580 危险的组合 Critical Mass
常见解决方法就是利用数学组合数推出一个具体的式子。但是这条路走不通,可以考虑列出递推公式然后求解。设 \(f_i\) 为前 \(i\) 个盒子满足方案,然后很显然的就是要分类讨论 前\(i-1\) 个盒子满足了,或者到了第 \(i\) 个才刚刚满足。前者为 \(2f_{i-1}\) ,后者需要前 \(i-4\) 个不满足,正难则反一下就是 \(2^{i-4}-f_{i-4}\)。
P3978 [TJOI2015] 概率论
很常规的想法,就是通过 \(i-1\) 的来新增一个节点推导 \(i\) 的答案,看放在哪里可以产生贡献,发现只有在叶子节点才会产生贡献。如果不会算可以打表猜规律。我的想法是从树中提取出一个单点如果它没有儿子那么可以左右挂两个,如果它有一个儿子,可以挂一个,通过另一边必定会多出一个叶子节点,叶子节点一个点可以挂两个,如果它有两个儿子同理,可得 \(n-1\) 的二叉树可以挂 \(n\) 个叶子节点。设 \(f_i\) 为 \(i\) 个节点的二叉树个数,\(g_i\) 为所有 \(i\) 个节点的二叉树的节点总数。可得 \(g_n=n \times f_{n-1}\) 。问题在于计算 \(f\) , 利用计数思想找到一个基准点,只需要两个数的左子树不同那么这个树就不同,于是有 \(f_i=\sum\limits_{i=1}^{n-1}f_i \times f_{n-i-1}\) ,积累一下此时的 \(f\) 为卡特兰数列,或者你可以通过看前几项 \(1\) \(2\) \(5\) \(14\) \(42\) 猜出来。通项公式为 \(Cat_n= \frac{C_{2n}^n}{n+1}\)。
AT_abc158_f [ABC158F] Removing Robots
先找计数基准点,显然为左边第一个激活的机器人。设 \(f_i\) 为必须激活左边的第 \(i\) 个机器人的方案数,则有
记 \(g_i=\sum\limits_{j=i}^{n+1}f_j\),则有 \(f_i=g_j\)。答案即为 \(g_1\)。
现在来解决 \(k(i)\) 如何求,\(k(i)\) 表示 \(i\) 右边第一个不受影响的机器人。大概是这么一个过程:在机器人 \(i\) 的覆盖范围内找到一个覆盖范围最广的点 \(z\)。由于该点的覆盖范围此前已经求出,于是 \(k(z) \to k(i)\)。
这个过程可以通过线段树完成,但是我们也可以发现本质是维护一系列随着位置单调递增,覆盖范围也单调递增的点。于是单调栈就可以了。
AT_arc074_c [ARC074E] RGB Sequence
这一种有约束的 \(dp\) 计数题目,设计的状态里面必须有约束条件。比如这题是要求区间颜色种数,发现颜色数很少,于是直接设 \(f_{i,j,k}\) 表示上一个与 \(a_i\) 不同的是 \(a_j\), 再上一个与 \(a_i\) 和 \(a_j\) 不同的是 \(a_k\)。这样就可以快速判断区间颜色种数。
CF1332F Independent Set
如果枚举每一个子图计算的话,必然会重复计算信息,导致复杂度增大。假设 \(G' \subset G\),我们可以发现对于 \(G'\) 的计算和 \(G\) 中 \(G'\) 的计算除了子图 \(G'\) 的根节点外其余都相同。于是我们设出状态 \(f_{i,0/1/2}\) 分别表示 \(i\) 作为子图根节点的时候的答案,与父节点联通时 \(i\) 点的不选或者选。于是有
注意蓝笔部分,因为题目中子图是通过边集定义的,而题目要求边集不为空,所以子图就不能由一个点构成。
AGC008F Black Radius
本题的计数方式并不是以具体的点集来计数,而是基于 \((u,d)\) 的计数,可是可能会有重复。于是我们希望对于任意一个点集 \(T\) 用最小的 \(d\) 来刻画。显然除了全集之外,任意一个集合都可以通过最小的 \(d\) 来唯一确定中心点 \(u\)。(先不考虑全集,最后加上即可)。
首先思考给出集合 \(S\) 为全集的时候,对于一个点 \(u\) 如何才能产生贡献。设 \(f_u\) 表示 \(u\) 的最深子树的深度,\(g_u\) 为 \(u\) 的次深子树的深度。首先 \(d \le f_u-1\),其次为了防止 \(u\) 周围的点 \(v\) 用 \(d-1\) 的深度覆盖了 \(u\) 的深度 \(d\)(此时 \(v\) 在 \(u\) 的最深子树中,且其他子树的深度被完全覆盖)。那么需要满足 \(d \le g_u+1\),于是 \(S\) 为全集的时候,\(d\) 需要满足 \(d \in [0,\min(f_u-1,g_u+1)]\)。
考虑如何扩展到子集,为了和之前的形式统一,我们依然可以将贡献产生放在 \(d\) 最小的点上,哪怕他不是关键点。于是有,对于关键点 \(d\) 要求同上,对于非关键点至少要覆盖到一个关键点,于是 \(d\) 的下界是深度最低的关键点子树深度 \(h(u)\)。
CF1943D2 Counting Is Fun
首先一个小结论,一个序列可以通过长度大于 \(1\) 的区间减变成全 \(0\),当且仅当对于任意位置 \(a_{i+1}+a_{i-1}\ge a_i\)。
于是我们可以设 \(dp_{i,j,t}\) 表示第 \(i\) 个位置是 \(j\),前一个位置是 \(t\) 的时候的方案数。
于是我们可以 \(dp_{i,j,t} \to dp_{i+1,z,j}\),这个时候是 \(O(n^3)\)。
注意一下这种 \(dp\) 的形式,一般这种有状态重叠的 dp(没有变化的 \(j\) ),我们可以考虑直接去掉一个维度。
设 \(dp_{i,j}\) 表示第 \(i\) 个位置为 \(j\) 的总方案数。那么下面解决的就是如何消除去掉的那个维度的影响。
首先是 \(dp_{i+1,t} \gets \sum\limits_{j=0}^k dp_{i,j}\),后面可以用到容斥。
也就是减去该位置不合法的方案数,就是每个 \(dp_{i,j}\) 对应的使得该状态不合法的 \(dp_{i-1,s}\),这里注意我们的对象使得 \((i,j)\) 不合法,于是只能是 \(s+t<j\),而不能是 \(j+t<s\) 这种情况。同时还有一个性质就是不可能出现相邻两个位置不合法,这也就为我们使用 \(dp_{i-1,s}\) 提供了保障。
我们不可能对于每个 \((i,j)\) 都累加 \((i-1,s)\) 然后求和,这样复杂度太高了,于是直接统计每个 \((i-1,s)\) 会出现几次直接减去 \(dp\) 值乘以对应个数即可。
于是 \(dp_{i,t}=\sum\limits_{j=0}^k dp_{i,j}-\sum\limits_{s=0}^k dp_{i-1,s} \times\max(0,k-t-s)\)
ABC134F Permutation Oddness
看似需要用一个 \(S\) 表示目前用了哪些,十分不可做。实际上这种问题的状态只需要和题目中要求的量有关即可,或者说可以计算出那个就行了,也就是说只要当前的状态可计算就行了,状态里面维护那些产生能贡献的量即可。
考虑贡献法,此时每个位置可以拆成两个 \(i\) 和 \(p_i\),两部分都可以产生贡献。
我们设 \(f_{i,j,k}\) 表示前 \(i\) 个位置里未匹配的位置有 \(j\) 个,目前总和为 \(k\) 的方案数,这里注意一个小细节很重要就是未匹配的 \(i\) 和 \(p_i\) 数量应该相同,所以我们可以这么设置状态以减少维度。
向自己匹配:\(f_{i-1,j,k} \to f_{i,j,k}\)
与后面数匹配:\(f_{i-1,j-1,k+2i} \to f_{i,j, k}\)
与前面的数匹配:\(f_{i-1,j+1,k-2i}\times (j+1)^2 \to f_{i,j,k}\)
一前一后:\(f_{i-1,j,k} \times 2j \to f_{i,j,k}\)
一般图边覆盖计数
meet-in-the-middle
考虑将图分为两部分,对于每一部分设 \(f(s)\) 表示对于点集中的点选若干条边完成点覆盖的方案数,每次加入一个点的时候考虑包含这个点的边,如果边集内部已经有了这条边的另一个点,那么说明之前没有选该边,这次也不考虑。由于是计数我们每次加入当前任意一个端点不在边集中的最小编号边即可。然后只需要枚举两部分之间的边暴力算即可。对于两部分的划分,我们可以多随机几组然后选择时间最优的组合来计算。
更好的做法
ABC252G Pre-Order
先序遍历就是 dfs 序,dfs序的子树就是一段区间,所以我们考虑区间 dp。设 \(dp_{i,j}\) 表示 \([i,j]\) 这一段的方案数。很套路的就得到类似于 \(dp_{l,k} \times dp_{k+1,r} \to dp_{l,r}\) 这样子的方程,但是注意一下在计数 dp 里面一定要小心要不重不漏,也就说上述区间划分会造成多统计的,比如区间 \([1,2]\),\([3,4]\) 和 \([5,6]\) 可以通过 \(dp_{1,4} \times dp_{5,6}\) 产生,也可以通过 \(dp_{1,2} \times dp_{3,6}\) 产生。
解决方法就是钦定第一个划分出来的区间不同即可,于是我们就要求划分出来的左区间单独形成一个子树,不能再被划分为多个子树。这里不需要再开辅助数组 \(f\) 来单独计数。可以直接通过钦定区间最左边的一个数为根解决,这样子当 \([l,r]\) 进行划分的时候最先划出来的就是 \([l+1,k]\) 其中由于 \(l+1\) 为根,所以第一个划分出来的区间只有一个子树,而为了让后面划分出来的右区间可以有形成多个区间的选择,我们这里可以创建一个虚根 \(k\),使得有右区间 \([k+1,r] \to [k,r]\),这样子有了虚根之后就在虚根 \(k\) 下面划分出多个子树了。
于是我们可以列出转移方程 \(dp_{l,r}=\sum\limits_{k}dp_{l+1,k} \times dp_{k,r}\)。转移条件是 \(a_{l+1}<a_{k+1}\) 或 \(k=r\),为了满足先遍历小编号儿子的要求。
ZROI2835.罗生门
不要被题目形式所迷糊,冷静分析一下,或者写个程序输出一下发现是 \(a_i\) 行 \(i\) 的二进制表示拼接在一起。如果是 \(n \times n\) 的积和式的话状压是可以解决的,但是这个做法在本题没有前途。
同样可以考虑容斥,式子为 \(\sum\limits_{s}(-1)^{\lvert s\rvert}\sum\limits_{i}\prod\limits_{j\notin}A_{i,j}\)
在本题做法也是容斥原理,\(\sum\limits_{s}(-1)^{n-\lvert s\rvert}\sum\limits_{选 n 行} \prod\limits_{j \in s}A_{i,j}\)。对于每一行如果对应的二进制数为 \(T\),那一行可能产生权值的位置个数为 \(\lvert T~and~S\rvert\)。设可能位置个数为 \(i\) 的出现了 \(c_i\) 次。那么给定 \(S\) 如何求 \(\sum\limits_{T_i\operatorname{and} S}a_i\)。我们设 \(dp_{i,j,s}\) 表示考虑了 \([0,i-1]\) 位,当前有 \(j\) 个 \(1\),\(s\) 是由 $[i,n] $ 为原数,\([1,i-1]\) 位为和 \(S\) 按位与之后的结果,的 \(\sum a\)。初始值是 \(dp_{0,0,s}=a_s\),每次不断缩短 \(s\) 即可。
某一行每一行可以选择的有 \(i\) 个,乘法原理即可。注意这个容斥的时候由于一共有 \(n\) 行我们却只选了某些列,所以允许列重复出现。用生成函数解决一下就是
直接生成函数部分二项式定理计算为 \(O(2^nn^3)\),其中容斥是 \(O(2^n)\),计算是 \(O(n^3)\)。
通过先取 ln 再反过来 exp 可以做到 \(O(2^nn^2)\) 来计算。