组合数学
组合数学
包含基本推导,容斥,二项式反演等内容
杨辉三角相关 link
结论
定义式:
对称恒等式(下指标反转):
吸收恒等式:
加法递推式:
列递推式:
列前缀和:
组合意义:枚举选的最后一个
斜前缀和:
组合意义:枚举选一个后缀
二项式定理:
【推论1】第\(n\)行求和\(=2^n\)
每一种组合方案映射到长度为 \(n\) 的 \(01\) 串,共有 \(2^n\) 种
【推论2】第\(n\)行交错求和\(=0\)
【推论3】第\(n\)行\(偶数和=奇数和=2^{n-1}\)
上指标反转:
交错行前缀和:
用上指标反转可证明
注意组合数行前缀和并没有封闭形式
组合数乘积:
组合意义:\(n\)个物品分成3份:\(k,m-k,n-m\)
范德蒙德恒等式:
二项式反演
恰好 \(\to\) 至多
\(f_i\) 恰好 \(k\) 个的方案数
当 \(g_i\) 为至多 \(k\) 个的方案数
当 \(g_i\) 为至少 \(k\) 的方案数
多重组合数
多重集是指包含重复元素的广义集合
\(S=\{a_1 \cdot n_1,...,a_k \cdot n_k\}\)
表示 \(n_1\) 个元素有 \(a_1\) 个,令 \(n=\Sigma n_i\)
\(S\) 的全排列个数为 $${n \choose {n_1,n_2,..n_k}}=\frac{n!}{\prod_{i=1}^k n_k!}$$
P2415 集合求和
\(pts1100\) 基本思路
考虑每个数的贡献
去掉这个数任选的集合方案为 \(2^{n-1}\)
那么强选这个数,这个数产生的贡献即为 \(w_i \times 2^{n-1}\)
\(ans=sum \times 2^{n-1}\)
题目
P2822 [NOIP2016 提高组] 组合数问题
\(pts 1300\,\) \(trick\) 和细节
发现这个式子大概率可以做一个二维前缀和,所以只用考虑当前的 \(i\;j\) 即可
当前产生贡献的充要条件是 \(k|\)\(i \choose j\)
那么一个小 \(trick\) 直接取模即可
还有一个小坑,就是前缀和也相当于一个杨辉三角,是由三个值转移而来,最后需要加一句
\(f_{i,i+1}=f_{i,i}\)
画个二维前缀和的矩阵就看出来了, \(f_{i,i+1}\) 相当于在算 \(f_{i+1,i+1}\) 的时候填上了 \(-f_{i,i}\)
#include<bits/stdc++.h>
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x]) //注意加了&
using namespace std;
const int N=2e3+10;
int k;
int C[N][N],f[N][N];
int fr(){ //double 不能快读!!!!
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
void init()
{
for(int i=0;i<=2000;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%k;
}
for(int i=1;i<=2000;i++)
for(int j=1;j<=i;j++)
f[i][j]=(C[i][j]==0);
for(int i=1;i<=2000;i++)
{
for(int j=1;j<=i;j++)
f[i][j]+=f[i-1][j]+f[i][j-1]-f[i-1][j-1];
f[i][i+1]=f[i][i];
}
}
int main()
{
int T=fr();
k=fr();
init();
while(T--)
{
int n=fr(),m=fr();
fw(f[n][min(m,n)]),nl;
}
return 0;
}
P1450 [HAOI2008] 硬币购物
\(pts 2000\) 思路巧妙
直接暴力是 \(O(qlog_ds)\) 的需要卡常
考虑优化
如果能够直接去掉那个 \(log_d\) 就够了,想到完全背包,就是 \(O(m)\) 的
如果每一种都没有限制,就是完全背包,考虑容斥去掉不合法方案
设 \(f_i\) 表示不满足第 \(i\) 种限制的方案总数,直接容斥原理即可
不合法即至少选 \(d_i+1\) 个 \(c_i\) 货币,方案数为 \(f_{m-(d_i+1)\cdot c_i}\)
#include<bits/stdc++.h>
#define int long long
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x]) //注意加了&
using namespace std;
const int N=7,M=1e5+10;
int n=4,m,q;
int c[N],d[N],f[M];
int fr(){ //double 不能快读!!!!
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
signed main()
{
for(int i=1;i<=n;i++) c[i]=fr();
f[0]=1;
for(int i=1;i<=n;i++)
for(int j=c[i];j<=M-10;j++)
f[j]+=f[j-c[i]];
q=fr();
while(q--)
{
for(int i=1;i<=n;i++) d[i]=fr();
m=fr();
int ans=0;
for(int i=0;i<=(1<<n)-1;i++)
{
int cnt=0,val=0;
for(int j=0;j<n;j++)
if((i>>j)&1) cnt++,val+=(d[j+1]+1)*c[j+1];
cnt=(cnt&1)?-1:1;
if(m>=val) ans+=cnt*f[m-val];
}
fw(ans),nl;
}
return 0;
}
P6692 出生点
\(pts 1800\) 公式推导较为麻烦
考虑一个简单的情况,无障碍点
\(\Sigma_{i=1}^n\Sigma_{j=1}^m\Sigma_{i=1}^n\Sigma_{l=1}^m |i-k|+|j-l|\)
\(x\) \(y\) 贡献独立,拆开计算
\(m^2\Sigma_{i=1}^n\Sigma_{k=1}^n |i-k|+n^2\Sigma_{j=1}^m\Sigma_{l=1}^m|j-l|\)
只用考虑前一种,后面将 \(m\) 换成 \(n\) 即可
\(m^2\Sigma_{i=1}^n (i-1+i-2+...+i-(i-1)+0+(i+1)-i+..+n-i)\)
考虑 \(i\) 出现的次数,把 \(i\) 都拿掉,剩下的就是两个等差数列
\(m^2 \Sigma_{i=1}^n \frac{n^2+n-2(n+1)i+2i^2}{2}\)
然后每一项出现 \(n\) 次
\(\Sigma_{i=1}^n i^2=\frac{n(n+1)(2n+1)}{6}\)
总贡献为 \(\frac{m^2n(n^2-1)}{3}\)
注意题目说了 小 \(W\) 出生在点 \(A\),小 \(H\) 出生在点 \(B\),跟小 \(W\) 出生在点 \(B\),小 \(H\) 出生在点 \(A\),这两种情况视作同一种情况。
即第一部分总贡献为 \(\frac{m^2n(n^2-1)+n^2m(m^2-1)}{3 \times 2}\)
然后考虑障碍点,减掉
\(\Sigma_{p=1}^k\Sigma_{i=1}^n\Sigma_{j=1}^m |x_p-i|+|y_p-j|\)
显然也可以拆开计算
\(m \times \Sigma (x_p-1+..+x_p-(x_p-1)+0+(x_p+1)-x_p+..+n-x_p)\)
同上
\(m \Sigma((x-1)x-\frac{(x-1+1)(x-1)}{2} + \frac{(n-x+1)(n-x)}{2})\)
\(m \Sigma (\frac{2x^2-2(n+1)x+n^2+n}{2})\)
然后容斥一下,会减掉两个都是障碍点的,要加回来,这里可以排序
\(\Sigma_{i=1}^{k}\Sigma_{j=1}^{i-1} |x_i-x_j|+|y_i-y_j|\)
单独考虑每个横坐标的贡献
\((x_i-x_1)+(x_i-x_2)+..+(x-x_{i-1})\)
\((i-1)x_i-\Sigma_{j=1}^{i-1} x_j\)
\(ans=ans1-ans2+ans3\)
#include<bits/stdc++.h>
#define int long long
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x]) //注意加了&
using namespace std;
const int N=5e5+10,Q=1e9+7;
int n,m,k,ans;
int sx[N],sy[N];
pi p[N];
int fr(){ //double 不能快读!!!!
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
bool cop1(pi a,pi b){return a.first<b.first;}
bool cop2(pi a,pi b){return a.second<b.second;}
int qkw(int a,int k)
{
int ans=1,base=a;
while(k)
{
if(k&1) ans=ans*base%Q;
base=base*base%Q;
k>>=1;
}
return ans;
}
signed main()
{
n=fr(),m=fr(),k=fr();
for(int i=1;i<=k;i++) p[i]={fr(),fr()};
int inv2=qkw(2,Q-2),inv6=qkw(6,Q-2);
ans=(m*m%Q*n%Q*(n*n%Q-1)%Q+n*n%Q*m%Q*(m*m%Q-1)%Q)%Q*inv6%Q; //n*n 取模!!
for(int i=1;i<=k;i++)
{
//这部分是减!!
int x=p[i].first,y=p[i].second;
ans=(ans-m*((2*x*x%Q-2*(n+1)%Q*x+n*n%Q+n%Q)%Q*inv2%Q)%Q); //也是三个数相乘都要取模!!
ans=(ans-n*((2*y*y%Q-2*(m+1)%Q*y+m*m%Q+m%Q)%Q*inv2%Q)%Q);
}
ans=(ans%Q+Q)%Q;
sort(p+1,p+1+k,cop1);
for(int i=1;i<=k;i++)
{
int x=p[i].first;
sx[i]=(sx[i-1]+x)%Q;
ans=(ans+(i-1)*x%Q-sx[i-1])%Q;
}
sort(p+1,p+1+k,cop2);
for(int i=1;i<=k;i++)
{
int y=p[i].second;
sy[i]=(sy[i-1]+y)%Q;
ans=(ans+(i-1)*y%Q-sy[i-1])%Q;
}
ans=(ans%Q+Q)%Q;
fw(ans);
return 0;
}
P2606 [ZJOI2010]排列计数
\(pts 2100\) 思维难度高,取模 \(trick\)
性质 \(p_i \gt p_{\lfloor i/2 \rfloor}\) 等价于 \(p_i \lt min(p_{2i},p_{2+1})\)
此为小根堆(树形结构),父亲小于儿子的性质
等价于给你 \(1\to n\) 的数,让你构造一个小根堆的方案数
首先 \(p_1\) 只能等于 \(1\)
不难想到此题为 \(dp\),考虑对这个二叉树进行树形 \(dp\)
\(f_i\) 表示 \(1 \to i\) 的排列中构成小根堆性质的个数,记 \(l\;r\) 为左右儿子的 \(size\)
\(f_i=f_l \times f_r \times {i-1 \choose l}\)
由于根必定是 \(1\) ,所以还剩 \(i-1\) 个点留给子树
左右子树分的个数任意,注意只要每个子树内部满足二叉堆性质即可!!
子树的所有方案只与大小有关,而与子树填的什么数无关
因为首先无重复的数,其次可以把任选出来的数排序,等价于 \(1 \to l_{size}\) 的排列方案,所以之于大小有关
考虑如何求出 \(l\;r\)
作为一个二叉堆,其为完全二叉树
从 \(2\) 开始递归左子树打标记即可
那么每次枚举以 \(1\) 为根的二叉树的新增节点,判断是在左子树还是右子树即可
相当于是扩展 \(1\) 这个节点直到一棵树
取模 \(trick\)
此题未保证 \(p \gt n\) 所以 \(n!\) 在 \(mod\;p\) 意义下可能不存在逆元,因为 \(n!\) 中会含有 \(p\),所以无法用阶层逆元的方式求解
考虑 \(lucas\) 解决,其解决了 \(n \gt m\) 的问题,因本质是转成 \(p\) 进制数分别求组合数,规避了 \(n!\) 的逆元问题
#include<bits/stdc++.h>
#define int long long
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x])
using namespace std;
const int N=1e6+10;
int n,p;
int f[N],isl[N],fac[N],nf[N];
int fr(){
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
int qkw(int a,int k)
{
int ans=1,base=a;
while(k)
{
if(k&1) ans=ans*base%p;
base=base*base%p;
k>>=1;
}
return ans;
}
int C(int a,int b)
{
if(a<b) return 0;
return fac[a]*nf[a-b]%p*nf[b]%p;
}
int lucas(int a,int b)
{
if(a<p || b<p) return C(a,b);
return C(a%p,b%p)*lucas(a/p,b/p)%p;
}
void findl(int x)
{
isl[x]=1;
if((x<<1)<=n) findl(x<<1);
if((x<<1|1)<=n) findl(x<<1|1);
}
signed main()
{
n=fr(),p=fr();
fac[0]=nf[0]=1;
int m=min(n,p-1);
for(int i=1;i<=m;i++) fac[i]=fac[i-1]*i%p;
nf[m]=qkw(fac[m],p-2);
for(int i=m-1;i;i--) nf[i]=nf[i+1]*(i+1)%p;
findl(2);
f[0]=f[1]=1;
for(int i=2,l=0,r=0;i<=n;i++) //枚举以 $1$ 为根的二叉树中出现的新节点
{
isl[i]?l++:r++; //新增一个节点,要么在左子树,要么在右子树
f[i]=f[l]*f[r]%p*lucas(i-1,l)%p;
}
fw(f[n]);
return 0;
}
P6521 [CEOI2010 day2] pin
\(pts 2200\)
\(sol1\) 反演模板,\(sol2\) 简化反演
\(D\) 位不同,等价于 \(4-D\) 位相同
\(sol1\)
这恰好,一眼反演
设 \(g_i\) 为至少 \(i\) 位相同的情况
直接暴力枚举 \(2^4\) 种相同位情况,不同的填 \(?\) 或者 \(#\) ,然后 \(O(n)\) + \(hash\) 统计这几位相同的数量,就求出了所有至少的情况
然后做一次二项式反演即可
\(O(k2^4n)\) \(k\) 为 \(hash\) 的巨大常数
#include<bits/stdc++.h>
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x]) //注意加了&
using namespace std;
const int N=5e4+10,M=6;
int n,k,ans;
int g[M],C[M][M];
map<string,int> tot;
string s[N];
int fr(){ //double 不能快读!!!!
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
int main()
{
n=fr(),k=fr(),k=4-k;
for(int i=1;i<=n;i++) cin>>s[i];
for(int i=0;i<=(1<<4)-1;i++)
{
int cnt=0;
tot.clear();
for(int j=0;j<4;j++) cnt+=((i>>j)&1);
for(int j=1;j<=n;j++)
{
string t=s[j];
for(int k=0;k<4;k++) if(!((i>>k)&1)) t[k]='?';
g[cnt]+=tot[t];
tot[t]++;
}
}
for(int i=0;i<=4;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=C[i-1][j]+C[i-1][j-1];
}
for(int i=k;i<=4;i++) ans+=(((i-k)&1)?-1:1)*C[i][k]*g[i];
fw(ans);
return 0;
}
\(sol2\)
对反演进行一定的优化
设 \(f_i\) 为恰好 \(i\) 位相同的方案数
当 \(D==1\) 的时候 \(3\) 位相同
\(f_3=g_3\),没有两个字符串四位全相同
当 \(D==2\) 的时候 \(2\) 为相同
但是求的是恰好两位,这个时候 \(g_2\) 包含了 \(g_3\) 的情况,所以要去掉
\(f_2=g_2-3 \times f_3\) 任选两个三个相同的,都可以变成 \(3\) 对两个相同的
注意 \(cnt\) 统计的时候,就是和之前的对数匹配,相当于就是任选两对的过程
当 \(D==3\) 的时候 \(1\) 位相同
包含 \(f_2\;f_1\)
\(f_1=g_1 - 2 \times f_2 -3 \times f_3\)
当 \(D==4\) 的时候,直接用任选的容斥即可
\(f_0=C_n^2 - f_1 -f_2 - f_3\)
#include<bits/stdc++.h>
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x]) //注意加了&
using namespace std;
const int N=5e4+10,M=6;
int n,k,ans;
long long f[M],g[M];
map<string,int> tot;
string s[N];
int fr(){ //double 不能快读!!!!
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
int main()
{
n=fr(),k=fr(),k=4-k;
for(int i=1;i<=n;i++) cin>>s[i];
for(int i=0;i<=(1<<4)-1;i++)
{
int cnt=0;
tot.clear();
for(int j=0;j<4;j++) cnt+=((i>>j)&1);
for(int j=1;j<=n;j++)
{
string t=s[j];
for(int k=0;k<4;k++) if(!((i>>k)&1)) t[k]='?';
g[cnt]+=tot[t];
tot[t]++;
}
}
f[3]=g[3];
f[2]=g[2]-3*f[3];
f[1]=g[1]-2*f[2]-3*f[3];
f[0]=1ll*n*(n-1)/2-f[1]-f[2]-f[3];
fw(f[k]);
return 0;
}
P4678 [FJWC2018]全排列
\(pts 2600\) 综合题目,细节多
题意:
\(C(P,x)\) 该序列内小于 \(x\) 的个数
\(F(P_1,P_2)\) 区间 \([l,r]\) 内,\(i \in [l,r]\),\(C(P_1,P_{1i})=C(P_2,P_{2i})\) 且 \(P_1\) 中逆序对数目 \(\leq k\)
\(1 \leq l \leq r \leq n\)
求 \(\Sigma F(P_1,P_2)\) \(|P_{1,2}|=n\)
先来一道前置题
P2513 [HAOI2009]逆序对数列
发现其实每个数的贡献为其后面比它小的数,即与相对大小有关
设计初始状态 \(f_{i,j}\)
表示 \(1 \to i\) 的排列中,逆序对个数为 \(j\) 的方案数
那么直接枚举前 \(i-1\) 比 \(i\) 大的数有多少个即可
\(f_{i,j}=\Sigma_{k=0}^{min(i-1,j)} f_{i-1,j-k}\)
正确性:找到一个位置插入第 \(i\) 个数,可以产生 \([0,i-1]\) 个逆序对,因为第 \(i\) 个数比 \([1,i-1]\) 都大
初始化:\(f_{1,0}=1\)
#include<bits/stdc++.h>
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x]) //注意加了&
using namespace std;
const int N=1e3+10,Q=1e4;
int n,k;
int f[N][N];
int fr(){ //double 不能快读!!!!
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
int main()
{
n=fr(),k=fr();
f[1][0]=1;
for(int i=2;i<=n;i++)
for(int j=0;j<=min((i*(i-1))>>1,k);j++)
for(int k=0;k<=min(i-1,j);k++)
f[i][j]=(f[i][j]+f[i-1][j-k])%Q;
fw(f[n][k]);
return 0;
}
前缀和优化
把上述式子转化一下
\(f_{i,j}=f_{i,j}=\Sigma_{k=0}^{min(i-1,j)} f_{i-1,j-k}=\Sigma_{max(j-(i-1),0)}^j f_{i-1,k}\)
直接对 \(f_{i,j}\) 前缀和即可
初始化 \(f_{1,0}=f_{1,1}=1\)
不能拿 \(0\) 当前点,无前驱但是不合法
然后一个细节就求前缀和要到 \((i+1) \cdot (i+1-1)/2\) 因为下一次访问的时候,会访问到这个前缀和,这也是上面为什么还要初始化 \(f_{1,1}\) 的原因
#include<bits/stdc++.h>
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x])
using namespace std;
const int N=1e3+10,Q=1e4;
int n,k;
int f[N][N];
int fr(){
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
int main()
{
n=fr(),k=fr();
f[1][0]=f[1][1]=1;
for(int i=2;i<=n;i++)
{
for(int j=0;j<=min(i*(i-1)>>1,k);j++)
f[i][j]=(f[i-1][j]+(j>=i?Q-f[i-1][j-i]:0))%Q;
if(i==n) continue;
for(int j=1;j<=min(i*(i+1)>>1,k);j++)
f[i][j]=(f[i][j]+f[i][j-1])%Q;
}
fw(f[n][k]);
return 0;
}
回到本题
性质:两段序列按值离散化的下标相同
这个题目类似一个区间 \(dp\) 的形式,于是我们想到不妨按照 \([l,r]\) 的区间长度划分集合
\(ans=\Sigma_{i=1}^n((C_n^i \times (n-i)!)^2 \times f_{i,k} \times C_{n-i+1}^1)\)
第一部分:选择 \(i\) 个数,剩下的随便填,然后两个序列等价
第二部分:长度为 \(i\),逆序对 \(\leq k\) 的任意填 \([1,n]\) 的方案数
第三部分:这个区间可放在序列的任意合法地方,注意两个区间的 \([l,r]\) 相同,同时移动
正确性:把 \([l,r]\) 排个序,等价于 \([1,i]\)
\(O(T(n^3+nE))\)
显然 \(T\) 飞飞
考虑再次预处理每次询问降到 \(O(n)\) 级别
#include<bits/stdc++.h>
#define int long long
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x])
using namespace std;
const int N=502,Q=1e9+7;
int n,k;
int fac[N],C[N][N],f[N][N*(N-1)>>1];
int fr(){
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
void solve()
{
int n=500;
f[1][0]=f[1][1]=1;
for(int i=2;i<=n;i++)
{
for(int j=0;j<=i*(i-1)>>1;j++)
f[i][j]=(f[i-1][j]+(j>=i?Q-f[i-1][j-i]:0))%Q;
for(int j=1;j<=i*(i+1)>>1;j++)
f[i][j]=(f[i][j]+f[i][j-1])%Q;
}
fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%Q;
for(int i=0;i<=n;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%Q;
}
}
signed main()
{
solve();
int T=fr();
while(T--)
{
n=fr(),k=fr();
int ans=0;
for(int i=1;i<=n;i++)
{
int rs=fac[n-i]*C[n][i]%Q;
ans=(ans+rs*rs%Q*f[i][min(k,i*(i-1)>>1)]%Q*(n-i+1)%Q)%Q;
}
fw(ans),nl;
}
return 0;
}
P9035 KDOI-04 Pont des souvenirs
\(pts 2600\) 综合题目,数学推导
首先,这种题目都要转成单增的,才可以用组合计数。
\(b_i=a_i+i-1\) 。
\(1 \leq b_1 \lt b_2 \lt b_3 \lt .... \lt b_n \leq k+n-1\) 。
显而易见 \(b_n=a_n+n-1 \geq n\) 。
每一种 \(b_n\) 的取值集合,都对应一类 \(a\) 序列方案
根据条件 \(2\) ,都满足就只需最大的满足即 \(a_{n-1}+a_n \leq k+1\) 。
\(a_{n-1} \leq \lfloor \frac{k+1}{2} \rfloor\) 。
考虑枚举最高位 \(a_n\),\(a_n\)绝对是求不了的只能枚举,\(a_n\) 的影响只在 \(a_{n-1}\)。
\(a_n=1\;a_{n-1}=1\;\;a_n=2\;a_{n-1}=1\,or\,2\;\;a_n=k\;a_{n-1}=1\)
\(a_n=\lceil \frac{k+1}{2} \rceil\;\;a_{n-1}=\lfloor \frac{k+1}{2} \rfloor\)
整个 \(a_{n-1}\) 取值类似 \(1\;2\;3\;4\;3\;2\;1\) 或者 \(1\;2\;3\;4\;4\;3\;2\;1\)
这里只算一半的方案即下取整的部分,或者说峰值左侧,因为 \(a_n\) 的取值对 \(a_{n-1}\) 的取值影响范围是确定的
所以 \(a_{n-1} \in [1,\lfloor \frac{k+1}{2} \rfloor]\),\(a_{n_{max}}= \lceil \frac{k+1}{2} \rceil\)
\(b_{n-1} \in [1,\lfloor \frac{k+1}{2} \rfloor]+n-2\)
$ans=\Sigma;C_{p+n-2}^{,n-1};;p \in [1, \lfloor \frac{k+1}{2} \rfloor] $
前面的所有数值域是 \(p+n-2\) 从值域中选 \(n-1\) 个数出来。
然后按从小到大填到序列 \(b\) 里面
根据以下公式,\(C_a^b =C_{a-1}^b + C_{a-1}^{b-1}\) 。
化简得 \(ans=C_{n-2+1}^{n}+C_{n-2+1}^{\,n-1}+C_{n-2+2}^{\,n-1}+C_{n-2+3}^{\,n-1}+...= C_{n-2+maxp+1}^{n}\)
由于 \(k\) 的奇偶性不知,这个 \(\lfloor \frac{k+1}{2} \rfloor\) 需要分类讨论一下
-
k为基:单峰值,\(a_n=a_{n-1}\) 先算 \(maxp\),再把 \(maxp\) -- 算剩下的部分也是 \(ans*2\)。
-
k为偶:双峰值,直接 \(ans*2\) 即可
至于基偶峰值,需要打表看
本题求组合数只能用阶层的方式,且求解逆元需要线性复杂度
\(code\)
#include<bits/stdc++.h>
#define int long long
#define pt putchar(' ')
#define nl puts("")
#define pi pair<int,int>
#define pb push_back
#define go(it) for(auto &it:as[x])
using namespace std;
const int N=2e7+10,Q=1e9+7;
int t,n,k;
int f[N],nf[N];
int fr(){
int x=0,flag=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
return x*flag;
}
void fw(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) fw(x/10);
putchar(x%10+'0');
}
int max(int a,int b){return a>b?a:b;}
int min(int a,int b){return a<b?a:b;}
void init()
{
f[0]=nf[0]=f[1]=nf[1]=1; //注意1也要初始化
for(int i=2;i<N;i++)
{
f[i]=f[i-1]*i%Q;
nf[i]=nf[Q%i]*(Q-(Q/i))%Q;
}
for(int i=1;i<N;i++) nf[i]=nf[i-1]*nf[i]%Q; //转成阶层逆元
}
int C(int a,int b)
{
if(a<b) return 0;
return f[a]*nf[b]%Q*nf[a-b]%Q;
}
signed main()
{
init();
t=fr();
while(t--)
{
n=fr(),k=fr();
if(n==1) {fw(k),nl;continue;}
if(k&1)
{
int maxp=(k+1)/2;
int ans=C(n-2+maxp,n-1);
maxp--;
ans=(ans+2*C(n-2+maxp+1,n)%Q)%Q;
fw(ans),nl;
}
else
{
int maxp=(k+1)/2;
int ans=2*C(n-2+maxp+1,n)%Q;
fw(ans),nl;
}
}
return 0;
}
CF232B
\(pts 2500\) 性质发现+公式推导
我们发现 \(i\) 和 \(i+n\) 的情况是等价的,我们只用考虑 \(n^2\) 的矩阵即可
根据每一行选 \(j\) 个来划分集合
快速幂预处理这个次幂,\(O(n^4)\)
计数题能预处理就预处理
const int N=110,M=1e4+10,Q=1e9+7;
int n,m,q;
int C[N][N],f[N][M],val[N][M];
int qkw(int a,int k)
{
int ans=1,base=a;
while(k)
{
if(k&1) ans=ans*base%Q;
base=base*base%Q;
k>>=1;
}
return ans;
}
void prework()
{
for(int i=0;i<=n;i++)
for(int j=0;j<=i;j++)
{
if(!j) C[i][j]=1;
else C[i][j]=(C[i-1][j-1]+C[i-1][j])%Q;
}
for(int j=0;j<=n;j++)
{
int k1=qkw(C[n][j],m/n+1);
int k2=qkw(C[n][j],m/n);
for(int i=1;i<=n;i++)
val[i][j]=(i<=m%n)?k1:k2;
}
}
signed main()
{
n=fr(),m=fr(),q=fr();
prework();
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=n;j++)
for(int k=0;k+j<=min(i*n,q);k++)
(f[i][k+j]+=f[i-1][k]*val[i][j]%Q)%=Q;
fw(f[n][q]);
return 0;
}