【题解】组合数学
来自\(\texttt{SharpnessV}\)的省选复习计划中的组合数学。
由于作者非常菜所以只能随便写点基础的。
简单数数。越狱的方案数等于总方案数减没有越狱的方案数。
所以\(Ans=m^n-m\times (m-1)^{n-1}\) 。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
#define P 100003
using namespace std;
typedef long long ll;
ll Pow(ll x,ll y){
ll now=1;
for(;y;y>>=1,x=x*x%P)if(y&1)now=now*x%P;
return now;
}
int main(){
ll m,n;scanf("%lld%lld",&m,&n);
printf("%lld\n",(Pow(m,n)-m*Pow(m-1,n-1)%P+P)%P);
return 0;
}
下面是组合数环节,以下是必背恒等式。
递推式
阶乘式
对称式
合并式
求和式 \(1\)
求和式 \(2\)
二项式定理
范德蒙德卷积
奇偶和相等
除了等式,还需要知道二项式反演
卢卡斯定理
直接递推求组合数。时间复杂度\(\rm O(N^2)\),好处是不用担心 \(P\) 不是质数。
#include<bits/stdc++.h>
using namespace std;
int t,k;
int f[2005][2005],sum[2005][2005];
void prepare(){
f[0][0]=1;
for(int i=1;i<=2000;i++){
for(int j=1;j<=2000;j++)
f[i][j]=(f[i-1][j-1]+f[i-1][j])%k;
f[i][0]=1;
}
for(int i=0;i<=2000;i++)
for(int j=0;j<=2000;j++)
if(i>=j&&!f[i][j])
sum[i][j]++;
for(int i=1;i<=2000;i++){
for(int j=1;j<=2000;j++)
sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
}
}
int main()
{
scanf("%d%d",&t,&k);
prepare();
while(t--){
int x,y;scanf("%d%d",&x,&y);
printf("%d\n",sum[x][y]);
}
return 0;
}
\(\rm Prufer\) 序列,唯一需要知道的是一个长度为 \(n-2\) 的值域为 \(n\) 的序列与一颗有标号无根树对应,并且是一一对应关系。
并且度数为 \(d_i\) 的节点在序列中恰好出现 \(d_i-1\) 次。
所以 \(n\) 个节点的无根树个数是 \(n^{n-2}\) 。
这道题给定度数,就是可重集排列,直接套公式即可。
套用定理,预处理\(p\)以内的阶乘,对于\(n,m\ge p\)的组合数,利用定理递归下去即可。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
using namespace std;
int n,m,P,fac[N],inv[N];
int Pow(int x,int y){
int now=1;
for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
return now;
}
int C(int n,int m){
if(n<m||n<0||m<0)return 0;
return 1LL*fac[n]*inv[m]*inv[n-m]%P;
}
int lucas(int n,int m){
if(n<P&&m<P)return C(n,m);
return 1LL*lucas(n/P,m/P)*lucas(n%P,m%P)%P;
}
int main(){
int T;scanf("%d",&T);
fac[0]=inv[0]=1;
while(T--){
scanf("%d%d%d",&n,&m,&P);
rep(i,1,P-1)fac[i]=1LL*fac[i-1]*i%P,inv[i]=Pow(fac[i],P-2);
printf("%d\n",lucas(n+m,n));
}
return 0;
}
非常水的基础组合数学题。
首先我们对每个括号算贡献,简单容易一下得到一个括号的贡献为可重集组合。
这里的可重集组合是每个数都有无限个,从 \(n\) 种数种选择 \(m\) 个数的方案数。
我们假设第 \(i\) 次选的数是 \(a_i\),不失一般性我们令 \(a_i\) 不减。
令 \(k_i=a_i+i-1\) ,不难发现 \(k_i\) 递增,且 \(k_i\) 序列与 \(a_i\) 序列一一对应。
而 \(k_i\) 的取值范围是 \(n+m-1\),所以可重集组合数就是\(\binom{n+m-1}{m}\)。
#include<bits/stdc++.h>
#define int long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 2000005
#define P 998244353
using namespace std;
typedef long long ll;
char s[N];int n,k,sta[N],top;ll fac[N<<1],cur[N],inv[N<<1],ans;
ll Pow(ll x,int y){ll now=1;for(;y;y>>=1,x=x*x%P)if(y&1)now=now*x%P;return now;}
ll C(int x,int y){return fac[x]*inv[y]%P*inv[x-y]%P;}
void init(){
inv[0]=fac[0]=1;
rep(i,1,n+k)fac[i]=fac[i-1]*i%P,inv[i]=Pow(fac[i],P-2);
rep(i,1,n)cur[i]=C(i+k-1,k);
}
ll R(int x,int y){return (cur[x]-cur[x-y])%P;}
ll T(int x){return cur[x];}
signed main(){
scanf("%lld%lld",&n,&k);
scanf("%s",s+1),init();
for(int i=1;i<=n;i++)if(s[i]=='(')sta[++top]=i;
else ans+=1LL*R(i,i-sta[top])*T(n-i+1)%P,top=std::max(0LL,top-1);
top=0;sta[0]=n+1;
for(int i=n;i;i--)if(s[i]==')')sta[++top]=i;
else ans+=T(i)*R(n-i+1,sta[top]-i)%P,top=std::max(top-1,0LL);
printf("%lld\n",(ans%P+P)%P);
return 0;
}
错位排列问题,\(D_n\) 表示 \(n\) 个数的错排方案数。
我们对最后一个数 \(n\) ,它可以错开到 \(1\sim n-1\)。
假设它错开到 \(i\) ,那么如果第 \(n\) 位为 \(i\) ,方案数为\(D_{n-2}\),否则方案数为 \(D_{n-1}\)。
这样我们就得到了递推式\(D_n=(n-1)(D_{n-1}+D_{n-2})\),或者换一种写法\(D_n=nD_{n-1}+(-1)^n\)。
给定\(n,g\),求 \(\large g^{\sum\limits_{d\mid n}\binom{n}{d}}\bmod 999911659\)
\(999911659\)是一个质数,根据费马小定理,我们直接求\(\sum\limits_{d\mid n}\binom{n}{d}\bmod 999911658\) 即可。
这个数显然不是质数,但是这个数可不是乱写的,\(999911658=2\times 3\times 4679\times 35617\),这又是四个小质数。
我们对每个质数用\(\rm Lucas\)求出\(\binom{n}{d}\),然后用 \(\rm CRT\) 合并答案即可。
首先存在长度为 $3 $ 的下降子序列的序列不是好的。
可以简单理解一下,如果存在,则中间的元素必定要向右一次后向左一次,等于做了无用功,无法卡到下界。
那么我们要求不存在长度为 \(3\) 的下降子序列。
我们不难\(\rm DP\),设 \(f[i][j]\) 表示前 \(i\) 个数 ,最大数为 \(j\) 的方案数。如果\(a_{i+1}>j\),则\(f[i][j]\to f[i+1][a_{i+1}]\)。否则,\(a_{i+1}\)必须为最小的数,有\(f[i][j]\to f[i+1][j]\)。
我们把 \(f[i][j]\) 看成平面直角坐标系的点 \((i,j)\),我们从\((0,0)\)开始,要到\((n,n)\),每次可以直接向右移动一步,也可以向右上移动。
向右上移动看起来非常没有规律,改一下,将向右上移动等价于先向上移动\(k\)步,再向右移动一步。
这就很清晰了,我们从原点开始,到\((n,n)\)结束,每次可以选择向右或向上移动一步的方案数。
还有一个现实条件\(i\le j\),因为排列中 \(i\) 个数的最大值一定不小于 \(i\),在平面上,就是路径穿过 \(y=x\) 的直线。
这就是经典组合模型,直接折线法,\(\rm Ans=\dbinom{2n}{n}-\dbinom{2n}{n-1}\) 。
考虑字典序的限制条件,类似于数位\(\rm DP\),我们前求出前\(i\)个走到的点,并计算当前点到终点的方案数。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 1200006
#define P 998244353
using namespace std;
int Pow(int x,int y){
int now=1;
for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
return now;
}
int fac[N],inv[N],n;
int C(int x,int y){
if(x<y||y<0||x<0)return 0;
return 1LL*fac[x]*inv[y]%P*inv[x-y]%P;
}
int get(int x,int y){return (C(n-x+n-y,n-x)-C(n-x+n-y,n-y-1))%P;}
int a[N],c[N];
inline void add(int x){for(;x<=n;x+=x&-x)c[x]++;}
inline int ask(int x){int sum=0;for(;x;x-=x&-x)sum+=c[x];return sum;}
void solve(){
memset(c,0,sizeof(c));
scanf("%d",&n);
rep(i,1,n)scanf("%d",&a[i]);
int y=0,ans=0;
rep(i,1,n-1){
if(a[i]<y){
ans=(ans+get(i-1,y+1))%P;
if(ask(a[i])!=a[i]-1)break;
}
else ans=(ans+get(i-1,a[i]+1))%P;
y=max(a[i],y);add(a[i]);
}
printf("%d\n",(ans+P)%P);
}
int main(){
fac[0]=inv[0]=1;
rep(i,1,N-5)fac[i]=1LL*fac[i-1]*i%P,inv[i]=Pow(fac[i],P-2);
int T;scanf("%d",&T);
while(T--)solve();
return 0;
}
不难想到枚举匹配的情侣和他们坐的位置,有\(\dbinom{n}{k}^2k!\) 的方案数。
考虑剩下的情侣,错排问题,定义 \(D_i\) 为 \(i\) 对情侣的错排方案,不考虑排列顺序。
经典错排问题中\(D_i=(i-1)(D_{i-1}+D_{i-2})\) 。
这里错排的情侣可以男女配对,也可以同行配对,所以方案数翻倍。
还考虑每对座椅分左右,再\(\times 2^n\)
总的方案数为\(\dbinom{n}{k}^2k!2^n(n-k)!D_{n-k}\),直接递推即可。
这里要线性复杂度,所以预处理阶乘,次幂,并线性求逆元。
这样我们不用生成函数解决了这道题。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5000005
#define P 998244353
using namespace std;
int w = N - 5 , inv[N] , fac[N] , d[N] , pw[N];
int C(int n,int m){
if(n<0||m<0||n<m)return 0;
return 1LL*fac[n]*inv[m]%P*inv[n-m]%P;
}
int main(){
pw[0]=1;
rep(i,1,w)pw[i]=(pw[i-1]<<1)%P;
inv[1]=1;rep(i,2,w)inv[i]=-1LL*(P/i)*inv[P%i]%P;
fac[0]=inv[0]=1;
rep(i,1,w)fac[i]=1LL*fac[i-1]*i%P,inv[i]=1LL*inv[i-1]*inv[i]%P;
d[0]=1;d[1]=0;
rep(i,2,w)d[i]=1LL*2*(i-1)*(d[i-1]+d[i-2])%P;
int T;scanf("%d",&T);
while(T--){
int n,k;scanf("%d%d",&n,&k);
printf("%lld\n",1LL*C(n,k)*C(n,k)%P*fac[k]%P*pw[n]%P*d[(n-k)]%P*fac[n-k]%P);
}
return 0;
}
难度很高的一题。
首先考虑 \(a=b\) 的情况。
对于任意一种 \(A\) 赢了 \(B\) 的情况,将每次抛硬币正负取反,对应于一种 \(B\) 赢了 \(A\) 的情况。
那么我们将所有情况分为平局和不平局的情况,\(A\)赢了的情况显然是不平局的情况的\(\dfrac{1}{2}\) 。
所以我们只用计算平局的情况,枚举正面向上的次数,得到\(\sum\limits_{i=0}^a \dbinom{n}{i}^2\),卷积一下得到\(\dbinom{2n}{n}\) 。
考虑 \(a>b\) 的情况。
首先对于平局\(x=y\)的情况,\(a-x>b-y\),取反后一定是 \(A\) 赢 。
对于 \(A\) 输局 \(x<y\) 的情况,\(a-x>b-y\),取反后也一定是 \(A\) 赢。
对于 \(A\) 赢局 \(x>y\) 的情况,无法判断 \(a-x\) 和 \(b-y\) 的大小。
我们发现前两种情况都是输赢对应,最后一种可能输赢对应,也可能都是赢。
我们计算都是赢的情况,有\(a-x>b-y\to a-b>x-y\) ,考虑到\(a-b\le 10^4\),我们可以枚举 \(y\) 和 \(x-y\),令 \(k=x-y\) 。
然后直接计算组合数即可,模数不为质数,考虑扩展\(\rm Lucas\)。
斯特林数。
递推式
组合意义,考虑第 \(n\) 个元素重新分组还是加入原先的组。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 105
long long s[N][N];
int main(){
s[1][1]=1;
rep(i,2,N-5)rep(j,1,i)s[i][j]=s[i-1][j]*j+s[i-1][j-1];
int n,m;
while(~scanf("%d%d",&n,&m))printf("%lld\n",s[n][m]);
return 0;
}
//这道题需要高精度
可能有用的模板。
考虑将 \(n\) 个球放入 \(m\) 个不同盒子中,允许有空盒子,得到恒等式。
令\(f(x)=x^n,g(x)=\begin{Bmatrix}n\\x\end{Bmatrix}x!\),发现上面就是个二项式反演的标准形式。一波推导后的到
发现这就是一个卷积,直接\(\rm NTT\)即可。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 600005
#define P 167772161
using namespace std;
int Pow(int x,int y){
if(y<0)return Pow(Pow(x,P-2),-y);
int now=1;
for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
return now;
}
int n,a[N],b[N],rev[N],t,fac[N];
void ntt(int *u,int op){
rep(i,0,t-1)if(rev[i]>i)swap(u[rev[i]],u[i]);
for(int l=1,k=2;k<=t;k<<=1,l<<=1){
int cur=Pow(3,(P-1)/k*op);
for(int i=0;i<t;i+=k){
int now=1;
rep(j,0,l-1){
int x=u[i+j],y=1LL*u[i+j+l]*now%P;
u[i+j]=(x+y)%P;u[i+j+l]=(x-y)%P;
now=1LL*now*cur%P;
}
}
}
}
int main(){
scanf("%d",&n);
t=1;while(t<=n*2)t<<=1;
rep(i,1,t-1)rev[i]=(rev[i>>1]>>1)|((i&1)?(t>>1):0);
fac[0]=1;rep(i,1,n)fac[i]=1LL*fac[i-1]*i%P;
rep(i,0,n)a[i]=1LL*Pow(i,n)*Pow(fac[i],P-2)%P,b[i]=Pow(-1,i)*Pow(fac[i],P-2);
ntt(a,1);ntt(b,1);rep(i,0,t-1)a[i]=1LL*a[i]*b[i]%P;ntt(a,-1);
int cur=Pow(t,-1);
rep(i,0,n)printf("%lld ",(1LL*cur*a[i]%P+P)%P);
return 0;
}
看起来多项式乘次幂乘组合数非常不可做。
一个比较讨论的思路是通常幂转下降幂。
众所周知
这里多项式直接转下降幂
这里已经可以\(\rm O(M\log M)\)计算,我们还需要计算下降幂的系数\(b_i\),直接斯特林数即可。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1005
int P,n,m,x,a[N],b[N],s[N][N];
int Pow(int x,int y){
int now=1;
for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
return now;
}
int main(){
scanf("%d%d%d%d",&n,&x,&P,&m);
rep(i,0,m)scanf("%d",&a[i]);
s[0][0]=1;
rep(i,1,m)rep(j,1,m)s[i][j]=(s[i-1][j-1]+1LL*s[i-1][j]*j)%P;
rep(i,0,m)rep(j,i,m)b[i]=(b[i]+1LL*a[j]*s[j][i])%P;
int ans=0;
rep(i,0,m){
int cur=1;
rep(j,n-i+1,n)cur=1LL*cur*j%P;
ans=(ans+1LL*b[i]*cur%P*Pow(x,i)%P*Pow(x+1,n-i)%P)%P;
}
printf("%d\n",ans);
return 0;
}
卡特兰数。
我们定义 \(f[i]\) 表示 \(i\) 级阶梯的方案数,我们枚举矩形将阶梯分成两部分,得到两个递归子问题,从而得到方程\(f[i]=\sum\limits_{j=0}^{i-1}f[j]\times f[i-j-1]\)。
这就是卡特兰数的递推式,一般卡特兰数写作\(C_i\)
卡特兰数有许多经典模型:
- 给定\(1\sim n\)的入栈数列,合法的出栈序列数
- 凸\(n\)边形的三角形划分
- \(n\)个节点的二叉树个数
- \(n\)对括号的合法括号序列数
这些我们都可以直接用递推式序列,比如括号匹配,我们定义\(f_i\)表示\(n\)对括号的方案数。显然第一个一定是左括号,我们枚举与之匹配的右括号位置,这就是卡特兰数的递推式。
我们可以换一个思路\(\rm DP\),定义\(f[i][j]\)表示长度为 \(i\) 的序列,剩余 \(j\) 个左括号的方案数,显然 \(j\ge 0\)。
在平面直角坐标系中,就是从原点出发,每次可以向右上或右下走一格,走到\((2n,0)\)的方案数,不能碰到直线 \(y=-1\) 。
直接用折线法可以得到方案数,即为卡特兰数, \(C_n=\dbinom{2n}{n}-\dbinom{2n}{n-1}\)。
把组合数拆开可以得到 \(C_n=\dfrac{(2n)!}{n!(n+1)!}\) 。
同时可以得到一阶递推公式 \(C_n=\dfrac{4n-2}{n+1}C_{n-1}\) 。
从小到大考虑每个数的填放位置。
不难发现对于奇数位和偶数位,都是从左往右填,且任何时刻奇数位填的数不少于偶数位填的数。
我们定义\(f[i][j]\)表示奇数位填了 \(i\) 个,偶数位填了 \(j\) 个。显然\(f[i][j]\)可以转移到\(f[i+1][j]\),\(f[i][j+1]\),且不能穿过直线 \(y=x\)。
这就是卡特兰数的折现模型,答案为\(C_n\)。
首先 \(n\) 个节点的二叉树有 \(C_n\)个。
我们将二叉树的叶子删去一个,得到 \(k\) 个大小为 \(n-1\) 的二叉树。
反过来,我们可以在大小为 \(n-1\) 的二叉树上挂一个节点,得到大小为 \(n\) 的二叉树,每个二叉树有 \(n\) 个可以挂的位置。
所以叶子数之和为 \(nC_{n-1}\),答案为\(\dfrac{nC_{n-1}}{C_n}=\dfrac{n(n+1)}{2(2n-1)}\)
#include<bits/stdc++.h>
int main(){
int n;scanf("%d",&n);
return printf("%.10lf",1.00*n*(n+1)/2/(2*n-1)),0;
}
容斥原理。
有若干个物品,每个物品有若干属性(可能没有),现在要求出带有属性的物品个数。
我们加上至少带有一个属性的物品,减去至少带有两个属性的物品,再加上至少带有三个,以此类推。
而最简单的容斥就是求补集,有时候答案集合并不好求,我们直接求答案集合的补集。
简单容斥,考虑总方案数减去不符合条件的。
不符合条件就是某个食材用了超过\(\left\lfloor\dfrac{k}{2}\right\rfloor\)次,最多只有一个食材超过了。我们枚举是哪一个食材超过了,然后减去即可。
其实如果限制是\(\left\lfloor\dfrac{k}{3}\right\rfloor\),甚至是\(\left\lfloor\dfrac{k}{4}\right\rfloor\)都是可以做的,不妨思考一下。
硬币种类很少,考虑容斥。
先不考虑数量限制,直接多重背包求出方案。
然后枚举哪些硬币超过了限制,容斥即可得到答案。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
using namespace std;
int c[4],d[4],s,n,w=N-5;long long f[N];
int main(){
rep(i,0,3)scanf("%d",&c[i]);
f[0]=1;
rep(i,0,3)rep(j,0,w-c[i])f[j+c[i]]+=f[j];
scanf("%d",&n);
rep(i,1,n){
rep(j,0,3)scanf("%d",&d[j]);
scanf("%d",&s);long long ans=f[s];
rep(S,1,15){
long long cur=0,op=1;
rep(k,0,3)if((S>>k)&1)op*=-1,cur+=(d[k]+1)*c[k];
if(cur<=s)ans+=op*f[s-cur];
}
printf("%lld\n",ans);
}
return 0;
}
求 \(n-1\) 个公司每个公司修恰好一条边的生成树个数。
如果不考虑什么公司修,直接矩阵树定理即可。
既然要考虑,直接容斥即可。把哪些公司没有修作为容斥中物品的属性,那么答案就是总方案数减去有属性的物品数量,经典容斥模型。