基础容斥
清华集训 主旋律
给定一个 \(N\) 个点的有向图,问这个图有多少个强连通子图。保证 \(N\le 15\)。
(也可以尝试去做完全图的情况:\(N\) 个点的强连通图个数。)
解法
(图里可能有 \(N^2\) 条边)
简单问题:给定一个有向图,问有多少个子图是 DAG。
DP: \(f[S]\) 表示在点集 \(S\) 之间删除一些边,使得剩下的图是个DAG的方案数。枚举哪些点的入度为 0,然后递归。
但是这个递归式会算重:枚举的时候只保证了 \(T\) 里的点入读为 0,但是 \(S-T\) 可能也有入度为0的点。使用容斥:
使用这个DP计算,计算量是 \(3^N\)。
原题
一个图如果不强连通的话,那它就可以写成大于 1 个强连通分量连成的一个DAG。
强连通图的个数 = 所有图的个数 - 非强连通的个数。
尝试计算非强连通图的个数,枚举一个 \(T\),让 \(T\) 连成一个强连通图,\(T\) 不接受来自 \(S-T\) 的边。
ALL(S) 表示所有可能的图的个数。
令 \(g[T]\) 表示:
- 把集合 \(T\) 划分成若干块,每一块里连成一个强连通块。如果有奇数个块:产生 1 的贡献;如果有偶数个块:产生 -1 的贡献。
- \(g[T]\) 是所有的贡献之和。
怎么算 \(g\)?给定一个集合 \(S\),固定 \(x\in S\),枚举 x 所在的强连通块:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int in[1<<15],out[1<<15],cnt[1<<15],sum[1<<15],w[1<<15];
long long b[215],f[1<<15],g[1<<15];
void dfs(int S,int T){
if(S&(T-1))dfs(S,S&(T-1));
w[T]=w[T-(T&-T)]+cnt[in[T&-T]&S];
}
const long long md=1e9+7;
int main(){
scanf("%d%d",&n,&m);
b[0]=1;
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y),x--,y--;
in[1<<y]|=1<<x,out[1<<x]|=1<<y;
b[i]=b[i-1]*2%md;
}
for(int i=1;i<(1<<n);i++){
int x=i-(i&-i);cnt[i]=cnt[x]+1,sum[i]=sum[x]+cnt[in[i&-i]&i]+cnt[out[i&-i]&i],f[i]=b[sum[i]];
dfs(i,i);
for(int j=x;j;j=x&(j-1))g[i]=(g[i]-f[i^j]*g[j]%md)%md;
for(int j=i;j;j=i&(j-1))f[i]=(f[i]-b[sum[i]-w[j]]*g[j])%md;
g[i]=(g[i]+f[i])%md;
}
printf("%lld\n",(f[(1<<n)-1]+md)%md);
return 0;
}
ZJOI2016 小星星
给定一个 \(n\) 个点 \(m\) 条边的无向图和一个 \(n\) 个点的树。问把这个树“嵌入”到这个图里的方案数。保证 \(n\le 17\)。
做法 1:在树上DP,写 \(f[x][S][y]\) 表示处理了 x 为根的子树,图里的S这些点已经被用了,\(x\) 被映射到了图里 y 这个点上。
直接做是 \(O(3^n\cdot n^3)\)。可以用 FMT(快速莫比乌斯变换) 优化到 \(O(2^n\cdot n^{3})\).
做法 2:容斥。如果不考虑“每个图里的点都要用”,写一个DP:\(f[x][y]\) 表示 x 为根处理完,x 映射到 y 上的方案数。
这样做的话可能图里有些点没有被用到。枚举哪些点没有被用到,容斥。 \(O(2^n\cdot n^3)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
bool mp[25][25];
int ver[45],ne[45],head[45],tot;
inline void link(int x,int y){
ver[++tot]=y;
ne[tot]=head[x];
head[x]=tot;
}
long long dp[25][25],ans;
vector<int> vec;
void dfs(int x,int fi){
for(auto e:vec)dp[x][e]=1;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x);
for(auto e:vec){
long long tmp=0;
for(auto t:vec){
if(!mp[e][t])continue;
tmp+=dp[u][t];
}dp[x][e]*=tmp;
}
}
}
int cnt[1<<20];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
mp[x][y]=mp[y][x]=1;
}
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}
for(int s=0;s<(1<<n);s++){
vec.clear();cnt[s]=cnt[s>>1]+(s&1);
for(int i=0;i<n;i++)if((s>>i)&1)vec.push_back(i+1);
dfs(1,1);
if((n-cnt[s])&1)for(auto x:vec)ans-=dp[1][x];
else for(auto x:vec)ans+=dp[1][x];
}
printf("%lld",ans);
return 0;
}
SHOI 黑暗前的幻想乡
给定一个 \(n\) 个点的图。每条边被染上了 \(n-1\) 种颜色里的一种。问有多少个生成树包含了 \(n-1\) 个不同颜色的边。\(n\le 17\)。 (author:陈立杰)
如果没有颜色的限制:Matrix Tree 定理,\(O(n^3)\)。
有颜色限制:枚举哪些颜色没有被用到,做容斥。\(O(2^nn^3)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
const long long md=1e9+7;
struct mat{
long long a[17][17];
mat(){memset(a,0,sizeof(a));}
inline long long* operator [](int t){
return a[t];
}
inline long long pwr(long long x,long long y){
long long res=1;
while(y){
if(y&1)res=res*x%md;
x=x*x%md;y>>=1;
}return res;
}
inline long long det(){
long long res=1;
for(int i=1;i<n;i++){
int loc=i;
for(int j=i;j<n;j++)if(a[j][i])loc=j;
if(loc!=i)swap(a[i],a[loc]),res=-res;res=res*a[i][i]%md;
long long tmp=pwr(a[i][i],md-2);
for(int j=i;j<n;j++)a[i][j]=a[i][j]*tmp%md;
for(int j=i+1;j<n;j++){
long long tmp=a[j][i];
for(int t=i;t<n;t++)a[j][t]=(a[j][t]-a[i][t]*tmp)%md;
}
}for(int i=1;i<n;i++)res=res*a[i][i]%md;return res;
}
};
struct node{
int x,y;
node(int _x,int _y){
x=_x;y=_y;
}
};
vector<node> vec[17];
inline long long solve(int s){
mat res;
for(int i=0;i<n;i++){
if((s>>i)&1)for(auto x:vec[i]){
res[x.x][x.x]++;res[x.y][x.y]++;
res[x.x][x.y]--;res[x.y][x.x]--;
}
}return res.det();
}
int cnt[1<<17];
int main(){
scanf("%d",&n);
for(int i=0;i<n-1;i++){
int m;scanf("%d",&m);
for(int j=0;j<m;j++){
int x,y;scanf("%d%d",&x,&y);
vec[i].push_back(node(x-1,y-1));
}
}
long long ans=0;
for(int s=0;s<(1<<(n-1));s++){
cnt[s]=cnt[s>>1]+(s&1);
if((n-1-cnt[s])&1)ans=(ans-solve(s))%md;
else ans=(ans+solve(s))%md;
}
printf("%lld",(ans+md)%md);
return 0;
}
随机 K 大值
现在有 \(N\) 个独立的随机变量,第 \(i\) 个变量 \(X_i\) 服从 \([L_i, R_i]\) 上的均匀分布。问 \(X_1,\dots, X_N\) 中的第 \(K\) 大的数的期望。\(N\le 50, L_i,R_i\le 100\)。
考虑简单版本:\(L_i = 0, R_i = 100\) (从 [0, 100] 里均匀独立地随机 N 个数,问其中第 K 大的期望是多少。)
通常的容斥:有 \(N\) 个限制条件,问有多少种方案 (1) 满足至少一个限制条件 / (2) 不满足任何限制条件。
如果我们问 (3) 有多少种方案满足恰好 K 个限制条件? (3‘)有多少方案满足至少 K 个条件。
用 \(f[S]\) 表示满足并且只满足集合 S 里的限制条件的方案数。用 \(g[S]\) 表示至少满足了集合 S 里的限制条件的方案数。
由反演公式:
那么问题 1 的答案是:
这里是因为 \(g[\varnothing] = ALL\)。
问题 (3):
问题 (3'):
其中 \(A(n, k) = \sum_{i = k}^{n} \binom{n}{i}\cdot (-1)^{n-i}\).
(作为一个特殊情形,考虑 \(k=1\) 的时候,\(\sum_{i=1}^{n} (-1)^{n-i}{n\choose i} = (-1)^{n+1}\),此时得到奇偶错位求和)。
其中 \(f[S]\) 表示 \(X_i \ge x\) 对任意 \(i\in S\) 成立,并且 \(X_i < x\) 对任意的 \(i\notin S\) 成立。
\(g[S]\) 表示 \(X_i\ge x\) 对任意 \(i\in S\) 成立。\(c_{|S|}\) 是一个容斥系数。(这个概率就是 \(\prod_{i\in S} \frac{R-x}{R}\),对 \(x\) 积分即可)。
当 \(L_i,R_i\) 不一定一样的时候:把 \([0, +\infty)\) 按照 \(L_i, R_i\) 分段,每一段用上述技巧计算多项式,求积分。(ABC:Random K-th Max)
BZOJ 已经没有什么好害怕的了
给定两个长度为 \(N\) 的数组 \(\{A_i\}\) 和 \(\{B_i\}\),现在要把他们配对起来,使得恰好有 \(K\) 对 \((A,B)\) 满足 \(A\ge B\),求方案数。\(N\le 2000\)。
考虑一个暴力容斥:
枚举集合 \(A\) 里的 \(t\) 个数构成的集合 \(S\),要求他们必须匹配到比自己小的,其余的\(N-t\) 个数随意,求方案数。
暴力枚举完所有可能情况,可以算出 \(g[t]\):表示在集合A里钦定了某 t 个数,保证它们的匹配对象比自己要小,其他随意的方案数。
用 \(g[t]\) 反演出 \(f[t]\):恰好有 t个A中的数,满足他们的匹配对象比自己小。答案就是 \(f[K]\).
优化:用一个DP来计算 \(g\)。令 \(dp[i][j]\) 表示在集合 A 的前 i 个数里,钦定了 j 个数的匹配对象比自身小,的方案数之和。
\(dp[N][t]\cdot (N-t)! = g[t]\)。
运算量是 \(O(N^2)\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
int a[2005],b[2005],loc[2005];
long long fac[2005],inv[2005];
long long dp[2005][2005],f[2005],g[2005];
const long long md=1e9+9;
inline void init(){
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i%md;
for(int i=2;i<=n;i++)inv[i]=(md-md/i)*inv[md%i]%md;
for(int i=2;i<=n;i++)inv[i]=inv[i]*inv[i-1]%md;
}
inline long long C(int x,int y){
return fac[x]*inv[y]%md*inv[x-y]%md;
}
int main(){
scanf("%d%d",&n,&k);
init();
if((n+k)&1){puts("0");return 0;}k=(n+k)>>1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)scanf("%d",&b[i]);
sort(a+1,a+n+1);
sort(b+1,b+n+1);
for(int i=1,j=0;i<=n;i++){
while(j<n&&b[j+1]<a[i])j++;
loc[i]=j;
}
dp[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=i;j++){
if(j)dp[i][j]=(dp[i-1][j]+max(0,loc[i]-j+1)*dp[i-1][j-1])%md;
else dp[i][j]=dp[i-1][j];
}
}
for(int i=1;i<=n;i++)g[i]=dp[n][i]*fac[n-i]%md;
long long ans=0;
for(int i=k;i<=n;i++){
ans=(ans+(((i-k)&1)?-1:1)*g[i]*C(i,k)%md)%md;
}
printf("%lld",(ans+md)%md);
return 0;
}
Atcoder Beginner Contest-???
给定一个 \(1\sim N\) 的排列 \(P\)。问有多少 \(i\ne j\) 满足 \(gcd(i,j) > 1\) 且 \(gcd(P_i, P_j) > 1\)。 \(N\le 2\times 10^5\)。
简单问题:有多少 \((i,j)\) 满足 \(gcd(P_i, P_j) > 1\)(不考虑 \(gcd(i,j) > 1\) 的限制)。容斥(莫比乌斯反演)
- 计算有多少 \((i, j)\) 满足 $2 | (P_i,P_j) $:加上。
- 计算多少满足 \(3 | (P_i, P_j)\):加上。
- 计算有多少 \(6|(P_i, P_j)\):减掉。
- 。。。
- 计算有多少 \(d|(P_i, P_j)\):产生 \(-\mu(d)\) 的贡献。
可以实现一个数据结构,支持插入一个数,删除一个数;询问集合里有多少个互质的pair。
- 每次插入/删除一个数 \(x\) 的时候,枚举 \(x\) 的约数 \(d\),更新 \(d\) 的倍数的个数。同时更新 ANS。
- 每次对数据结构的操作时间是\(2^{\omega(x)}\le \sqrt{N}\),其中 \(\omega(x)\)表示 x 不同的质因子的个数。
原题:现在带上了 i j 的限制。
- 只考虑 \(2|(i, j)\) 的那些下标,计算一遍简单问题。产生 1 的贡献
- 枚举 \(3 | (i, j)\) 的下标,计算一遍简单问题。产生 1 的贡献
- 。。。
- 枚举 \(d|(i,j)\) 的下标,计算一遍简单问题。产生 \(-\mu(d)\) 的贡献。
总共会对数据结构进行 \(O(N\log N)\) 次更新。
计算量:\(O(N\sqrt{N}\log N)\).