Combinatorics 第一弹
学习一些经典老题,锻炼基本功。
1. P5367 【模板】康托展开
拿到一个排列,求其排名就是数有多少个排列比它小。
枚举一个 \(i\),设 \(f_i\) 表示钦定前 \(i-1\) 位相同,第 \(i\) 位比它小,后面随便填的方案数,显然这样不会数重,答案就是 \(\sum\limits_{1\le i\le n}f_i\)。
设 \(sum_i\) 表示钦定前 \(i-1\) 位相同后,剩下有多少个数字比第 \(i\) 位上的数要小。那么 \(f_i=sum_i(n-i)!\)。\(sum_i\) 可以拿树状数组算。
由于是排名,最后要加一。
\(O(n\log n)\)。
2. P8594 「KDOI-02」一个仇的复
首先发现可以旋转其实是假的,只有 \(1\times 2\) 的骨牌才可以转。
考虑没有竖着的怎么做,将 \(k\) 个分配给两行,一行内部用 \(i\) 个骨牌铺满的方案数可以直接插板,为 \(\dbinom{n-1}{i-1}\)。于是合在一起就是 \(\sum\limits_{i=0}^k\dbinom{n-1}{i-1}\dbinom{n-1}{k-i-1}=\dbinom{2n-2}{k-2}\)。
那么有竖着的怎么办?不妨设有 \(i\) 个竖着的,将整个序列分成 \(j\) 块。我们关注每一块的长度,设 \(a_{1,2,\cdots ,j}\) 表示这个东西。考虑每一块前后有几个竖着的,设 \(d_{0,1,2,\cdots,j}\) 表示这个东西,它要满足:\(d_0\ge 0\) 且 \(d_j\ge 0\) 且 \(\forall t\in (0,j),d_t\ge 1\) 且 \(\sum\limits_{0\le t\le j} d_t=i\)。可以插板法,得到合法的 \(d\) 有 \(\dbinom{i+1}{j}\) 组。
然后考虑将剩下的 \(k-i\) 块骨牌分配到 \(j\) 块中,不妨设 \(b_{1,2,\cdots ,j}\) 表示每个块分配到的骨牌数量。现在要对每一块算答案然后乘起来,那其实是这样的一个式子:
如何化简?考虑 GF,\(\dbinom{2a_l-2}{b_l-2}=[z^{b_l-2}](1+z)^{2a_l-2}\),所以后面那一坨其实是一个多重卷积,于是原式就是 \(\sum\limits_{\sum\limits_{1\le t\le j}a_t=n-i}[z^{\sum\limits_{1\le t\le j}b_t-2}](1+z)^{\sum\limits_{1\le t\le j}2a_t-2}=\sum\limits_{\sum\limits_{1\le t\le j}a_t}\dbinom{2n-2i-2j}{k-i-2j}\)。那么只用考虑分配 \(a_t\) 有多少种情况。不难使用插板法得到有 \(\dbinom{n-i-1}{j-1}\) 种。
综上,答案就是 \(\sum\limits_{i=0}^k\sum\limits_{j=0}^{i+1}\dbinom{i+1}{j}\dbinom{2n-2i-2j}{k-i-2j}\dbinom{n-i-1}{j-1}\)。
真的吗?其实少了一点。考虑 \(n=k\) 时,式子中 \(i=k,j=0\) 处其实没有值,因为 \(\dbinom{-1}{-1}=0\)。但是这个东西本来应该算一种。所以上面还要加上 \([n=k]\)。
综上,答案就是 \([n=k]+\sum\limits_{i=0}^k\sum\limits_{j=0}^{i+1}\dbinom{i+1}{j}\dbinom{2n-2i-2j}{k-i-2j}\dbinom{n-i-1}{j-1}\)。
\(O(n+k^2)\)。
3. P5376 [THUPC 2019] 过河卒二
计算的是走出棋盘的方案数,不太好。考虑在题中给出的移动方式下,走出棋盘后仅有一种方式走到 \((N+1,M+1)\),走到 \((N+1,M+1)\) 的一种方案又唯一对应着一种走出棋盘的方式。故只需计算走到 \((N+1,M+1)\) 的方案数。
但是又因为从 \((1,1)\) 出发,所以可以等价于从 \((0,0)\) 走到 \((N,M)\)。
先不考虑障碍。考虑求出从 \((0,0)\) 走到 \((n,m)\) 的方案数。
卒可以向右上方走,这不好。先枚举有 \(i\) 步向右上方走,剩下的只能向右或向上走。那么现在一共会走 \(n-i+m-i+i=n+m-i\) 步,考虑将这 \(i\) 步放进去,剩下的再经典格路计数,写出式子,\(\sum\limits_{i\ge 0}\dbinom{n+m-i}{i}\dbinom{n+m-2i}{n-i}\)。
然后现在考虑障碍,显然要容斥。设 \(g_S\) 表示钦定经过 \(S\) 中的障碍的方案数,那么答案即 \(\sum\limits_{S}(-1)^{|S|}g_S\)。这个可以考虑设 \(f_S\) 表示恰好经过 \(S\) 中的障碍的方案数,所求即为 \(f_{\emptyset}\),然后子集反演得到。或者考虑经典容斥。
然后就可以将障碍排序,然后预处理任意两个障碍之间的方案数,做容斥就好了。组合数可以用 Lucas 定理算。
\(O(k^2\min(n,m)+2^kk)\)。
4. P3270[JLOI2016] 成绩比较
看到恰好 \(k\) 人被碾压,直接上二项式反演,转化到钦定 \(k\) 人被碾压。
设 \(f_i\) 表示钦定 \(i\) 个人被碾压的方案数,\(g_i\) 表示恰好 \(i\) 个人被碾压的方案数。写一下式子,\(f_k=\dbinom{n-1}{k}\prod\limits_{0\le i<m}\dbinom{n-k-1}{n-R_i-k}\)。那么 \(g_k=\sum\limits_{i=k}^n (-1)^{i-k} \dbinom{i}{k} \dbinom{n-1}{i}\prod\limits_{0\le j<m}\dbinom{n-i-1}{n-R_j-i}\)。
现在还要算给每个人分配分数的方案数。可以枚举中间那个人的分数 \(x\),然后方案数就是 \(\prod\limits_{0\le j<m}\sum\limits_{1\le x\le U_j} x^{n-R_j}(U_i-x)^{R_j-1}\)。把这个东西乘到 \(g_k\) 的后面就得到了答案:
但是这样你发现时间复杂度好像有点问题,\(O(n^2V\log n)\) 过不了。继续化简式子,用二项式定理拆最后那一个求和号后面的式子:
最后的一坨是连续自然数幂和,可以拉插 \(O(n)\) 算一次。
视 \(n,m\) 同阶,分析一下复杂度。那一坨连续自然数幂和只有 \(O(n^2)\) 种不同的值,可以 \(O(n^3)\) 预处理。然后整个式子 \(O(n^3)\) 算即可。
所以是 \(O(n^3)\) 的。
5. P3773 [CTSC2017] 吉夫特
发现 \(\text{原式} \bmod 2>0\),那么 \(\text{原式} \bmod 2=1\)。看到一堆组合数取模,直接 Lucas 定理拆开。那么拆出来的每一项都是 \(\dbinom{1}{1}\) 或者 \(\dbinom{1}{0}\) 或者 \(\dbinom{0}{1}\) 或者 \(\dbinom{0}{0}\)。又要结果不为 \(0\),所以不能出现 \(\dbinom{0}{1}\),即在二进制下,子序列的后一项包含于前一项。
那不妨设 \(f_x\) 表示以数 \(x\) 结尾的长度大于等于 \(2\) 的子序列数量。每往序列中加入一个数 \(x\),就可以直接将 \(f_x\) 累加到答案中,然后用 \(x\) 更新其他的数的答案。
即枚举 \(s\subset x\),\(f_s\gets f_s+f_x+1\)。
\(O(3^{\log \max a_i})\)。
6. P6846 [CEOI 2019] Amusement Park
首先可以发现对于一种可行的方案,将其中的边全部反向可以得到另一种方案。这两种方案相互对应,并且反向的边数之和必为 \(m\)。设将一些边反向得到 DAG 的方案数为 \(x\),那么答案就是 \(\dfrac{x\cdot m}{2}\)。
设 \(f_S\) 表示点集为 \(S\) 的导出子图将边定向后得到 DAG 的方案数。考虑 DAG 上拓扑排序的过程,每次取出一些零度点,剩下的部分仍是 DAG,这是一个子结构。注意取出的点必定是独立集。
但是不能直接这样转移,因为会算重。一个有 \(c\) 个零度点的 DAG 会被算重 \(2^c-1\) 次。于是需要容斥,给一个钦定 \(i\) 个点作为零度点的方案,设置容斥系数为 \((-1)^{i+1}\),因为这样对于原本会被算重的有 \(c\) 个零度点的 DAG,有 \(\sum\limits_{i=1}^c\dbinom{c}{i}(-1)^{i+1}=1-\sum\limits_{i=0}^c\dbinom{c}{i}(-1)^i=1\),这样是算对了的。
设 \(con_S\) 表示点集 \(S\) 是否为一个独立集,于是有转移 \(f_S=\sum\limits_{T\subset S}(-1)^{|T|+1}con_Tf_{S \setminus T}\)。
这样就做完了。时间复杂度 \(O(3^n+2^nm)\)。可以子集卷积优化至 \(O(n^22^n)\)。
7. P11714 [清华集训 2014] 主旋律
强连通难以刻画,不妨单步容斥,转化为所有方案减去删掉边集的一个子集后不强连通的方案。先设 \(h_S\) 表示删去 \(S\) 的导出子图的边集的一个子集使之强连通的方案数。
不强连通的图,必然可以 SCC 缩点成 DAG,于是还是 DAG 容斥,套路地设 \(f_S\) 表示将 \(S\) 中的点划分为若干个无交的 SCC 的方案数。但是由于 DAG 容斥经典结论,容斥系数与 SCC 个数有关,设 SCC 个数为 \(x\),那么容斥系数为 \((-1)^{x+1}\),但是这里看不出来 \(x\)。所以修改定义,设 \(g_S\) 表示将 \(S\) 中的点划分为偶数个 SCC 的方案数减去划分为奇数个 SCC 的方案数。这里的 \(g\) 就含有容斥系数了。
考虑转移 \(h\),若钦定 \(T\) 为入度为 \(0\) 的 SCC 的点集的并集,那么
8. P8329 [ZJOI2022] 树
9. P6277 [USACO20OPEN] Circus P
显然不论这些牛的初始位置在哪里,我们都可以将它们移动到某特定的 \(k\)个位置上,然后计数分配标号的方案数。不妨认为将这些牛移动到编号为 \(1,2,\cdots,k\) 的点上。
牛在树上的移动会受到其他牛的限制,有时会被堵死,有时可以互换位置。所以我们只关心某两个位置上的两头牛是否能交换位置。
如果位置 \((x,y)\) 上的牛可以交换位置且不改变其它牛的位置或者可以将其它牛恢复到原位,则称这两个位置有交换关系。显然这种关系具有传递性。
我们将这种关系视作一张图上连接 \((x,y)\) 的一条边,那么由于传递性,最终会形成若干个团。不妨对将牛排列在 \(1,2,\cdots,k\) 位置的标号排列计数。设第 \(i\) 个团的大小为 \(s_i\),那么答案就是 \(\dfrac{k!}{\prod\limits_i (s_i!)}\)。这些 \(s_i\) 都是在 \(k\) 头牛限制下得到的,可以说明 \(i\le k\)。
现在来刻画这种交换关系在树上表现出的形态。感性上,两个位置想要交换,必然需要一个空位置,来让其中一头牛在上面等待另一头牛移动。
这告诉我们,一条不分叉的路径上,两个位置不能交换。以下定义链为两端的点度数不为 \(2\),中间的每个点度数都为 \(2\) 的一条路径。
不妨将一条链拎起来,称左端点及其下方部分为左子树,设其大小为 \(A\),右端点同理,设其大小为 \(B\),设链长(含两个端点的点数)为 \(C\)。显然有等式 \((A-1)+(B-1)+C=n\)。
考虑在 \(k\) 的限制下,何时可以交换左右子树中的一个点。
可以断言:当且仅当 \(k<(A-1)+(B-1)=n-C\) 时,左右子树中的点可以任意交换。
必要性是显然的,若 \(k>n-C\),那么链上至少会有 \(k-n+C\) 头牛无法交换位置,故左右子树中的点无法交换。若 \(k=n-C\),容易验证与前一种情况类似。
充分性可以构造得证。感性理解就是 \(k<n-C\) 时,将 \(k\) 头牛扔进左右子树后仍有空位,可以利用这些空位完成交换。
以下称一条链是非法的,如果 \(C\ge n-k\)。
考虑对于固定的 \(k\) 算一次答案。我们将 \(C\ge n-k\) 的非法链从原图中删去,那么剩下的若干个连通块一一对应着若干个团。
此处应注意区分:连通块的大小指树上的点的数量,而团的大小指奶牛的数量。
考虑计算一个连通块对应的团的大小。直接算是不好算的,这个团是由与这个连通块相连的非法链刻画的,每条非法链会从这个团中扣掉一部分。不妨用牛的总数 \(k\) 减去每条非法链从这个团中划分出去的数量。设一条非法链已知其 \(A,B,C\),那么它可以从这个团中划分出 \(k-(A-1)=k-(n-(B-1)-C)\) 个点。
那么对于一个连通块,枚举其每一条非法链出边,计算其团的大小。设连通块大小为 \(siz\),非法链出边数量为 \(cnt\)。
对每个连通块维护 \(cnt\) 和 \(siz\),就可以算出对应的团的大小。
从大到小枚举 \(k\),就可以改删边为加边,使用并查集维护。上述过程在 \(k=n\) 时会越界,要特殊处理 \(k=n\) 时的答案,显然这是 \(n!\)。
注意到这里枚举的复杂度是 \(O(\sum\limits_k \text{连通块个数})=O(\sum\limits_k\text{非法链个数})\)。考虑每条非法链的贡献,可以得到 \(O(\sum\limits_k\text{非法链个数})=O(\sum\limits_k\text{链长})=O(n)\)。过程中如果用 set 维护,时间复杂度 \(O(n\log n)\)。
代码
#include<bits/stdc++.h>
using namespace std;
constexpr int maxn=1e5+10,mo=1000000007;
using i64=long long;
int n,ans[maxn],d[maxn],siz[maxn],cnt[maxn],fa[maxn],fac[maxn],ifac[maxn];
vector<int> e[maxn];
set<int> s;
int findf(int x){
return x==fa[x]?x:fa[x]=findf(fa[x]);
}
void merge(int u,int v,int L){
int fu=findf(u),fv=findf(v);
fa[fu]=fv;cnt[fv]+=cnt[fu]-2;siz[fv]+=siz[fu]+L-2;
s.erase(fu);
}
int ksm(int x,int y){
int rs=1;for(;y;y>>=1,x=(i64)x*x%mo) if(y&1) rs=(i64)rs*x%mo;return rs;
}
void prework(){
fac[0]=1;for(int i=1;i<=n;++i) fac[i]=(i64)fac[i-1]*i%mo;
ifac[n]=ksm(fac[n],mo-2);for(int i=n;i;--i) ifac[i-1]=(i64)ifac[i]*i%mo;
for(int i=1;i<=n;++i){
if(d[i]!=2){
fa[i]=i;siz[i]=1;cnt[i]=d[i];
s.insert(i);
}
}
}
int tot;
struct Chn{
int u,v,L;
}chn[maxn];
bool cmp(Chn x,Chn y){
return x.L<y.L;
}
void dfs(int u,int f,int p,int L){
if(d[u]!=2){
if(p) chn[++tot]=(Chn){p,u,L};
L=1;p=u;
}
for(int v:e[u]) if(v!=f) dfs(v,u,p,L+1);
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<n;++i){
int u,v;cin>>u>>v;
e[u].emplace_back(v);e[v].emplace_back(u);
d[u]++;d[v]++;
}
prework();
dfs(*s.begin(),0,0,0);sort(chn+1,chn+tot+1,cmp);
ans[n]=fac[n];
int p=1;
for(int k=n-1;k;--k){
while(p<=tot&&chn[p].L<n-k){
merge(chn[p].u,chn[p].v,chn[p].L);p++;
}
ans[k]=fac[k];
for(int x:s){
ans[k]=(i64)ans[k]*ifac[(cnt[x]-1)*(n-k-1)+siz[x]-1]%mo;
}
}
for(int i=1;i<=n;++i) cout<<ans[i]<<'\n';
return 0;
}
10. [AGC066E] Sliding Puzzle On Tree
同 P6277 [USACO20OPEN] Circus P 的分析,不过答案变成 \(\dbinom{n}{k}\prod (s_i!)\)。
11. P7293 [USACO21JAN] Sum of Distances P
12. [AGC001E] BBQ Hard
先范围翻倍,方便处理。形式对称,换元 \(t_i=a_i+b_i\)。上生成函数,\(\dbinom{t_i+t_j}{a_i+a_j}=[z^{a_i+a_j}](1+z)^{t_i+t_j}\)。原式每一项乘上 \(z^{-a_i-a_j}\),将 \([z^{a_i+a_j}]\) 变成 \([z^0]\) 提到最外面。然后里面是一个多项式的平方,直接处理。
最后提取 \([z^0]\) 即可。
13. [AGC040C] Neither AB nor BA
14. [AGC005D] ~K Perm Counting
看到排列,考虑图论建模。发现拆置换环好像没啥前途,直接放弃。
用另一种看待排列的方法:将 \((i,p_i)\) 看做二维平面上的一个点,于是选择一个点之后同一行同一类不能再选点。
尝试容斥,因为发现直接 DP 不了。钦定选 \(k\) 个非法位置。在二维平面上将根据排列的性质(同一行同一列不能有两个点)在非法的位置之间连边,发现是若干条链,其实就是给每条链选若干非法的点,满足是一个独立集的方案数卷积起来,直接考虑一条链的情况就好。
而这个是好算的。结束了。
15. [AGC006C] Rabbit Exercise
看到期望设随机变量。
直接考虑随机变量的变化,可以得到一次小操作后的期望位置。于是直接得到期望的变化。
得到式子后可以直接维护期望算答案,但是需要优化。
发现下标相差 \(\pm 1\),不大,考虑差分数组,得到差分上的变化只是交换两个位置。
一个大操作是重复若干次小操作,每个小操作的交换是相同的,这需要维护置换的幂。
手法是:拆置换环,置换乘上自身就相当于在置换环上走一步,这个可以直接根据同余快速计算。
可以做到线性。
需要小小补一下置换的知识。
16. [ARC101E] Ribbons on Tree
考虑直接 DP,设 \(f_{u,i}\) 表示 \(u\) 子树中还有 \(i\) 个点要向上匹配。直接转移是 \(O(n^3)\) 的,爆了。
考虑容斥,设 \(g_S\) 表示钦定 \(S\) 中的边没有被覆盖的方案数,最后的答案就是 \(\sum\limits_{S} (-1)^{|S|}g_S\)。
修改状态为 \(f_{u,i}\) 表示 \(u\) 所在连通块大小为 \(i\),暂时不考虑 \(u\) 所在连通块的贡献(这是为了方便算合并两个连通块后的方案)。
由于是钦定断边,所以连通块内部随便连,不用考虑是否连通。一个大小为 \(x\) 的连通块内部两两配对的方案数是好算的,类似 \(\prod (2k-1)\) 的一个东西。
在 DP 途中带上容斥系数,可以得到转移:
-
合并两个连通块:\(f'_{u,i+j}\gets f_{u,i}f_{v,j}\);
-
钦定一条断边:\(f'_{u,i}\gets (-1)\cdot f_{u,i}f_{v,j}coef_j\);
这里 \(coef_j\) 是大小为 \(j\) 的连通块内部任意连边的方案数。
于是复杂度 \(O(n^2)\)。
17. [ARC102E] Stop. Otherwise...
两个数的和为 \(x\),那么必然是形如 \((p,x-p)\) 的对,首先这个东西的种类数是好算的。
发现限制非常强劲,考虑容斥。针对非法的个数容斥,发现不可区分导致很难容斥。于是针对出现过的非法对的种类容斥。
再用个插板算方案数就好了。
18. [ARC104E] Random LIS
经典的问题了。
首先转化为所有方案中 LIS 之和。注意到 \(n\) 极小,于是可以直接钦定排名,算出 LIS。这里用搜索,可以加上一点剪枝。
于是只要算钦定排名后,符合这个排名的方案数,就可以算出 LIS 的贡献。
先把相同排名的缩到一起,取值上限取最小的。设缩完之后有 \(m\) 个数,数列为 \(b_i\),问题转化为:\(b_i\) 的取值在 \([1,V_i]\),有多少种方案使得序列单调递增。
由于 \(V_i\) 很大,考虑离散化值域。设 \(f_{i,j}\) 表示考虑了前 \(i\) 个数,第 \(i\) 个数在值域上第 \(j\) 段的方案数。这里有个小技巧:给所有的右端点都加一,变成左闭右开的形式,这样处理值域段时都是左闭右开的。此题特殊,左端点都是 \(1\),于是也可以不这样处理,转而定义左开右闭的形式,只需再加入一个 \(0\)。
转移就是考虑上一段数在哪里。枚举这一段有多少个数和上一段是哪一段,可以得到 \(f_{i,j}=\sum\limits_{k=0}^{i-1}\sum\limits_{t=0}^{j-1}f_{k,t}\times coef\)。
后面的系数是要将 \(i-k\) 个数放入第 \(j\) 段的方案数,就是 \(\dbinom{len_j}{i-k}\)。这个组合数可以暴力算,\(len_j\) 太大而 \(i-k\) 较小。
DP 过程中还要排除一些非法情况,比如 \(i-k\) 个数中有取不到第 \(j\) 段的。
直接这样做就可以 \(O(n^{n+5})\) 了。\(O(n^n)\) 是为了枚举排名。
19. P3643 [APIO2016] 划艇
和上一题一样的套路。
用上小技巧,将取值范围搞成左闭右开的。再离散化。
设 \(f_{i,j}\) 表示考虑前 \(i\) 个数,第 \(i\) 个在第 \(j\) 段的方案数。
统计答案只需对 \(f\) 求和即可。
限制比较强力,序列中除了 \(0\) 以外,必须单调递增。还是先写出转移:
这里的系数略有不同。还是将 \(i-k\) 个数放入第 \(j\) 段,其中取值范围不含第 \(j\) 段的必须取 \(0\),而取值范围含第 \(j\) 段的可以取 \(0\) 也可以不取。
不妨设取值范围含第 \(j\) 段的数的个数为 \(m\),由于第 \(i\) 个数必选,计算系数时要特殊考虑其影响。枚举有多少个数不为 \(0\),这个系数就是
化一下转移的式子,\(f_{i,j}=\sum\limits_{k=0}^{i-1}\dbinom{len_j+m-1}{m}\sum\limits_{t=0}^{j-1}f_{k,t}\),这里前缀和优化一下,倒序枚举 \(k\) 来递推组合数,即可 \(O(n)\) 转移。
于是 \(O(n^3)\)。
20. CF1295F Good Contest
还是同一个套路。题目要求单调不增,可以翻转一下变成单调不降。
和上一道题的区别仅在转移系数。只需考虑在 \(len_j\) 中挑出 \(i-k\) 个数使得其单调不增的方案数。
用插板法,将 \(i-k\) 个数看做无标号小球,放到 \(len_j\) 个有标号盒子中,盒子可以为空。不难得出系数为 \(\dbinom{len_j+i-k-1}{i-k-1}\)。
一样做就行了,\(O(n^3)\)。

浙公网安备 33010602011771号