数论题解
- 计算系数
- combination
- abc433F - 1122 Subsequence 2
- abc154F - Many Many Paths
- abc240G - Teleporting Takahashi
- [CodePlus 2018 4 月赛]组合数问题 2
- [SHOI2015]超能粒子炮・改
- [USACO 2025 US Open, Gold] Problem 1. Moo Decomposition
- F - Maximum Diameter
- [清华集训2016] 组合数问题
- agc043B - 123 Triangle
- [NOIP2025]清仓甩卖
- 信封问题
- [JSOI2011] 分特产
- BZOJ1042 硬币购物
- 水仙花
- BZOJ2839 集合计数
- BZOJ3622 已经没有什么好害怕的了
- [NOI Online #2 提高组] 游戏
- [CF285E]Positions in Permutations
- [THUPC 2019]过河卒二 / chess
- [HAOI2017]方案数
- [清华集训 2012] 串珠子
计算系数
二项式定理:
int ans=((quick(a,n)%mod*quick(b,m)%mod)%mod*(C(k,n)%mod))%mod;
cout<<ans;
combination
直接算\(C_n^m\)
while(T--)
{
int n,m;
cin>>n>>m;
cout<<C(n,m)<<"\n";
}
abc433F - 1122 Subsequence 2
范德蒙卷积
设:
- \(n\) 为分界点前面包括分界点的某元素出现次数,
- \(m\) 为分界点后面对应元素出现次数。
分界点确定后,方案数就是对于每个 \(k\) 前面这个元素中选择 \(k−1\) 个与后面对应元素中选择 \(k\) 个的方案数之积的和。
式子:
前缀和求,预处理组合数。
int n=s.size();
s=" "+s;
for(int i=1;i<=n;i++) for(int j=0;j<10;j++) qzh[i][j]=qzh[i-1][j]+(j==s[i]-'0');
for(int i=n;i>=1;i--) for(int j=0;j<10;j++) hzh[i][j]=hzh[i+1][j]+(j==s[i]-'0');
int ans=0;
for(int i=1;i<=n;i++)
{
if(s[i]=='9') continue;
int x=qzh[i][s[i]-'0'];
int y=hzh[i][s[i]-'0'+1];
if(x==0||y==0) continue;
ans+=C(x+y-1,x);
ans%=mod;
}
cout<<ans;
abc154F - Many Many Paths
推式子
预处理即可
for(int i=r1;i<=r2;i++) f[i]=(C(c2+i+1,c2)-C(c1+i,c1-1)+mod)%mod;
int ans1=0;
for(int i=r1;i<=r2;i++) ans1=(ans1+f[i])%mod;
cout<<ans1%mod;
abc240G - Teleporting Takahashi
把平面旋转 \(45°\)
那么可以先枚举第一维,就转化成二维平面中是否能从 \((0,0)\) 走到 \((y,z)\),旋转一下,就变化成了 \((y−z,y+z)\)
走了 \(a\) 步 \(+1\),\(b\) 步 \(−1\),总共走了 \(k\) 步,目的地是 \(x\),可以得到一个方程组:
\(a=\frac{x+k}{2},b=\frac{x-k}{2}\),而最终方案就是 \(C_k^a\)或\(C_k^b\)。
拓展到这题上面,最终方案就是到 \(y−z\) 的方案数乘上到 \(y+z\) 的方案数,也就是:
对于第一维的判断;设走了\(a\) 步 \(+1\),\(b\) 步 \(−1\),那么最终方案数就是\(C_n^a*C_{n-a}^b(a+b≤n)\)。
inline int solve(int k,int kx,int ky)
{
int a=abs(kx-ky),b=abs(kx+ky);
if(k+a&1||k+b&1||k<a||k<b) return 0;
return C(k,k+a>>1)*C(k,k+b>>1)%mod;
}
x=abs(x),y=abs(y),z=abs(z);
JN();
for(int i=0,j=x;i+j<=n;i++,j++) ans=(ans+C(n,i)*C(n-i,j)%mod*solve(n-i-j,y,z)%mod)%mod;
cout<<ans;
[CodePlus 2018 4 月赛]组合数问题 2
求\(K\)个不同组合数的最大和。
容易发现\(C_n^m\)都比要\(C_{n-1}^m\)都大。
于是一开始可以将所有的\(C_n^i\)都丢到一个优先队列中,每次取出最大值\(C_x^y\),然后将\(C_x^{y-1}再丢\)到优先队列中。重复\(k\)次即可。
发现组合数太大,优先队列无法比较。
于是考虑对组合数取对数,然后比较,就可以了。
\(log(C_x^y)=log({x!\over y!(x-y)!})=log(x!)-log(y!)-log((x-y)!)\)
我们用\(log(x!)=log((x-1)!*x)=log((x-1)!)+log(x)\)预处理阶乘的log即可
priority_queue<pair<double,pair<int,int>>> q;
for(int i=0;i<=n;i++) q.push({lg[n]-lg[n-i]-lg[i],{i,n}});
for(int i=1;i<=k;i++)
{
int x=q.top().se.fi;
int y=q.top().se.se;
q.pop();
ans=(ans+((jie[y]*ni[y-x])%mod*ni[x])%mod+mod)%mod;
y--;
if(y>=x) q.push({lg[y]-lg[y-x]-lg[x],{x,y}});
}
cout<<ans;
[SHOI2015]超能粒子炮・改
方式1
设\(f(n,k)=\sum_{i=0}^kC_n^i\),使用卢卡斯定理
前面那个东西可以提前预处理好前缀和
方式2
设\(f(n,k)=\sum_{i=0}^kC_n^i\),使用卢卡斯定理修改之
\(=\sum_{i=0}^kC_{n\%p}^{i\%p}C_{n/p}^{i/p}\)
对着\(i/p\)相同的若干段分别计算
\(C_{n/p}^0*\sum_{i=0}^{p-1}C_{n\%p}^{i\%p}\)
\(+C_{n/p}^1*\sum_{i=p}^{2p-1}C_{n\%p}^{i\%p}\)
\(+C_{n/p}^2*\sum_{i=2p}^{3p-1}C_{n\%p}^{i\%p}\)
...
\(=\sum_{j=0}^{k/p-1}C_{n/p}^j*\sum_{i=0}^{p-1}C_{n\%p}^i\)
右边本来是在左边的\(\sum\)内部的,但是可以发现循环的是\(i\),和外面的\(j\)无关,直接拿到外面即可
因此\(i/p\)的前面一大堆可以替换为\(f(n/p,k/p-1)*f(n\%p,p-1)\)
对于最后一段不足\(p\)的一段:
\(\sum_{i=k/p*p}^kC_{n\%p}^{i\%p}C_{n/p}^{i/p}\)
=\(C_{n/p}^{k/p}\sum_{i=k/p*p}^kC_{n\%p}^{i\%p}\)
\(=C_{n/p}^{k/p}\sum_{i=0}^{k\%p}C_{n\%p}^i\)
\(=C_{n/p}^{k/p}f(n\%p,k\%p)\)
结论是如果\(k\%p\)非\(0\),\(f(n,k)=f(n/p,k/p-1)*f(n\%p,p-1)+C_{n/p}^{k/p}f(n\%p,k\%p)\)
如果\(\%p=0\)不需要后面那块
如果\(f(n,k)\)落在了\(p\)内部,可以提前预处理之,快速拿到答案。
\(C_{n/p}^{k/p}\)这块可以卢卡斯定理
const int N=3010;
const int mod=2333;
int c[N][N];
inline int solve(int n,int k)
{
if(n<mod) return c[n][min(n,k)];
if(!k) return 1;
int x=k%mod,y=n%mod;
int ret=c[y][min(y,x)]*solve(n/mod,k/mod);
if(k-x!=0) ret=(ret+(c[y][y]-c[y][min(y,x)]+mod)*solve(n/mod,(k-x-1)/mod))%mod;
return ret;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
for(int i=0;i<N;++i) c[i][0]=1;
for(int i=1;i<N;++i) for(int j=1;j<=i;++j) c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
for(int i=0;i<N;++i) for(int j=1;j<=i;++j) c[i][j]=(c[i][j]+c[i][j-1])%mod;
int T,n,k;
cin>>T;
while(T--)
{
cin>>n>>k;
cout<<solve(n,k)<<"\n";
}
return 0;
}
[USACO 2025 US Open, Gold] Problem 1. Moo Decomposition
只看一段循环节就行,每一段循环节之间不会互相影响,如果不同段的 M、O 匹配,说明有一段必然有剩余的 O,总体的 M、O 数量一定不匹配,必然无解,与题目描述矛盾。
设遇到字符 O 的数量为 O,局部的方案数为 ans。
从后往前枚举。
- 如果遇到 O,令 O 增加一;
- 如果遇到 M,从 O 个中选 K 个匹配,令 O 减少 K,令 ans 增加从 O 个中选 K 个的方案数,即\(C_O^K\)
答案为\(ans^L\)
int k,n,l;
string s;
cin>>k>>n>>l>>s;
s=" "+s;
int cnt=0,O=0,ans=1;
for(int i=1;i<=n;i++) if(s[i]=='0') cnt++;
for(int i=n;i>=1;i--)
{
if(s[i]=='O') O++;
else
{
ans=ans*C(O,k)%mod;
O-=k;
}
}
cout<<quick(ans,l);
F - Maximum Diameter
-
合法序列判定条件
长度为 \(N\) 的正整数序列 \(X\) 能构成好树的充要条件是序列所有元素之和等于 \(2(N-1)\)。因为 \(N\) 个节点的树共有 \(N-1\) 条边,每条边贡献 \(2\) 个度数,总度数和为 \(2(N-1)\)。
-
最大直径计算逻辑
对于任意合法序列,所有好树的最大直径等于序列中度数\(≥2\) 的节点数量\(+1\)。构造方式为将所有度数\(≥2\) 的节点连成一条链,两端各接一个叶子节点,剩余叶子节点挂载到链上的节点即可,此时树的直径为链的长度\(+1\),也就是度数\(≥2\) 的节点数\(+1\)。
-
求和公式推导
我们需要计算所有合法序列的\(f(X)\)之和,\(f(X)=\)度数\(≥2\)的节点数\(+1\),因此总和可以拆分为:
总和 \(=\) 所有合法序列的总数 \(+\) 所有合法序列中度数\(≥2\)的节点总数- 合法序列总数:通过插板法可得合法序列的数量为组合数\(C(2N-3,N-1)\)。
- 所有合法序列中度数\(≥2\) 的节点总数:每个位置上 \(X_i≥2\) 的合法序列数量为 \(C(2N-4, N-2)\),总共有 \(N\) 个位置,因此这部分总和为 \(N*C(2N-4, N-2)\)。
- 将两部分合并化简后得到最终公式:总和 \(= (N-3)*C(2N-4, N-2)+2*C(2N-3, N-1)\)
int T;
cin>>T;
while(T--)
{
int n;
cin>>n;
cout<<((n-3)*C(2*n-4,n-2)%mod+2*C(2*n-3,n-1)%mod+mod)%mod<<"\n";
}
[清华集训2016] 组合数问题
k是100以内的质数,nm很大,考虑卢卡斯定理
众所周知,卢卡斯定理告诉我们,\(C_a^b≡C_{a\%k}^{b\%k}C_{a/k}^{b/k}\)
对着\(C_{a/k}^{b/k}\)接着用卢卡斯定理,此时是不是变成了\(C_{a\%k}^{b\%k}C_{a/k\%k}^{b/k\%k}C_{a/k/k}^{b/k/k}\)
也就是说,他是\(a,b\)在\(k\)进制下的每一位一一对应的组合数乘起来:设\(a\)的\(k\)进制下是\(a_0,a_1,a_2...\),\(b\)的\(k\)进制下是\(b_0,b_1,b_2...\),则\(C_a^b≡C_{a_0}^{b_0}*C_{a_1}^{b_1}*C_{a_2}^{b_2}...\)
那么\(C_a^b\%k≡0\)等价于\(k\)进制下存在某一位\(C_{a_i}^{b_i}≡0\),
我们的\(a_i,b_i\)是\([0,k-1]\)范围内的,根据组合数的定义式\(C_a^b={a!\over b! (a-b)!}\),分子和分母涉及的阶乘都小于\(k!\),都没有\(k\)出现,因此是不存在\(a_i>=b_i\)的情况下\(C_{a_i}^{b_i}≡0\)的
只可能是\(a_i<b_i\)的情况\(C_{a_i}^{b_i}=0\)
我们问题转变成了,k进制下,对于\(0\leq i \leq n,0\leq j \leq min(i,m)\),有多少个\(ij\)满足\(k\)进制下存在某一位\(a_i<b_i\),数位dp之
\(f[i][flag1][flag2][flag3]\)表示到了\(k\)进制下第\(i\)位,有没有顶着n的上界,有没有顶着m的上界,之前是不是已经满足了\(a_i<b_i\)
const int N=2e5+5;
const int M=110;
const int mod=1e9+7;
const int inf=1e18;
int t,k,a[M],b[M],cnta,cntb,vis[M][2][2][2][2];
inline int dfs(int s,int bl,int fl,int l1,int l2)
{
if(s==0) return bl;
if(vis[s][bl][fl][l1][l2]!=-1) return vis[s][bl][fl][l1][l2];
int ls1=k-1,ls2=k-1;
if(l1) ls1=a[s];
if(l2) ls2=b[s];
int ans=0;
for(int i=0;i<=ls1;i++)
{
for(int j=0;j<=ls2;j++)
{
if(fl==0)
{
if(i!=j)
{
if(i>j) ans=(ans+dfs(s-1,bl|(i<j),1,l1&&i==ls1,l2&&j==ls2))%mod;
else continue;
}
else ans=(ans+dfs(s-1,bl|(i<j),0,l1&&i==ls1,l2&&j==ls2))%mod;
}
else ans=(ans+dfs(s-1,bl|(i<j),fl,l1&&i==ls1,l2&&j==ls2))%mod;
ans=(ans+mod)%mod;
}
}
ans%=mod;
vis[s][bl][fl][l1][l2]=ans;
return ans;
}
int n,m;
cin>>n>>m;
cnta=0,cntb=0;
memset(vis,-1,sizeof(vis));
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
while(n)
{
a[++cnta]=n%k;
n/=k;
}
while(m)
{
b[++cntb]=m%k;
m/=k;
}
cout<<dfs(max(cnta,cntb),0,0,1,1)<<'\n';
代码逐行详解
const int N=2e5+5;
const int M=110; // 最多k为100,1e18转k进制最多60位,足够存储
const int mod=1e9+7;
const int inf=1e18;
int t,k,a[M],b[M],cnta,cntb,vis[M][2][2][2][2];
// t:测试数据组数,k:给定的质数
// a[]:存储n的k进制各位(低位到高位),cnta: a的有效长度
// b[]:存储m的k进制各位(低位到高位),cntb: b的有效长度
// vis:数位DP记忆化数组,维度分别是:当前处理的位、是否已经找到满足条件的对、是否已经出现i>j、n是否还在原范围、m是否还在原范围
inline int dfs(int s,int bl,int fl,int l1,int l2)
{
// s:当前处理到第s位,bl:是否已经找到符合条件的(i,j)对(1表示已经找到)
// fl:是否已经出现i>j的情况(1表示已经出现,后续j可以随便选)
// l1:当前i的选择是否还受n的限制(1表示受限制,最多只能选到a[s])
// l2:当前j的选择是否还受m的限制(1表示受限制,最多只能选到b[s])
if(s==0) return bl; // 所有位处理完成,返回是否找到符合条件的对
if(vis[s][bl][fl][l1][l2]!=-1) return vis[s][bl][fl][l1][l2]; // 记忆化返回
int ls1=k-1,ls2=k-1; // 当前位i、j的最大可选值
if(l1) ls1=a[s]; // 如果还受n限制,最大只能选a[s]
if(l2) ls2=b[s]; // 如果还受m限制,最大只能选b[s]
int ans=0;
for(int i=0;i<=ls1;i++) // 枚举当前位i的取值
{
for(int j=0;j<=ls2;j++) // 枚举当前位j的取值
{
if(fl==0) // 还没出现i>j的情况,需要保证j<=i
{
if(i!=j) // 当前位i不等于j
{
if(i>j) ans=(ans+dfs(s-1,bl|(i<j),1,l1&&i==ls1,l2&&j==ls2))%mod; // i>j,标记fl=1,后续j可以随便选
else continue; // i<j不符合要求,跳过
}
else ans=(ans+dfs(s-1,bl|(i<j),0,l1&&i==ls1,l2&&j==ls2))%mod; // i==j,fl保持0
}
else ans=(ans+dfs(s-1,bl|(i<j),fl,l1&&i==ls1,l2&&j==ls2))%mod; // 已经出现i>j,j可以随便选
ans=(ans+mod)%mod;
}
}
ans%=mod;
vis[s][bl][fl][l1][l2]=ans;
return ans;
}
int n,m;
cin>>n>>m;
cnta=0,cntb=0;
memset(vis,-1,sizeof(vis)); // 初始化记忆化数组
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
while(n) // 将n转换为k进制,存入a数组
{
a[++cnta]=n%k;
n/=k;
}
while(m) // 将m转换为k进制,存入b数组
{
b[++cntb]=m%k;
m/=k;
}
cout<<dfs(max(cnta,cntb),0,0,1,1)<<'\n'; // 从最高位开始DFS
agc043B - 123 Triangle
1. 第一步先做差
令 \(b_i=|a_i-a_{i+1}|\),\(i=1,\dots,N-1\)。此时 \(b_i\in\{0,1,2\}\)。
之后每一步都是对相邻数做绝对差,而绝对差在模 2 意义下就等于减法(因为 \(|x-y|\equiv x-y \pmod 2\))。所以最终答案的奇偶性可以直接用组合数算:
用 Lucas 定理:\(\binom{n}{k}\) 为奇数当且仅当 (n & k) == k,也就是代码里的 check(n-1, i-1)(这里 n 已经被减过 1,所以传的是 n-1 = N-2)。
2. 但答案不只是奇偶性,可能是 0/1/2
关键结论:
- 如果所有 \(b_i\) 中存在 1,那么最终答案只能是 0 或 1,此时只需算奇偶性即可(模 2 的结果就是答案)。
- 如果所有 \(b_i\) 都不是 1(即每个 \(b_i\in\{0,2\}\)),那么把每个 \(b_i\) 除以 2,答案也除以 2,变成 0 或 1 的问题,最后再乘回 2。
这正是代码中 flag 的作用:
for(int i=1;i<=n;i++) if(a[i]==1)
{
flag=1;
break;
}
if(!flag) for(int i=1;i<=n;i++) a[i]>>=1; // 全为 0/2 时整体除以 2
...
if(!flag) ans<<=1; // 最后答案乘回 2
逐段解释代码
int n,ans=0,flag=0;
cin>>n; // 原始 N
n--; // n = N-1,即差值序列长度
string s; cin>>s;
s=' '+s; // 1-indexed
for(int i=1;i<=n;i++) a[i]=abs(s[i]-s[i+1]); // 第一步差值 b_i
注意 s[i] 是字符 '1'~'3',s[i]-s[i+1] 是字符 ASCII 差,正好等于数字差(因为 '1','2','3' 连续),再取绝对值得到 \(b_i\)。
for(int i=1;i<=n;i++) if(a[i]==1){flag=1;break;}
if(!flag) for(int i=1;i<=n;i++) a[i]>>=1;for(int i=1;i<=n;i++) if(a[i]==1)
{
flag=1;
break;
}
if(!flag) for(int i=1;i<=n;i++) a[i]>>=1;
判断是否存在 1;不存在则整体除 2(此时 \(a_i\in\{0,2\}\to\{0,1\}\))。
for(int i=1;i<=n;i++) if(check(n-1,i-1)) ans^=(a[i]&1);
这里 n 已经是 \(N-1\),所以 n-1 = N-2,对应 \(\binom{N-2}{i-1}\)。用 Lucas 判断奇偶,只把系数为奇数的位置的 \(b_i\) 的奇偶性异或起来——这就是模 2 的求和。
if(!flag) ans<<=1;
cout<<ans;
如果一开始没有 1,说明答案被缩过一半,乘回 2。
[NOIP2025]清仓甩卖
设所有糖果按原价从大到小排序(相同原价按编号)。记为 \(a_1\ge a_2\ge\cdots\ge a_n\)(代码里就是这么排的)。
结论:一个定价方案"不能达到最大原价和",当且仅当存在某个位置,贪心买了一个"性价比更高但原价较小"的糖,占用了钱,导致后面一个"原价更大"的糖买不起。
更精确地,代码中的判定方式是:
对每个 \(i\)(作为分界点),考虑 \(a_i\) 这颗糖。若存在 \(j\) 满足 \(a_j\cdot 2 > a_i\)(即 \(a_j > a_i/2\),原价接近),并且贪心时因为买了这些"小原价、高性价比"的糖而挤掉了 \(a_i\),则这种方案是"坏"的。
为什么答案是 \(2^n - \text{坏方案数}\)?
因为"好方案"(能达到最大值)可能很难直接数,但"坏方案"有一个清晰的结构:
- 存在一个"临界"的糖 \(a_i\)(按原价降序的第 \(i\) 个),
- 它本可以被买到(或者它代表最大原价和的一部分),
- 但因为排序后前面某些糖的定价 \(w=1\)(性价比高),导致钱被花掉,\(a_i\) 买不到。
代码:枚举"坏方案"的临界点
for(int i=1;i<=n;i++)
{
int pos=i;
while(pos<=n&&a[pos]==a[i]) pos++;
pos--;
int k=n+1;
for(int j=pos+1;j<=n&&a[j]*2>a[i];j++)
{
while(a[j]+a[k-1]<a[i]) k--;
ans+=C(j-2,m-i-1)*pow2[n-k+1];
ans%=mod;
}
}
cout<<(pow2[n]-ans+mod)%mod<<'\n';
这一段是整个算法的核心,我们逐个变量解释:
变量含义
-
\(i\):当前考虑的"原价降序第 \(i\) 个"糖,作为被挤掉的目标。
-
\(pos\):与 \(a_i\) 原价相同的最后一个位置(相同原价的糖,编号小的先考虑,所以它们可以一起处理)。
-
\(j\):从 \(pos+1\) 开始,考虑那些原价比 \(a_i\) 小但大于 \(a_i/2\) 的糖(条件
a[j]*2 > a[i])。- 为什么是 \(a_j > a_i/2\)? 因为若 \(a_j\le a_i/2\),则两颗这样的糖原价和 \(\le a_i\),用它们替换 \(a_i\) 不会更优,不构成"坏方案"的关键。只有 \(a_j > a_i/2\) 时,"买两颗小糖 vs 买一颗大糖"才可能出现"买了小的反而总和更大/更小"的微妙关系(性价比排序的干扰)。
-
\(k\):满足 \(a_j + a_{k-1} \ge a_i\) 的最小位置,即"能与 \(a_j\) 配对、原价和 \(\ge a_i\)"的糖的位置下界。
while(a[j]+a[k-1]<a[i]) k--;是从上往下收缩。
组合数部分
ans+=C(j-2, m-i-1)*pow2[n-k+1];
这一项计数的是:
- 前 \(j\) 个糖中(原价 \(\ge a_j\) 的部分),除去 \(a_i\) 所在的"目标段",要选出恰好让钱花到 \(m-i-1\) 元的方案数:
C(j-2, m-i-1)。 - \(k\) 之后的糖(原价较小的部分)可以任意定价:
pow2[n-k+1]。 - 中间段的定价被唯一确定(为了让 \(a_i\) 被挤掉,需要特定的 \(w\) 配置)。
输出
cout << (pow2[n]-ans+mod)%mod<<'\n';
总方案数 \(2^n\) 减去坏方案数 ans,就是"能达到最大原价和"的方案数。
信封问题
全错排模板
\(D(n)=(n-1)*(D(n-1)+D(n-2))\)
如果n号元素放在了i位置,并且n位置放了i号元素
先选元素i,然后发现方案数为\(D(n-2)\)
结论是\((n-1)*D(n-2)\)
如果n号元素放在了i位置,并且n位置没有放i号元素
先选i,然后发现方案数为\(D(n-1)\)
结论是\((n-1)*D(n-1)\)
d[1]=0;
d[2]=1;
d[3]=2;
for(int i=4;i<=n;i++) d[i]=(i-1)*(d[i-1]+d[i-2]);
cout<<d[n];
[JSOI2011] 分特产
一、核心工具:隔板法
1. 允许空盒
把 \(a_i\) 个相同物品放入 \(n\) 个不同盒子,允许空,方案数:
2. 不允许空盒
每个盒子至少 1 个:先给每个盒子放 1 个,剩 \(a_i-n\) 个随便放,方案 \(\binom{a_i-1}{n-1}\)(要求 \(a_i\ge n\))。
但本题不同特产可以互补(某个同学可以在某种特产上为 0,只要在另一种上非 0 就行),所以不能对每种特产单独要求"不空",必须用容斥。
二、无限制总数
若不要求每人至少一个,各特产独立:
这就是"所有分配方案"(可能有人空手)。
三、容斥:强制每人至少一个
设 \(A_k\) = "第 \(k\) 个同学没有拿到任何特产"。
我们要求 \(|\overline{A_1}\cap\overline{A_2}\cap\cdots\cap\overline{A_n}|\)。
由容斥原理:
按 \(|S|=k\) 归类,\(\binom{n}{k}\) 种选法。固定 \(S\)(\(k\) 个同学强制空手),剩下 \(n-k\) 个同学分所有特产(允许空),方案:
所以:
边界 \(k=n\)
剩下 0 个同学。若所有 \(a_i=0\),方案为 1(什么都不分,且所有人都"空手",但本题要求每人至少一个,实际这是被容斥的极端项);本题 \(a_i\ge1\),剩下 0 人无法分任何特产,\(\bigcap\) 为空,贡献 0。代码中 rem==0 直接 continue。
四、代码:容斥主循环
int ans=0;
for(int k=0;k<=n;k++)
{
int rem=n-k;
if(rem==0) continue;
int ways=C(n,k);
for(int i=1;i<=m;i++)
{
ways=ways*C(a[i]+rem-1,rem-1)%mod;
}
if(k&1) ans=(ans-ways+mod)%mod;
else ans=(ans+ways)%mod;
}
cout<<ans%mod<<"\n";
return 0;
逐行:
-
for(k=0..n):枚举强制空手的同学数 \(k\)。 -
rem=n-k:剩下参与分配的同学数。 -
rem==0:\(k=n\),无方案,跳过(本题 \(a_i\ge1\) 保证贡献为 0)。 -
ways=C(n,k):选哪 \(k\) 个同学空手,\(\binom{n}{k}\)。 -
内层
for(i=1..m):对每种特产,剩下rem人分 \(a_i\) 个(允许空),乘上 \(\binom{a_i+rem-1}{rem-1}\)。- 注意:\(a_i+rem-1=a_i+n-k-1\),
rem-1=n-k-1,与公式一致。
- 注意:\(a_i+rem-1=a_i+n-k-1\),
-
if(k&1) ans-=ways,否则ans+=ways:容斥符号 \((-1)^k\)。减法加mod防负。 -
输出
ans%mod。
公式总结
- 隔板法:\(a\) 个相同物品分 \(r\) 人允许空 \(=\binom{a+r-1}{r-1}\)。
- 容斥:强制 \(k\) 人空手,剩余 \(n-k\) 人任意分。
- 各特产独立,乘积;容斥符号交替。
BZOJ1042 硬币购物
一、题目重述
有 4 种硬币,面值分别为 \(c_1,c_2,c_3,c_4\)。
有 \(n\) 次询问,每次给出:
- \(d_1,d_2,d_3,d_4\):第 \(i\) 种硬币最多只能用 \(d_i\) 枚;
- \(s\):要凑出的总价值。
求每次有多少种付款方案,即求非负整数解 \((x_1,x_2,x_3,x_4)\) 的个数:
数据范围:\(c_i,d_i,s\le 10^5\),\(n\le 1000\)。
二、核心思路:完全背包 + 容斥原理
1. 如果没有数量上限
先忽略 \(x_i\le d_i\) 的限制,只求:
这就是一个完全背包计数问题。设 dp[j] 表示用四种硬币(每种无限枚)凑出价值 \(j\) 的方案数,则:
dp[0]=1;
for(int i=1;i<=4;i++) for(int j=c[i];j<=100000;j++) dp[j]+=dp[j-c[i]];
dp[j] 就是上面方程的非负整数解个数。这个预处理只需做一次,之后所有询问都能用。
2. 加上数量上限——容斥原理
有了上限后,直接 DP 不行(每次 \(d_i\) 不同)。用容斥原理把"上限"这个麻烦转成"无上限"。
定义集合 \(A_i\) 表示"第 \(i\) 种硬币用了超过 \(d_i\) 枚"的方案集合。我们要求的是:
由容斥原理:
所以:
3. 如何计算"强制某些硬币超限"的方案数
假设强制集合 \(S\) 中的硬币 \(j\) 至少用 \(d_j+1\) 枚。令:
那么原方程变成:
即剩余价值:
若 \(t<0\),说明强制超限后已经超过 \(s\),方案数为 0;否则方案数就是 dp[t](因为剩下的部分没有上限,正好是预处理好的完全背包)。
4. 容斥的实现
用二进制枚举 \(S\),i 从 0 到 15,第 \(j-1\) 位为 1 表示第 \(j\) 种硬币在 \(S\) 中:
for(int i=0;i<=15;i++)
{
int t=sum,cnt=0;
for(int j=1;j<=4;j++) if((i>>(j-1))&1)
{
t-=c[j]*(d[j]+1);
cnt^=1; // 记录 |S| 的奇偶
}
if(t<0) continue;
if(!cnt) res+=dp[t]; // |S| 偶数,加
else res-=dp[t]; // |S| 奇数,减
}
cnt用异或来统计选中个数的奇偶:偶数次异或为 0,奇数次为 1。|S|偶数(包括空集)符号为 \(+\),奇数符号为 \(-\),正好符合容斥公式。
三、正确性说明
- 完全背包正确性:标准完全背包计数,
dp[j]枚举每种硬币无限使用,得到所有非负解。 - 容斥正确性:容斥原理是集合论恒等式,对任意集合族成立。这里"全集"是无上限解,"\(A_i\)"是第 \(i\) 种超限的解,答案正是"所有 \(x_i\le d_i\)"的解,即补集大小。
- 强制超限的转化:令 \(x_j'=x_j-(d_j+1)\) 是一一对应的双射,所以"\(x_j\ge d_j+1\) 的解"与"\(x_j'\ge0\) 且总价值扣掉 \(c_j(d_j+1)\) 的解"一一对应,数量为
dp[t]。
一句话总结:先用完全背包预处理"无上限"方案数,再用容斥原理把"每种硬币有上限"转化为 16 种"强制某些硬币超限"的子问题,每个子问题扣掉强制部分后直接查 dp 表,奇减偶加即得答案。
水仙花
一、条件化简
相邻两数 \((a_i,a_{i+1})\) 不合法(两个条件都不满足):
- \(a_i>a_{i+1}\)(不满足条件 A)
- \(a_{i+1}\mid a_i\)(不满足条件 B)
所以不合法 = 后一项是前一项的真因子(更小且能整除)。
合法序列 = 不存在"相邻且后项是前项真因子"的序列。
二、关键观察
"后项是前项真因子"意味着数值严格下降且至少减半,所以一条连续的坏关系链长度最多 \(\log_2 m\le 17\)。
思路:用容斥。把序列切成若干段,每段内部全是"坏边"(下降整除链),段间是合法边。
设:
sum[j]= 长度为 \(j\) 的下降整除链数量(\(a_1>a_2>\dots>a_j\),每步整除)dp[i]= 长度为 \(i\) 的合法序列数,dp[0]=1
递推:
即 \(j\) 奇加、\(j\) 偶减。因为链长最多约 17,枚举到 20 够用。
三、怎么算 sum[j]
pre[i][j] = 以值 \(j\) 开头、长度 \(i\) 的下降整除链数。
- 边界:
pre[1][j]=1(长度 1,任何值都行),sum[1]=m - 转移:长度 \(i\)、开头 \(j\) 的链 = 接上以 \(j\) 的真倍数 \(k\) 开头的长度 \(i-1\) 链:
pre[i][j] = sum_{k=2j,3j,...<=m} pre[i-1][k]
for(int i=2;i<=20;i++)
for(int j=1;j<=m;j++)
for(int k=j*2;k<=m;k+=j)
pre[i][j]+=pre[i-1][k];
sum[i] = 所有 pre[i][j] 之和。
复杂度:倍数枚举 \(\sum m/j=O(m\log m)\),共 20 层,约 \(2.4\times10^7\),可过。
四、DP
dp[0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=20;j++){
if(i<j) continue;
if(j%2) dp[i]+=dp[i-j]*sum[j]%mod;
else dp[i]-=dp[i-j]*sum[j]%mod;
dp[i]=(dp[i]%mod+mod)%mod;
}
答案 dp[n]。
符号解释:长度为 \(j\) 的坏链段有 \(j-1\) 条坏边,容斥符号 \((-1)^{j-1}\),即 \(j\) 奇加、\(j\) 偶减。
一句话总结
不合法 = 后项是前项真因子 → 坏链长度只有 \(\log m\) → 预处理坏链数,容斥 DP 拼接,奇加偶减。
代码框架不变,只需注意:pre/sum 预处理一次,DP 用 dp[i-j]*sum[j],符号 j%2 交替,减法取模防负。
BZOJ2839 集合计数
一、题目意思
有一个 \(N\) 元集合,它有 \(2^N\) 个子集。从中选出至少一个子集,使得这些子集的交集元素个数恰好为 \(K\)。求方案数模 \(10^9+7\)。
样例 \(N=3,K=2\):交集恰有 2 个元素,答案 6。
二、核心思路:先"钦定"交集,再容斥
第 1 步:钦定 \(K\) 个元素必须在交集中
从 \(N\) 个元素里选出这 \(K\) 个,方案数 \(\binom{N}{K}\)。
设钦定的集合为 \(S\)(\(|S|=K\))。下面数:所有选中子集都包含 \(S\) 的方案数。
第 2 步:剩余 \(N-K\) 个元素怎么处理
每个子集必须包含 \(S\) 的全部元素,对剩下 \(N-K\) 个元素可以任意取舍。所以一个子集有 \(2^{N-K}\) 种形态。
现在要从这 \(2^{N-K}\) 种形态里选出至少一个子集(非空选取),方案数是:
(每个"形态"可选/不选,共 \(2^{2^{N-K}}\) 种选法,减去全不选的一种。)
第 3 步:容斥,让交集恰好是 \(K\) 个
上面的数法允许交集多于 \(K\) 个(含 \(S\) 只是至少含 \(K\) 个)。用容斥:
解释:
- \(\binom{N-K}{i-K}\):在剩余元素里再选 \(i-K\) 个,凑成"交集至少含 \(i\) 个"。
- \(2^{2^{N-i}}-1\):固定这 \(i\) 个元素后,剩余 \(N-i\) 个元素任意,子集形态 \(2^{N-i}\) 种,非空选取方案。
- \((-1)^{i-K}\):容斥符号,\(i-K\) 奇减偶加。
三、代码逐段对应
for(int i=k;i<=n;i++)
{
ans=(ans+(i%2==k%2?1:mod-1)*C(n-k,i-k)%mod
*((quick(2,quick(2,n-i,mod-1))+mod-1)%mod)%mod)%mod;
}
cout<<ans*C(n,k)%mod;
对照公式:
| 公式 | 代码 |
|---|---|
| \((-1)^{i-K}\) | (i%2==k%2?1:mod-1):同奇偶为 \(+1\),否则 \(-1\) |
| \(\binom{N-K}{i-K}\) | C(n-k,i-k) |
| \(2^{2^{N-i}}-1\) | (quick(2,quick(2,n-i,mod-1))+mod-1)%mod |
| 外层 \(\binom{N}{K}\) | 最后 *C(n,k) |
四、关键细节:指数取模用 \(mod-1\)
算 \(2^{2^{N-i}}\) 时,指数是 \(2^{N-i}\),可能巨大。由费马小定理,模 \(10^9+7\)(质数)下:
所以先算 quick(2,n-i,mod-1) 得到 \(E\bmod(mod-1)\),再 quick(2, 那个值) 得到 \(2^E\)。
这就是代码里 quick(2,quick(2,n-i,mod-1)) 的由来。
注意:\(mod-1=10^9+6\) 不是质数,但费马小定理只要求底数 2 与模 \(mod\) 互质,指数模 \(mod-1\) 即可,不需要 \(mod-1\) 是质数。
五、一句话总结
先钦定 \(K\) 个元素在交集中(\(\binom{N}{K}\)),对剩下元素用容斥保证交集恰好 \(K\) 个,每项是 \(2^{2^{N-i}}-1\)(非空选取子集),指数用费马小定理模 \(mod-1\)。
公式:
BZOJ3622 已经没有什么好害怕的了
一、题目转化
\(n\) 个糖果、\(n\) 个药片两两配对。设:
- \(X\) = "糖果能量 > 药片能量"的组数
- \(Y\) = "药片能量 > 糖果能量"的组数
由题意 \(X+Y=n\),要求 \(X-Y=k\),即:
先判断:若 \(n+k\) 为奇数,或 \(\frac{n+k}{2}>n\),或 \(k<0\),无解,输出 0。否则令 \(m=\frac{n+k}{2}\),问题变成:恰好有 \(m\) 组"糖果 > 药片"的配对方案数。
二、核心技巧:先算"至少"再容斥
直接求"恰好 \(m\) 组"很难。改用经典套路:
第 1 步:排序 + 预处理
把 \(a\)(糖果)和 \(b\)(药片)都升序排序。
设 cnt[i] = 有多少个药片 \(b_j\) 满足 \(b_j<a_i\)(即第 \(i\) 个糖果能"赢"的药片数)。由于排好序,双指针 \(O(n)\) 求出。
第 2 步:DP 求"钦定 \(i\) 组糖果赢"的方案数
设 f[i][j] = 考虑前 \(i\) 个糖果,钦定其中 \(j\) 个糖果配对给"它能赢的药片"(这些药片互不相同)的方案数。
转移:
- 第 \(i\) 个糖果不钦定赢:
f[i][j] += f[i-1][j] - 第 \(i\) 个糖果钦定赢:它能赢
cnt[i]个药片,但前面已用掉 \(j-1\) 个(都是它能赢的药片中的),所以有cnt[i]-(j-1)种选法:
边界 f[0][0]=1。
含义:f[n][j] = 钦定 \(j\) 组"糖果赢"的方案数(其余 \(n-j\) 组随便配,但只钦定了 \(j\) 组,剩余未指定)。
第 3 步:把"钦定 \(j\) 组"转成"恰好 \(j\) 组"
f[n][j] 中,一个"恰好有 \(t\) 组赢"的完整配对会被计算 \(\binom{t}{j}\) 次(从 \(t\) 组赢里选 \(j\) 组钦定)。所以:
其中 \(g[t]\) = 恰好 \(t\) 组赢的方案数。由二项式反演:
但注意 f[n][j] 只钦定了 \(j\) 组,剩下 \(n-j\) 组任意配对的方案数还没乘!完整写:
设 h[j] = 钦定 \(j\) 组赢、其余 \(n-j\) 组任意配的方案数 = \(f[n][j]\cdot(n-j)!\)。
则:
反演:
三、代码实现要点
// 排序
sort(a+1,a+n+1); sort(b+1,b+n+1);
// cnt[i]: 双指针
for(int i=1,p=0;i<=n;i++){
while(p<n&&b[p+1]<a[i])p++;
cnt[i]=p;
}
// DP
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=i;j++){
f[i][j]=f[i-1][j];
if(j>0)f[i][j]=(f[i][j]+f[i-1][j-1]*(cnt[i]-j+1))%mod;
}
// 反演
for(int j=m;j<=n;j++){
int term=C(j,m)*f[n][j]%mod*fac[n-j]%mod;
if((j-m)%2)ans=(ans-term+mod)%mod;
else ans=(ans+term)%mod;
}
四、一句话总结
排序后 DP 求"钦定 \(j\) 组糖果赢"的方案数 \(f[n][j]\),乘以 \((n-j)!\) 得"至少 \(j\) 组"的计数,再用二项式反演得到"恰好 \(m=\frac{n+k}{2}\) 组"的答案。
[NOI Online #2 提高组] 游戏
一、把游戏翻译成"匹配"
\(n=2m\) 个点,A、B 各 \(m\) 个。游戏 \(m\) 回合,每回合 A 选一个、B 选一个,配成一对。
一个完整的游戏过程 = 一个 A 到 B 的双射(完美匹配)。因为 A 的每个点恰好被选一次、B 的每个点也恰好被选一次,A 的每个点对应 B 的一个点。
一回合"非平局"⟺ 这对点有祖先-后代关系:
- A 的点在 B 点的子树内 → A 赢(非平局)
- B 的点在 A 点的子树内 → B 赢(非平局)
两者不可能同时发生(两个不同点,一个在另一个子树内,反过来不成立)。所以:
非平局对数 = 匹配中有祖先-后代关系的对数。
题目要求:对每个 \(k=0..m\),输出恰好有 \(k\) 对祖先-后代关系的匹配数。
二、套路:先"钦定",再"反演"
直接数"恰好 \(k\) 对"很难。标准套路分两步:
- 钦定:先数"钦定 \(j\) 对是祖先-后代关系"的方案数
f[1][j](其余点不管)。 - 反演:用二项式反演把"钦定 \(j\) 对"转成"恰好 \(k\) 对"。
三、第 1 步:树形背包求 f[1][j]
定义
f[u][j] = 在 \(u\) 的子树内,钦定 \(j\) 对祖先-后代关系(每对一个 A、一个 B,且一个是另一个的祖先)的方案数。其余点先不配对。
合并孩子(树上背包)
把每个孩子 \(v\) 的 DP 合并进 \(u\):
for(int j=sz[u]/2;j>=0;j--)
for(int k=1;k<=sz[v]/2;k++)
f[u][j+k]=(f[u][j+k]+f[u][j]*f[v][k])%mod;
sz[u] 是子树点数,最多 \(\lfloor sz[u]/2\rfloor\) 对。
在 \(u\) 处新增一对
合并完孩子后,考虑让 \(u\) 参与一对。若 \(u\) 属 A,它和子树内某个还没被用的 B 点配对——\(u\) 是祖先,B 点在子树内,合法!
子树内 B 点总数是 cntB[u]。已经钦定了 \(j-1\) 对(每对用掉一个 B),当前这对还要再占一个 B,所以可用 B 点数是:
递推:
\(u\) 属 B 时用 cntA[u] 同理。
边界
f[u][0]=1(一对都不钦定,方案数 1)。
这样 f[1][j] = 整棵树钦定 \(j\) 对的方案数。
四、第 2 步:从"钦定"到"恰好"——最关键的一步
f[1][j] 到底数了什么?
f[1][j] 只钦定了 \(j\) 对。剩下没配对的点完全没管。
此时用了 \(j\) 个 A、\(j\) 个 B,剩下:
- A 剩 \(m-j\) 个
- B 剩 \(m-j\) 个
要把这些剩下的 A 和 B 一一配对(双射),方案数是:
为什么不是 \((n-2j)!=(2m-2j)!\)?
\((2m-2j)!\) 是把剩下 \(2m-2j\) 个点全部排列的方案数。但这里 A 点和 B 点是分开的两类,配对必须是"A 配 B"。把 A 点排列有 \((m-j)!\) 种,B 点排列有 \((m-j)!\) 种,但配对方案只取决于"哪个 A 对哪个 B",固定 A 的顺序后 B 的排列决定配对,所以是 \((m-j)!\) 种。
简单例子:剩 2 个 A(A1,A2)、2 个 B(B1,B2),配对方案是 2! = 2 种:(A1-B1,A2-B2) 和 (A1-B2,A2-B1)。而全排列 \(4!=24\) 显然太多。
所以定义:
\(h[j]\) = "钦定 \(j\) 对祖先关系,其余任意配对"的方案数。
二项式反演
设 \(g[t]\) = 恰好 \(t\) 对祖先关系的完美匹配数。
一个恰好 \(t\) 对的匹配,从中选 \(j\) 对钦定,有 \(\binom{t}{j}\) 种选法。所以:
二项式反演:
对每个 \(k=0..m\) 输出 \(g[k]\)。
五、完整正确代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=5005;
const int mod=998244353;
const int inf=1e18;
int n,m;
char s[N];
vector<int>g[N];
int sz[N],cntA[N],cntB[N],f[N][N],tmp[N];
int fac[N],inv[N];
int quick(int x,int y)
{
int res=1;
while(y)
{
if(y&1)res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int C(int n,int m)
{
if(n<m||n<0||m<0)return 0;
return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
void dfs(int u,int fa)
{
sz[u]=1;
if(s[u]=='0')cntA[u]=1;
else cntB[u]=1;
f[u][0]=1;
for(int i=0;i<(int)g[u].size();i++)
{
int v=g[u][i];
if(v==fa)continue;
dfs(v,u);
for(int j=0;j<=sz[u]/2+1;j++)tmp[j]=f[u][j];
for(int j=sz[u]/2+1;j>=0;j--)f[u][j]=0;
for(int j=0;j<=sz[u]/2+1;j++)
{
if(!tmp[j])continue;
for(int k=0;k<=sz[v]/2+1;k++)
{
if(!f[v][k])continue;
f[u][j+k]=(f[u][j+k]+tmp[j]*f[v][k])%mod;
}
}
sz[u]+=sz[v];
cntA[u]+=cntA[v];
cntB[u]+=cntB[v];
}
int lim=sz[u]/2;
if(s[u]=='0')
{
for(int j=lim;j>=1;j--)
{
int use=cntB[u]-(j-1);
if(use>0)f[u][j]=(f[u][j]+f[u][j-1]*use%mod)%mod;
}
}
else
{
for(int j=lim;j>=1;j--)
{
int use=cntA[u]-(j-1);
if(use>0)f[u][j]=(f[u][j]+f[u][j-1]*use%mod)%mod;
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
m=n/2;
cin>>(s+1);
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
inv[n]=quick(fac[n],mod-2);
for(int i=n-1;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
dfs(1,0);
for(int k=0;k<=m;k++)
{
int ans=0;
for(int j=k;j<=m;j++)
{
int h=f[1][j]*fac[m-j]%mod;
int term=C(j,k)*h%mod;
if((j-k)&1)ans=(ans-term+mod)%mod;
else ans=(ans+term)%mod;
}
cout<<ans<<"\n";
}
return 0;
}
七、为什么"钦定"能这样数(正确性直觉)
有人会问:f[1][j] 钦定 \(j\) 对时,会不会重复计数?
不会。 因为 f[1][j] 是"选出 \(j\) 对具体的有序配对(哪个 A 配哪个 B)",每对是确定的。后续乘 \((m-j)!\) 是给剩余点做双射。一个完整匹配如果恰好有 \(t\) 对祖先关系,它被 \(h[j]\) 统计到 \(\binom{t}{j}\) 次(从 \(t\) 对里选 \(j\) 对当作"钦定"的),这正是二项式反演需要的系数。
八、一句话总结
树形背包数"钦定 \(j\) 对祖先关系"的方案 f[1][j] → 剩余 \(m-j\) 个 A 与 \(m-j\) 个 B 做双射,乘 \((m-j)!\) → 二项式反演得恰好 \(k\) 对 → 对每个 \(k\) 输出。
核心坑:剩余点是 A 配 B 的双射,乘 \((m-j)!\),不是 \((2m-2j)!\)。
[CF285E]Positions in Permutations
一、题意转化
排列 \(p_1\dots p_n\) 可看成位置 \(1..n\) 与值 \(1..n\) 的完美匹配。
位置 \(i\) 是好位置 \(\iff\) 它配的值是 \(i-1\)(往下)或 \(i+1\)(往上)。
求恰好 \(k\) 个好位置的匹配数。
二、冲突分析
值 \(v\) 作为好值,只能被两个位置使用:
- 位置 \(v-1\) 往上(\(p_{v-1}=v\))
- 位置 \(v+1\) 往下(\(p_{v+1}=v\))
所以值 \(v\) 被距离 2 的两个位置争抢。
画图(\(n=5\)):
位置: 1 2 3 4 5
好值: 2 1,3 2,4 3,5 4
- 值 2:位置 1(往上)、位置 3(往下)抢
- 值 3:位置 2(往上)、位置 4(往下)抢
- 值 4:位置 3(往上)、位置 5(往下)抢
关键结论:
- 距离 1 不冲突:位置 \(i\) 往下用 \(i-1\),位置 \(i-1\) 往上用 \(i\),值不同,可同时成立(如 \((2,1)\) 两个好位置)。
- 距离 2 冲突:位置 \(i\) 往下用 \(i-1\),位置 \(i-2\) 往上也用 \(i-1\),抢同一个值。
三、4 状态 DP
3.1 状态设计
处理到位置 \(i\) 后,为了判断未来是否冲突,必须记住两件事:
| 变量 | 含义 |
|---|---|
a |
值 \(i\) 是否被位置 \(i-1\) 使用(位置 \(i-1\) 往上) |
b |
值 \(i+1\) 是否被位置 \(i\) 使用(位置 \(i\) 往上) |
为什么记 b:下一步处理位置 \(i+1\) 时,判断它能否往下(用值 \(i\))要看 a;而位置 \(i+1\) 若往上(用值 \(i+2\)),这个信息要传给位置 \(i+2\),所以现在就要把"值 \(i+1\) 是否被位置 \(i\) 用"记成 b,下一步它变成新的 a。
= 处理完前 \(i\) 个位置,钦定 \(j\) 个好位置,状态为 \((a,b)\) 的方案数。
3.2 转移(从位置 \(i\) 到 \(i+1\))
从 dp[i][j][a][b] 出发,决定位置 \(i+1\) 怎么放:
决策 1:位置 \(i+1\) 不放好值
新状态:值 \(i+1\) 是否被位置 \(i\) 用 = b;值 \(i+2\) 是否被位置 \(i+1\) 用 = 0。
决策 2:位置 \(i+1\) 往下,用值 \(i\)
- 前提:值 \(i\) 没被占,即 \(a=0\);且 \(i\ge1\)(值 \(i\) 存在)。
- 新状态:\(a'=b\),\(b'=0\)。
- 好位置数 \(j+1\)。
决策 3:位置 \(i+1\) 往上,用值 \(i+2\)
- 前提:\(i+2\le n\)(值 \(i+2\) 存在)。
- 新状态:\(a'=b\),\(b'=1\)(位置 \(i+1\) 占了值 \(i+2\))。
- 好位置数 \(j+1\)。
注意:新状态的 a' 永远等于旧 b,因为"值 \(i+1\) 是否被位置 \(i\) 用"这个信息在新一轮成为"值 \((i+1)\) 是否被位置 \((i+1)-1\) 用"。
3.3 边界
- 初始
dp[0][0][0][0]=1。 - 位置 1 不能往下:决策 2 要求 \(i\ge1\),处理位置 1 时 \(i=0\),自动排除。
- 位置 \(n\) 不能往上:决策 3 要求 \(i+2\le n\),处理位置 \(n\) 时 \(i=n-1\),\(n+1\le n\) 不成立,自动排除。
3.4 取 f[j]
f[j] = 钦定 \(j\) 个好位置的方案数(其余位置不管)。
四、二项式反演
4.1 乘 \((n-j)!\)
f[j] 只钦定 \(j\) 个好位置(已指定这 \(j\) 个位置放什么值)。剩下 \(n-j\) 个位置与 \(n-j\) 个未被使用的值做任意双射,方案数 \((n-j)!\)。
= "至少钦定 \(j\) 个好位置"的计数。
4.2 反演公式
恰好 \(t\) 个好位置的排列,会被 \(h[j]\) 统计 \(\binom{t}{j}\) 次(从 \(t\) 个好位置里选 \(j\) 个当"钦定"):
反演:
五、完整代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=1005;
const int mod=1e9+7;
const int inf=1e18;
int dp[2][N][2][2],f[N],fac[N],inv[N];
int quick(int x,int y)
{
int res=1;
while(y)
{
if(y&1)res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
int C(int n,int m)
{
if(n<m||n<0||m<0)return 0;
return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,k;
cin>>n>>k;
int cur=0;
dp[0][0][0][0]=1;
for(int i=1;i<=n;i++)
{
int nxt=cur^1;
memset(dp[nxt],0,sizeof(dp[nxt]));
for(int j=0;j<=i;j++)
for(int a=0;a<2;a++)
for(int b=0;b<2;b++)
{
int val=dp[cur][j][a][b];
if(!val)continue;
// 不选好值
dp[nxt][j][b][0]=(dp[nxt][j][b][0]+val)%mod;
// 往下:用值 i-1,要求 a=0 且 i>=2
if(a==0&&i>=2)
dp[nxt][j+1][b][0]=(dp[nxt][j+1][b][0]+val)%mod;
// 往上:用值 i+1,要求 i+1<=n
if(i+1<=n)
dp[nxt][j+1][b][1]=(dp[nxt][j+1][b][1]+val)%mod;
}
cur=nxt;
}
for(int j=0;j<=n;j++)
for(int a=0;a<2;a++)
for(int b=0;b<2;b++)
f[j]=(f[j]+dp[cur][j][a][b])%mod;
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod;
inv[n]=quick(fac[n],mod-2);
for(int i=n-1;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
int ans=0;
for(int j=k;j<=n;j++)
{
int h=f[j]*fac[n-j]%mod;
int term=C(j,k)*h%mod;
if((j-k)&1)ans=(ans-term+mod)%mod;
else ans=(ans+term)%mod;
}
cout<<ans;
return 0;
}
六、一句话总结
好值被距离 2 的位置争抢,用 4 状态 (值 i 是否被位置 i-1 用, 值 i+1 是否被位置 i 用) DP 求出钦定 \(j\) 个好位置的方案 f[j],乘 \((n-j)!\),二项式反演得恰好 \(k\) 个。
[THUPC 2019]过河卒二 / chess
一、题意
\(N\times M\) 棋盘,起点 \((1,1)\)。每步可走:
- 向上:\((x+1,y)\)
- 向右:\((x,y+1)\)
- 向右上:\((x+1,y+1)\)
走出棋盘(越过上边界或右边界)即成功。有 \(K\le20\) 个障碍不能经过。求方案数模 \(59393\)。
\(N\le10^9\),\(M\le10^5\),\(K\le20\)。
二、\(W(dx,dy)\):三方向走格计数
从某点走 \(dx\) 步纵向、\(dy\) 步横向(允许右上步)的方案数。设右上走 \(k\) 步:
为什么 \(2^k\):每对"上+右"可交换顺序,对应 \(2\) 种排列。
int W(int dx,int dy)
{
if(dx<0||dy<0)return 0;
if(dy>dx)swap(dx,dy);
int res=0;
for(int k=0;k<=dy;k++)
{
res+=C_mod(dx,k)*C_mod(dy,k)%mod*pow2[k]%mod;
if(res>=mod)res-=mod;
}
return res;
}
\(dx\) 可达 \(10^9\),但 \(k\) 只到 \(\min(dx,dy)\le M\le10^5\),循环可控;\(\binom{dx}{k}\) 用 Lucas 算。
三、\(F(x,y)\):从 \((x,y)\) 走出棋盘
令 \(A=N-x\),\(B=M-y\)(到上边界、右边界的距离)。
3.1 枚举最后一步
- 走到 \((N,y')\) 再出去:\(y'=y..M-1\) 各有 2 种(上、右上);\(y'=M\) 角落有 3 种(上、右、右上)。
- 走到 \((x',M)\) 再出去:\(x'=x..N-1\) 各有 2 种;\(x'=N\) 角落有 3 种。
角落 \((N,M)\) 在两类中都被算,各贡献 3 次,实际应只算 3 次,多算 3 次需扣掉。
整理得:
3.2 Hockey-stick 化简
所以:
int F(int x,int y)
{
int A=N-x;
int B=M-y;
int S=0,T=0;
for(int k=0;k<=B;k++)
{
S=(S+C_mod(A,k)*pow2[k]%mod*C_mod(B+1,k+1))%mod;
T=(T+C_mod(B,k)*pow2[k]%mod*C_mod(A,k+1))%mod;
}
int WAB=W(A,B);
int res=(2*S+2*T+WAB)%mod;
return res;
}
注意 A=N-x、B=M-y 中的 N,M 必须是输入的棋盘大小(全局变量),不能是无关常量。
四、障碍容斥
障碍按 \(x\) 升序(同 \(x\) 按 \(y\))排序。
4.1 到障碍不经过其他障碍
\(p_j\le p_i\) 指 \(p_j.x\le p_i.x\) 且 \(p_j.y\le p_i.y\)。
for(int i=1;i<=K;i++)
{
f[i]=W(p[i].x-1,p[i].y-1);
for(int j=1;j<i;j++)
if(p[j].x<=p[i].x&&p[j].y<=p[i].y)
{
int w=W(p[i].x-p[j].x,p[i].y-p[j].y);
f[i]=(f[i]-f[j]*w%mod+mod)%mod;
}
}
4.2 答案
int ans=F(1,1);
for(int i=1;i<=K;i++)
ans=(ans-f[i]*F(p[i].x,p[i].y)%mod+mod)%mod;
五、Lucas 定理
\(p=59393\) 是质数,\(N\le10^9<p^2\)。\(\binom{n}{m}\bmod p\) 用 Lucas:
第二因子中 \(\lfloor m/p\rfloor\in\{0,1\}\)(因 \(m\le10^5\)):
- 若 \(\lfloor m/p\rfloor=0\):因子为 1
- 若 \(=1\):因子为 \(\lfloor n/p\rfloor\)
inline int C_mod(int n,int m)
{
if(m<0||m>n)return 0;
int n0=n%mod,n1=n/mod;
int m0=m%mod,m1=m/mod;
if(m1>n1)return 0;
int res=1;
if(m1==1)
{
if(n1==0)return 0;
res=n1%mod;
}
if(m0>n0)return 0;
res=res*fac[n0]%mod*invfac[m0]%mod*invfac[n0-m0]%mod;
return res;
}
预处理 \(0..p-1\) 的 fac 和 invfac。
六、完整正确代码
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define CCF
using namespace std;
const int mod=59393;
const int inf=1e18;
const int MAXM=100005;
int fac[mod],invfac[mod],pow2[MAXM];
int N,M,K;
int quick(int a,int b)
{
int res=1;
while(b)
{
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
inline int C_small(int n,int m)
{
if(m<0||m>n)return 0;
return fac[n]*invfac[m]%mod*invfac[n-m]%mod;
}
inline int C_mod(int n,int m)
{
if(m<0||m>n)return 0;
int n0=n%mod,n1=n/mod;
int m0=m%mod,m1=m/mod;
if(m1>n1)return 0;
int res=1;
if(m1==1)
{
if(n1==0)return 0;
res=n1%mod;
}
if(m0>n0)return 0;
res=res*fac[n0]%mod*invfac[m0]%mod*invfac[n0-m0]%mod;
return res;
}
int W(int dx,int dy)
{
if(dx<0||dy<0)return 0;
if(dy>dx)swap(dx,dy);
int res=0;
for(int k=0;k<=dy;k++)
{
res+=C_mod(dx,k)*C_mod(dy,k)%mod*pow2[k]%mod;
if(res>=mod)res-=mod;
}
return res;
}
int F(int x,int y)
{
int A=N-x;
int B=M-y;
int S=0,T=0;
for(int k=0;k<=B;k++)
{
S=(S+C_mod(A,k)*pow2[k]%mod*C_mod(B+1,k+1))%mod;
T=(T+C_mod(B,k)*pow2[k]%mod*C_mod(A,k+1))%mod;
}
int WAB=W(A,B);
int res=(2*S+2*T+WAB)%mod;
return res;
}
struct Point
{
int x,y;
bool operator<(const Point &o)const
{
if(x!=o.x)return x<o.x;
return y<o.y;
}
};
Point p[25];
int f[25];
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
#ifndef CCF
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
cin>>N>>M>>K;
for(int i=1;i<=K;i++)
{
cin>>p[i].x>>p[i].y;
}
fac[0]=1;
for(int i=1;i<mod;i++)fac[i]=fac[i-1]*i%mod;
invfac[mod-1]=quick(fac[mod-1],mod-2);
for(int i=mod-2;i>=0;i--)invfac[i]=invfac[i+1]*(i+1)%mod;
pow2[0]=1;
for(int i=1;i<MAXM;i++)pow2[i]=pow2[i-1]*2%mod;
sort(p+1,p+K+1);
for(int i=1;i<=K;i++)
{
f[i]=W(p[i].x-1,p[i].y-1);
for(int j=1;j<i;j++)
{
if(p[j].x<=p[i].x&&p[j].y<=p[i].y)
{
int w=W(p[i].x-p[j].x,p[i].y-p[j].y);
f[i]=(f[i]-f[j]*w%mod+mod)%mod;
}
}
}
int ans=F(1,1);
for(int i=1;i<=K;i++)
{
int fi=F(p[i].x,p[i].y);
ans=(ans-f[i]*fi%mod+mod)%mod;
}
cout<<ans%mod<<"\n";
return 0;
}
[HAOI2017]方案数
斯特林数
一、 题意
三维空间,起点 \((0,0,0)\),终点 \((n,m,r)\)。每步把某一个坐标换成它的超集(按位包含):
求不经过障碍的路径数,模 \(998244353\)。\(n,m,r\le10^{18}\),障碍 \(o\le10^4\)。
二、关键观察:三维独立 + 交错
每步只动一个坐标,所以:
一条三维路径 = 一条 \(x\) 方向的一维路径 + 一条 \(y\) 方向的一维路径 + 一条 \(z\) 方向的一维路径,三条路径的步骤交错排列。
这提示我们用"分步/交错"的组合方法。
三、一维:从 \(0\) 到 \(x\)(最重要的一步)
3.1 一维问题
从 \(0\) 走到 \(x\),每步把当前数 \(u\) 换成 \(u'\),满足 \(u\subseteq u'\) 且 \(u'\subseteq x\)(不能超出目标)。求方案数。
3.2 等价刻画
设 \(x\) 的二进制里有 \(k\) 个 \(1\)(即 \(k=\operatorname{popcount}(x)\))。把这些 \(1\) 位记作"目标位"。
一维路径的每一步,相当于:把当前还没设置的目标位里,挑一个非空子集一次性全部设上。
所以"从 \(0\) 到 \(x\)"等价于:
把 \(k\) 个目标位分成若干个有序非空块(块的顺序 = 操作顺序,每块一次操作全部设上)。
3.3 方案数
设分了 \(a\) 步(\(a\) 个块)。把 \(k\) 个不同元素分成 \(a\) 个非空无序块的方案数是第二类斯特林数 \(S(k,a)\)。再给块排顺序,乘 \(a!\):
四、三维交错:除以 \(a!b!c!\) 的抵消
4.1 交错数
\(x\) 方向用 \(a\) 步,\(y\) 方向 \(b\) 步,\(z\) 方向 \(c\) 步,共 \(a+b+c\) 步。把这三个方向的操作交错排列的方案数(同类步骤内部顺序已确定,只需选位置)是多重组合:
4.2 合并一维与交错
一维内部已经乘了 \(a!\)(块排序),交错时又除以 \(a!\),恰好抵消!
这就是为什么最终公式里只有 \(S\),没有 \(a!\)。 这是本题最容易错的地方——很多人以为一维系数是 \(a!S(k,a)\),代入后忘记交错时除以 \(a!\),或者记错了。
五、公式的直觉理解
\(H(k_1,k_2,k_3)\) 的物理意义:
- \(S(k_1,a)S(k_2,b)S(k_3,c)\):三个维度各自把目标位分成 \(a,b,c\) 个无序块。
- \((a+b+c)!\):把 \(a+b+c\) 个块排成全序(作为操作序列)。
为什么用无序块 + 全排列,而不是有序块?
因为三个方向的块混在一起全排列时,同方向的块谁先谁后也被决定了。这样"块的内容"和"块的位置"分开处理,公式最干净。如果一维用有序块(\(a!S\)),那么交错时必须除以 \(a!\) 才能去重。
六、预处理 \(H\) 表
\(k_i\le60\)(\(10^{18}<2^{60}\)),\(H\) 大小 \(61^3\approx22.7\) 万。
直接三重求和 \(a,b,c\) 是 \(O(60^6)\) 太慢。用卷积分层:
6.1 合并后两维
6.2 与阶乘卷积
6.3 合并第一维
总运算量约 \(60^4\approx1.3\times10^7\)(每层常数很小),可过。
// 第二类斯特林数
S[0][0]=1;
for(int n=1;n<MAXB;n++)
for(int k=1;k<=n;k++)
S[n][k]=(S[n-1][k-1]+k*S[n-1][k])%MOD;
// G
for(int k2=0;k2<MAXB;k2++)
for(int k3=0;k3<MAXB;k3++)
for(int b=0;b<=k2;b++)
for(int c=0;c<=k3;c++)
G[k2][k3][b+c]+=S[k2][b]*S[k3][c];
// P
for(int k2...;k3...;a...)
for(int t=0;t<=k2+k3;t++)
P[k2][k3][a]+=G[k2][k3][t]*fact[a+t];
// H
for(int k1...;k2...;k3...)
for(int a=0;a<=k1;a++)
H[k1][k2][k3]+=S[k1][a]*P[k2][k3][a];
七、障碍容斥
7.1 筛障碍
障碍 \((x,y,z)\) 只有在 \(x\subseteq n,\ y\subseteq m,\ z\subseteq r\) 时才可能出现在某条合法路径上(否则从它出发到不了终点)。其余直接丢弃。
if((x&n)==x && (y&m)==y && (z&r)==z)
vec.push_back(...);
7.2 排序
按 \((x,y,z)\) 升序排序。重要性质:若 \(A\subseteq B\) 且 \(A\ne B\),则 \(A\) 的坐标逐项小于等于 \(B\),字典序一定 \(A<B\)。所以所有能到 \(B\) 的点都排在 \(B\) 前面。
7.3 DP
把终点也加入 vec(终点不是障碍,只是作为求答案的对象),排序后它一定是最后一个。
全集:\(H(\operatorname{popcount}(p_i))\)。
减去:经过某个障碍 \(j<i\)(\(p_j\subseteq p_i\))的方案。从 \(p_j\) 到 \(p_i\) 的"增量"是逐维 popcount 之差:
(因为 \(p_j\subseteq p_i\),\(p_i\) 的 1 位比 \(p_j\) 多出的部分正好是还要设置的位数。)
注意这里 \(f[j]\) 已经是"到 \(j\) 不经过更早障碍"的方案,所以减去的每项都代表"第一次经过的障碍是 \(j\)",正好容斥,不重不漏。
for(int i=0;i<sz;i++){
f[i]=H[pcx][pcy][pcz];
for(int j=0;j<i;j++){
if((v.x&~u.x)==0 && (v.y&~u.y)==0 && (v.z&~u.z)==0){
int d1=pcx-v.pcx, d2=pcy-v.pcy, d3=pcz-v.pcz;
f[i]=(f[i]-f[j]*H[d1][d2][d3]%MOD+MOD)%MOD;
}
}
}
7.4 答案
f[sz-1](终点在最后)。
八、 一句话总结
三维独立 → 每个维度把 popcount 个目标位分成块(斯特林数)→ 三方向块全排列 \((a+b+c)!\)(\(a!\) 抵消)→ 预处理 \(H\) 表 → 障碍按字典序排序后容斥 DP,\(f[i]=H(\operatorname{pc}(p_i))-\sum_{j\subseteq i}f[j]H(\Delta)\),答案 \(f[\text{终点}]\)。
容斥
一、题意回顾
三维空间,起点 \((0,0,0)\),终点 \((n,m,r)\)。每步把某一个坐标换成它的按位超集(\(a\subseteq b\iff a\land b=a\))。有 \(o\) 个障碍不能经过。求不经过障碍的方案数模 \(998244353\)。
\(n,m,r\le10^{18}\),\(o\le10^4\)。
二、为什么三维可以"拆开理解"
每步只动一个坐标,所以:
一条三维路径 = 三条一维路径(\(x\) 方向、\(y\) 方向、\(z\) 方向)交错排列。
关键:每一步动哪个坐标是任意的,没有固定顺序。所以"交错"是本质。
传统做法用斯特林数 + 多重排列显式计算交错,但那样容易把系数搞错(多乘/漏除 \(a!\))。下面用直接递推绕开这个坑。
三、一维问题的递推
3.1 一维在做什么
从 \(0\) 走到 \(x\),每步把当前数 \(u\) 换成 \(u'\)(\(u\subseteq u'\subseteq x\))。
等价地:把 \(x\) 的二进制里那些 \(1\) 位(共 \(k=\operatorname{popcount}(x)\) 个)一批一批地设置上,每批挑还没设的若干个位一次性设。
3.2 定义
\(f[k]\) = 还剩 \(k\) 个位要设置时,到目标的一维方案数。
3.3 递推
第一步从 \(k\) 个待设位里挑 \(j\) 个(\(1\le j\le k\))一次性设上,选法 \(\binom{k}{j}\),剩下 \(k-j\) 个位继续:
这就是 Fubini 数,不用显式算斯特林数。
验证:\(f[1]=\binom11f[0]=1\);\(f[2]=\binom21f[1]+\binom22f[0]=2+1=3\)(即 \(00\to11\)、\(00\to01\to11\)、\(00\to10\to11\))。
四、三维问题的递推(核心)
4.1 定义
= 三维空间中,当前点到终点,\(x,y,z\) 方向分别还有 \(k_1,k_2,k_3\) 个位要设置时的方案数。
注意:\(H\) 只依赖剩余的位数,不依赖具体的数是多少!因为按位包含的操作只关心"哪些位还没设",而位数相同的目标,结构完全同构。
4.2 第一步枚举
从状态 \((k_1,k_2,k_3)\) 出发,第一步只能动一个坐标:
- 动 \(x\):从 \(k_1\) 个待设位里挑 \(j\) 个(\(1\le j\le k_1\)),选法 \(\binom{k_1}{j}\),进入状态 \((k_1-j,k_2,k_3)\)。
- 动 \(y\):\(\binom{k_2}{j}\),进入 \((k_1,k_2-j,k_3)\)。
- 动 \(z\):\(\binom{k_3}{j}\),进入 \((k_1,k_2,k_3-j)\)。
所以:
边界:\(H[0][0][0]=1\)。
4.3 为什么这样自动处理了"交错"
传统做法要显式算"\(a\) 步 \(x\)、\(b\) 步 \(y\)、\(c\) 步 \(z\) 的全排列数"。这里不需要:
- 每次递推"第一步动哪个坐标",就是一次交错的决策。
- 递归下去自然枚举了所有交错顺序。
- 没有任何"除以 \(a!\)"的麻烦,因为每一步是"这个位置动哪个坐标",天然区分。
4.4 递推顺序
每项里某个坐标严格减小,所以按 \(k_1\) 升序、再 \(k_2\)、再 \(k_3\) 顺序算,被依赖的状态一定已经算过。
H[0][0][0]=1;
for(int k1=0;k1<MAXB;k1++)
for(int k2=0;k2<MAXB;k2++)
for(int k3=0;k3<MAXB;k3++)
{
if(k1==0&&k2==0&&k3==0)continue;
int sum=0;
for(int j=1;j<=k1;j++)
sum=(sum+C[k1][j]*H[k1-j][k2][k3])%mod;
for(int j=1;j<=k2;j++)
sum=(sum+C[k2][j]*H[k1][k2-j][k3])%mod;
for(int j=1;j<=k3;j++)
sum=(sum+C[k3][j]*H[k1][k2][k3-j])%mod;
H[k1][k2][k3]=sum;
}
五、\(P(a\to b)\) 与 \(H\) 的对应
从点 \(a\) 到点 \(b\)(\(a\subseteq b\),即 \(b\) 是 \(a\) 的超集)的路径数:
其中 \(k_i=\operatorname{popcount}(b_i)-\operatorname{popcount}(a_i)\)。
为什么:因为 \(a\subseteq b\),\(a\) 的每个 1 位在 \(b\) 里也是 1。所以"从 \(a\) 到 \(b\) 要新设置的位数"就是 \(\operatorname{pc}(b_i)-\operatorname{pc}(a_i)\)。而 \(H\) 只关心这个位数。
六、障碍容斥(不变)
6.1 筛障碍
障碍 \(p=(x,y,z)\) 若 \(p\nsubseteq T=(n,m,r)\),则路径走不到它(或从它走不到终点),直接丢弃:
if((x&n)==x&&(y&m)==y&&(z&r)==z)
vec.push_back(...);
6.2 排序
按 \((x,y,z)\) 字典序升序。性质:若 \(p_j\subseteq p_i\) 且 \(p_j\ne p_i\),则 \(p_j\) 字典序 \(<p_i\)。所以能到达 \(p_i\) 的障碍全在它前面。
6.3 DP
把终点也加入 vec(作为最后一个点)。定义:
其中 \(\Delta_{ji}\) 是逐维 popcount 差。
容斥直觉:从 \(O\) 到 \(p_i\) 的所有路径,按"第一个经过的障碍"分类。第一个障碍为 \(p_j\) 的路径数 = \(f[j]\cdot P(p_j\to p_i)\)(\(p_j\) 之前不经过障碍,\(p_j\) 之后随便走)。减去所有这类,剩下的就是不经过任何障碍的路径。
答案 = \(f[\text{终点}]\)。
七、完整代码
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define CCF
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
const int MAXB=61;
int C[MAXB][MAXB];
int H[MAXB][MAXB][MAXB];
struct Node
{
int x,y,z;
int px,py,pz;
bool operator<(const Node &o)const
{
if(x!=o.x)return x<o.x;
if(y!=o.y)return y<o.y;
return z<o.z;
}
};
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
#ifndef CCF
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
C[0][0]=1;
for(int i=1;i<MAXB;i++)
{
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
H[0][0][0]=1;
for(int k1=0;k1<MAXB;k1++)
for(int k2=0;k2<MAXB;k2++)
for(int k3=0;k3<MAXB;k3++)
{
if(k1==0&&k2==0&&k3==0)continue;
int sum=0;
for(int j=1;j<=k1;j++)
sum=(sum+C[k1][j]*H[k1-j][k2][k3])%mod;
for(int j=1;j<=k2;j++)
sum=(sum+C[k2][j]*H[k1][k2-j][k3])%mod;
for(int j=1;j<=k3;j++)
sum=(sum+C[k3][j]*H[k1][k2][k3-j])%mod;
H[k1][k2][k3]=sum;
}
int n,m,r;
cin>>n>>m>>r;
int o;
cin>>o;
vector<Node> vec;
for(int i=0;i<o;i++)
{
int x,y,z;
cin>>x>>y>>z;
if((x&n)==x&&(y&m)==y&&(z&r)==z)
vec.push_back({x,y,z,__builtin_popcountll(x),__builtin_popcountll(y),__builtin_popcountll(z)});
}
vec.push_back({n,m,r,__builtin_popcountll(n),__builtin_popcountll(m),__builtin_popcountll(r)});
sort(vec.be,vec.en);
int sz=vec.size();
vector<int> f(sz,0);
for(int i=0;i<sz;i++)
{
Node &u=vec[i];
f[i]=H[u.px][u.py][u.pz];
for(int j=0;j<i;j++)
{
Node &v=vec[j];
if((v.x&~u.x)==0&&(v.y&~u.y)==0&&(v.z&~u.z)==0)
{
int d1=u.px-v.px,d2=u.py-v.py,d3=u.pz-v.pz;
f[i]=(f[i]-f[j]*H[d1][d2][d3]%mod+mod)%mod;
}
}
}
cout<<f[sz-1]%mod<<"\n";
return 0;
}
[清华集训 2012] 串珠子
一、 题意
\(n\) 个互不相同的点。对每对 \((i,j)\):
- 不连边(1 种选择)
- 或从 \(c_{i,j}\) 根不同颜色绳子中选一根连边(\(c_{i,j}\) 种选择)
所以每条边有 \(c_{i,j}+1\) 种选择。求整张图连通的方案数,模 \(10^9+7\)。
\(n\le16\)。
二、核心思路:连通 = 任意 − 不连通
2.1 先算"任意连边"
定义:
边与边独立,所以:
注意:\(g[S]\) 允许图不连通,它统计的是 \(S\) 内部所有边的任意选择。
2.2 再算"连通"
定义:
答案就是 \(f[\text{全集}]\)。
三、容斥递推(最关键的一步)
3.1 固定一个点
\(S\) 里任取一个点 \(r\)(代码里取最低位 r=S&-S)。任意一个 \(S\) 内部的图,\(r\) 一定属于某个连通块 \(T\):
- \(r\in T\subseteq S\)
- 若图不连通,则 \(T\) 是 \(S\) 的真子集
3.2 按 \(r\) 的连通块分类
"\(r\) 所在连通块恰为 \(T\)"的图数 = :
- \(T\) 内部连通:\(f[T]\)
- \(S\setminus T\) 内部任意:\(g[S\setminus T]\)
- \(T\) 与 \(S\setminus T\) 之间的边必须全断:每条跨边只有"不连"1 种,乘 1,不用额外因子
所以:
3.3 递推式
\(g[S]\) 是全部方案(含不连通),要得到连通方案,减去所有"\(r\) 所在连通块是 \(S\) 真子集"的情况:
正确性:任意一个不连通的图,\(r\) 所在的连通块 \(T\) 是唯一确定的真子集,所以它恰好被减去一次,不重不漏。
五、代码细节
5.1 位运算约定
- 状态 \(S\) 用 0-indexed 位表示:第 \(i\) 位(\(i=0..n-1\))对应点 \(i+1\)。
S&-S取最低位,__builtin_ctz(lb)得到最低位的下标 \(v\)(0-indexed)。full=1<<n,full-1是全 1 状态(全集)。
5.2 预处理 \(g[S]\)
直接对每个 \(S\) 枚举所有内部边是 \(O(n^2 2^n)\),也能过。但更快的做法是递推:
取 \(S\) 的最低位 \(v\),令 prev=S^lb(去掉 \(v\))。新增点 \(v\) 与 prev 中每个点 \(i\) 的边独立选择:
g[0]=1;
for(int S=1;S<full;S++)
{
int lb=S&-S;
int v=__builtin_ctz(lb);
int prev=S^lb;
int prod=1;
for(int i=1;i<=n;i++)
if(prev>>(i-1)&1)
prod=prod*((c[v+1][i]+1)%mod)%mod;
g[S]=g[prev]*prod%mod;
}
注意 \(v\) 是 0-indexed,访问矩阵用 c[v+1][i](1-indexed)。
复杂度 \(O(n2^n)\)。
5.3 容斥求 \(f[S]\)
for(int S=1;S<full;S++)
{
f[S]=g[S];
int r=S&-S;
for(int T=(S-1)&S;T;T=(T-1)&S) // 枚举 S 的所有非空真子集
{
if(T&r) // 只取含 r 的
f[S]=(f[S]-f[T]*g[S^T]%mod+mod)%mod;
}
}
枚举子集的技巧:for(T=(S-1)&S; T; T=(T-1)&S) 按降序枚举 \(S\) 的所有非空真子集。
if(T&r) 只保留含 \(r\) 的子集,避免重复。
复杂度 \(\sum_S 2^{|S|}=3^n\)。\(3^{16}\approx4.3\times10^7\),可过。
5.4 模数
题目是 \(10^9+7\),不是模板默认的 \(998244353\)。
六、完整代码
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define CCF
using namespace std;
const int N=(1<<16)+5;
const int mod=1000000007;
const int inf=1e18;
int c[20][20];
int g[N],f[N];
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
#ifndef CCF
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
int n;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>c[i][j];
int full=1<<n;
g[0]=1;
for(int S=1;S<full;S++)
{
int lb=S&-S;
int v=__builtin_ctz(lb);
int prev=S^lb;
int prod=1;
for(int i=1;i<=n;i++)
if(prev>>(i-1)&1)
prod=prod*((c[v+1][i]+1)%mod)%mod;
g[S]=g[prev]*prod%mod;
}
for(int S=1;S<full;S++)
{
f[S]=g[S];
int r=S&-S;
for(int T=(S-1)&S;T;T=(T-1)&S)
{
if(T&r)
f[S]=(f[S]-f[T]*g[S^T]%mod+mod)%mod;
}
}
cout<<f[full-1]%mod<<"\n";
return 0;
}
七、一句话总结
\(g[S]\) 用递推 \(g[S]=g[S\setminus\{v\}]\prod(c_{v,i}+1)\) 统计 \(S\) 内任意连边方案;\(f[S]=g[S]-\sum_{r\in T\subsetneq S}f[T]g[S\setminus T]\) 按最低点 \(r\) 的连通块容斥;答案 \(f[\text{全集}]\)。复杂度 \(O(3^n)\)。
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23363200

浙公网安备 33010602011771号