计数与递推
公式
排列组合
排列公式 P(n,k)=n!(n−k)!
组合公式 C(n,k)=P(n,k)P(k,k)=n!(n−k)!k!
组合公式的性质
- C(0,0)=C(n,0)=C(n,n)=1
- C(n,k)=C(n,n−k)
- C(n,k)=C(n−1,k)+C(n−1,k−1)
- C(n,k)=C(n,k−1)⋅(n−k+1)/k
递推
Catalen 数
给一个凸
n
边形,用
题目
[UVA11538] Chess Queen
国际象棋中,在同一行、列、对角线上的皇后是可以互相攻击的,求在 n×m 的棋盘上摆放两个可以互相攻击的皇后的方法数。
n,m≤106
首先,在同一行的方案数很容易想到: nm(m−1) ,那么同一列的就是 mn(n−1)
对于对角线上的情况,我们发现对角线的长度依次为:
考虑到对角线有两个方向,需要乘 2
那么答案为
#include<iostream>
#include<cstdio>
using namespace std;
typedef unsigned long long ull;
int main()
{
ull n,m;
while(cin>>n>>m)
{
if(!n&&!m) break;
if(n>m) swap(n,m);
cout<<n*m*(m+n-2)+2*n*(n-1)*(3*m-n-1)/3<<endl;
}
return 0;
}
[UVA11401] Triangle Counting
给定正整数 n ,求边长为不大于
n 的不同正整数的三角形的个数。n≤106
设最大边长为
a
的三角形有
可以暂时认为 fy 是一个等差数列, fb=0+1+2+3+⋯+(a−2)=(a−1)(a−2)2
然而这样我们计算了 b=c 的情况,由于 b∈[⌊a2⌋+1,a−1] ,则需要减去 ⌊a−12⌋
又由于 b,c 的对称性,还需要除以 2
则
而最长的边可以在 [1,n] 之间的整数中选择,故答案为 ans(n)=∑i=1nf(n)
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
const int MAXN=1e6;
ll f[MAXN+5];
inline void preTab()
{
for(ll i=4;i<=MAXN;i++)
f[i]=f[i-1]+((i-1)*(i-2)/2-(i-1)/2)/2;
}
int main()
{
preTab();
int n;
while(cin>>n)
{
if(n<3) break;
cout<<f[n]<<endl;
}
return 0;
}
[UVA11806] Cheerleaders
在一个 m×n 的矩形网格中放 k 个相同的石子,要求每个格子例最多放一个,石子必须用完,且第一行、最后一行、第一列、最后一列都要放石子。
T≤50,2≤m,n≤20,k≤500
如果没有最后一个条件的限制,答案可以很轻易用组合公式求出。有了限制之后可以考虑容斥原理,二进制枚举即可。
#include<iostream>
#include<cstdio>
const int MAXK=500,P=1e6+7;
int n,m,k;
int C[MAXK+5][MAXK+5];
inline void preTab()
{
int i,j;
C[0][0]=1;
for(i=1;i<=MAXK;i++)
{
C[i][0]=C[i][i]=1;
for(j=1;j<i;j++) C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
}
}
int main()
{
preTab();
int Case,T;scanf("%d",&T);
for(Case=1;Case<=T;Case++)
{
int ans=0;
scanf("%d%d%d",&n,&m,&k);
for(int S=0;S<16;S++)
{
int cnt=0,r=n,c=m;
if(S&1) r--,cnt++;if(S&2) r--,cnt++;
if(S&4) c--,cnt++;if(S&8) c--,cnt++;
if(cnt&1) ans=(ans-C[r*c][k]+P)%P;
else ans=(ans+C[r*c][k])%P;
}
printf("Case %d: %d\n",Case,ans);
}
return 0;
}
[POJ1737] Connected Graph
给定正整数 n ,求有
n 个节点的连通图个数。n≤50
直接计算答案并不容易,我们考虑求出所有的可能性后减去不连通的个数。
设
f(n)
为
n
个节点的连通图个数(答案),
对于
接下来我们希望找到一个递推式。
考虑
g(n)
的求法,对于一号节点,枚举其所在连通块的节点数
i
,那么有
由于显而易见的结论: f(n)+g(n)=h(n)
那么可得:
用 python 计算后我们发现结果很长,所以需要用高精度。
#include<iostream>
#include<cstdio>
#include<cstring>
const int MAXN=55;
struct BN
{
int num[1005],len;
BN(int x=0)
{
len=0;
memset(num,0,sizeof(num));
if(x) len--;
while(x) num[++len]=x%10,x/=10;
}
};
BN operator +(BN a,BN b)
{
BN res;res.len=std::max(a.len,b.len);
for(int i=0;i<=std::max(a.len,b.len);i++)
{
res.num[i]+=a.num[i]+b.num[i];
if(res.num[i]>=10)
res.num[i]-=10,res.num[i+1]++;
}
while(true)
{
res.num[res.len+1]+=res.num[res.len]/10,res.num[res.len]%=10;
if(res.num[res.len+1]) res.len++;
else break;
}
return res;
}
BN operator -(BN a,BN b)
{
BN res;res.len=std::max(a.len,b.len);
for(int i=0;i<=std::max(a.len,b.len);i++)
{
res.num[i]+=a.num[i]-b.num[i];
if(res.num[i]<0)
res.num[i]+=10,res.num[i+1]--;
}
while(!res.num[res.len]) res.len--;
return res;
}
BN t[1005];
BN operator *(BN a,BN b)
{
int i,j;
for(i=0;i<=a.len;i++) t[i]=BN();
for(i=0;i<=a.len;i++)
{
for(j=0;j<=b.len;j++)
t[i].num[i+j]=a.num[i]*b.num[j];
for(j=0;j<=b.len;j++)
t[i].num[i+j+1]+=t[i].num[i+j]/10,t[i].num[i+j]%=10;
t[i].len=i+b.len;
while(true)
{
t[i].num[t[i].len+1]+=t[i].num[t[i].len]/10,t[i].num[t[i].len]%=10;
if(t[i].num[t[i].len+1]) t[i].len++;
else break;
}
}
BN res;
for(i=0;i<=a.len;i++) res=res+t[i];
return res;
}
BN qpow(BN x,int n)
{
int i;
BN res=BN(1);
for(i=1;i<=n;i<<=1,x=x*x)
if(i&n) res=res*x;
return res;
}
BN f[MAXN],C[MAXN][MAXN],h[MAXN];
inline void initTab()
{
int i,j;
for(i=0;i<MAXN;i++)
h[i]=qpow(BN(2),i*(i-1)/2);
C[0][0]=BN(1);
for(i=0;i<MAXN;i++)
{
C[i][0]=C[i][i]=BN(1);
for(j=1;j<i;j++)
C[i][j]=C[i-1][j]+C[i-1][j-1];
}
for(i=1;i<MAXN;i++)
{
f[i]=h[i];
for(j=1;j<i;j++)
f[i]=f[i]-(C[i-1][j-1]*f[j]*h[i-j]);
}
}
void print(BN x)
{
for(int i=x.len;i>=0;i--)
putchar(x.num[i]+'0');
}
int main()
{
int i,n;
initTab();
while(scanf("%d",&n)!=EOF&&n)
{
print(f[n]);
putchar('\n');
}
}
[2017.08.25] 锦标赛游戏
有 n 个人进行循环赛,每两人进行比赛的胜率平分。如果两个人互相直接或间接赢过对方,那么成这两人水平相当。对于一个人,若有 k 个人与其水平相当,那么可以获得 di 奖金。求选手中期望奖金的最大值对 998244353 取模的结果。
n≤3000
设 f[n] 为所有种类的竞赛图的所有点的期望奖金之和,由于一种竞赛图中的所有点等价,所以所有种类的竞赛图中所有的点求平均即为答案。
设
则
#include<iostream>
#include<cstdio>
typedef long long ll;
const int MAXN=3e3+5,P=998244353;
int n;
int d[MAXN],f[MAXN],g[MAXN],ans[MAXN];
int C[MAXN][MAXN],pow2[MAXN*(MAXN-1)/2];
inline void preTab()
{
int i,j;C[0][0]=1;
for(i=1;i<=n;i++)
{
C[i][0]=C[i][i]=1;
for(j=1;j<i;j++) C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
}
pow2[0]=1;
for(i=1;i<=n*(n-1)/2;i++) pow2[i]=pow2[i-1]*2%P;
}
int qpow(int x,int y)
{
int res=1;
for(ll i=1;i<=y;i<<=1,x=(ll)x*x%P)
if(y&i) res=(ll)res*x%P;
return res;
}
int inv(int x){return qpow(x,P-2);}
int main()
{
freopen("tournament.in","r",stdin);
freopen("tournament.out","w",stdout);
int i,j;
scanf("%d",&n);
preTab();
for(i=1;i<=n;i++) scanf("%d",d+i);
for(i=1;i<=n;i++)
{
g[i]=pow2[i*(i-1)/2];
for(j=1;j<i;j++) g[i]=(g[i]-(ll)g[j]*C[i][j]%P*pow2[(i-j)*(i-j-1)/2]%P+P)%P;
}
for(i=1;i<=n;i++) for(j=1;j<=i;j++)
f[i]=(f[i]+(ll)g[j]*C[i][j]%P*(f[i-j]+(ll)d[j]*j%P*pow2[(i-j)*(i-j-1)/2]%P)%P)%P;
for(i=1;i<=n;i++) ans[i]=(ll)f[i]*inv(i)%P*inv(pow2[i*(i-1)/2])%P;
for(i=1;i<=n;i++) printf("%d\n",ans[i]);
}
[2017.09.10] Braid
统计在所有长度为 n 的环排列中, 满足顺时针看不出现
[i,i+1](1≤i<n) 和 [n,1] 这样的子串的环排列个数。答案对 998244353 取模。多组数据。n≤106,T≤20
考虑使用容斥原理进行计数.
包含至少一个形如 [i,i+1] 或 [n,1] 这样的子串的环排列个数是 (n1)(n−2)! 个;
可以推广为包含至少
k(0≤k<n)
个的环排列个数是
(nk)(n−k−1)!
,
同时注意到包含
n
个的环排列个数一定是
所以最终答案就是 (−1)n+∑k=0n−1(−1)n(nk)(n−k−1)!
这个序列 (OEIS A000757) 还有一个具有组合意义的递推式:
#include<iostream>
#include<cstdio>
typedef long long ll;
const int MAXN=1e5,P=998244353;
int f[MAXN+5];
int main()
{
std::ios::sync_with_stdio(false);
freopen("braid.in","r",stdin);
freopen("braid.out","w",stdout);
for(int i=3;i<=MAXN;i++) f[i]=((ll)(i-2)*f[i-1]%P+(ll)(i-1)*f[i-2]%P+((i&1)?1:-1))%P;
int T;std::cin>>T;
while(T--)
{
int n;std::cin>>n;
std::cout<<f[n]<<std::endl;
}
return 0;
}