Min-max 容斥与 kth 容斥
期望的线性性:
证明:
Min - Max 容斥:
我们现在有一个全集 \(U= \lbrace{a_1,a_2,a_3,...,a_n}\rbrace\)
我们设:
有:
二项式反演证明:
我们想构造一个函数 \(f\) ,使得:
然后依然考虑一个元素排序后在哪些集合产生贡献
假设某个元素从小到大后排在第 \(x\) 位(集
合大小为 \(n\)),那么它的贡献就是:
变换一下:
二项式反演:
于是:
证毕
kth 容斥:
证明:
设:
假设某个元素从小到大后排在第 \(x\) 位(集合大小为 \(n\)),有:
变换一下:
二项式反演:
于是:
证毕
Min-Max容斥定理在期望下也成立:
以 \((1)\) 为例:
证明:
由于:
有:
由期望的线性性,直接整理,得:
证毕
[HAOI2015]按位或
记 \(p(S)\) 表示选择 \(S\) 的概率,即题目输入。
\(E(\min(T))\) 表示第一次覆盖 \(T\) 中任一元素的期望操作次数。
设 \(P(T)\) 表示一次操作能覆盖 \(T\) 中任一元素的概率。
则
加法原理
记 \(P'(T) = \displaystyle \sum _ {S \subseteq T} p(S)\),则 \(P(T) = 1 - P'(U - T)\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int cnt[1<<20];
double p[1<<20],ans;
int main(){
scanf("%d",&n);
for(int s=0;s<(1<<n);s++){
scanf("%lf",&p[s]);
cnt[s]=cnt[s>>1]+(s&1);
}
for(int i=1;i<(1<<n);i<<=1){
for(int s1=0;s1<(1<<n);s1+=(i<<1)){
for(int s2=0;s2<i;s2++){
p[i+s1+s2]+=p[s1+s2];
}
}
}
for(int i=1;i<(1<<n);i++)if(1-p[i^((1<<n)-1)])ans+=((cnt[i]&1)?1:-1)/(1-p[i^((1<<n)-1)]);
if(ans<1e-10)puts("INF");
else printf("%.10lf",ans);
return 0;
}
重返现世
题目要求第 \(k\) 小。为了方便,以下令 \(k=n-k+1\),即变为求第 \(k\) 大。
很显然,这题是让我们求这个东西:
然而 \(n \leq 1000\) 的数据很明显不能暴力枚举每一个 \(T\)。为了优化复杂度,我们考虑一个类似于背包的 \(\text{DP}\) 。
设 \(f_{x,j,k}\) 表示前 \(x\) 个元素,满足 \(\sum p=j\),以 \(k\) 为基准的 \(\sum_T {|T|-1 \choose k-1} (-1)^{|T|-k}\) 的大小。可能你会奇怪为什么要记录 \(k\),先往后面看。
考虑转移。显然要根据 \(T\) 中是否有 \(x\) 这个元素进行分类讨论。
当 \(T\) 中没有 \(x\),直接转移,\(f_{x,j,k}=f_{x-1,j,k}\) 。
当 \(T\) 中有 \(x\) 时,显然前两维由 \(f_{x-1,j-v}(v=p_x)\) 转移而来,有
把 \(x\) 丢掉,转为考虑 \(x-1\) 时的 \(T\),此时 \(\sum_p = j-v\) :
最终我们得到转移方程:
边界条件为 \(f_{x,0,0}=1\) 。
发现这东西时空复杂度都是 \(O(nm(n-k))\),似乎要炸空间,所以还需要把第一维滚掉。
最后统计答案时枚举 \(\sum p\) 然后随便搞搞就好啦。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,t;
long long dp[15][10005];
const long long md=998244353;
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;
}
int main(){
scanf("%d%d%d",&n,&t,&m);
t=n-t+1;dp[0][0]=1;
for(int i=1;i<=n;i++){
int p;scanf("%d",&p);
for(int k=m;k>=p;k--){
for(int j=t;j;j--){
dp[j][k]=(dp[j][k]+dp[j-1][k-p]-dp[j][k-p])%md;
}
}
}
long long ans=dp[t][0];
for(int i=1;i<=m;i++)ans=(ans+dp[t][i]*pwr(i,md-2)%md)%md;
printf("%lld",(ans+md)*m%md);
return 0;
}
[PKUWC2018]随机游走
由于本题要求点集 \(S\) 中所有点都被经过的步数期望,即到达点集 \(S\) 中最后一个点的期望步数。运用 \(\min-\max\) 容斥可以将其转化为到达点集 \(S\) 中第一个点的期望步数。
设 \(f_{S, i}\) 表示从 \(i\) 出发,经过 \(S\) 中的至少一个点的期望步数,\(deg_{i}\) 为点 \(i\) 的度数,可以得到这样的递推式:
若 \(i \in S\) ,则 \(f_{i, S}=0\) 。
按照套路,将转移方程写成有关于父亲的函数,即 \(f_{S, i}=A_{i} \times f_{S, f a_{i}}+B_{i}\) ,分离儿子与父亲的贡献。
发现 \(A_{i}, B_{i}\) 的方程与父亲无关,可以直接树形 \(dp\) 求解。由于 \(root\) 不能从父亲转移,显然有 \(f_{S, root}= B_{\text {root 。 }}\) 。
所以点集 \(S\) 的答窒为 \(\sum_{T \in S, T \neq \varnothing}(-1)^{|T|+1} f_{T, r o o t}\) 。
预处理所有 \(f_{S,root}\) ,再 \(\text{FWT}\) 预处理所有子集和,复杂度 \(\mathcal{O}(n2^n)\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,q,rt;
int ver[45],ne[45],head[45],tot,deg[45];
inline void link(int x,int y){
ver[++tot]=y;
ne[tot]=head[x];
head[x]=tot;deg[y]++;
}
long long a[21],b[21];
const long long md=998244353;
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;
}
void dfs(int x,int fi,int S){
if((S>>(x-1))&1)return ;
long long tota=0,totb=0;
for(int i=head[x];i;i=ne[i]){
int u=ver[i];
if(u==fi)continue;
dfs(u,x,S);
tota=(tota+a[u])%md;totb=(totb+b[u])%md;
}
a[x]=pwr(deg[x]-tota,md-2);
b[x]=(deg[x]+totb)%md*a[x]%md;
}
long long dp[1<<18];
int cnt[1<<18];
int main(){
scanf("%d%d%d",&n,&q,&rt);
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
link(x,y);link(y,x);
}//puts("111");
for(int s=1;s<(1<<n);s++){
cnt[s]=cnt[s>>1]+(s&1);
for(int i=1;i<=n;i++)a[i]=b[i]=0;
dfs(rt,rt,s);dp[s]=(cnt[s]&1?1:-1)*b[rt];
}//puts("222");
for(int i=0;i<n;i++){
for(int s=0;s<(1<<n);s++){
if((s>>i)&1)continue;
dp[s|(1<<i)]=(dp[s|(1<<i)]+dp[s])%md;
}
}
while(q--){
int k,s=0;
scanf("%d",&k);
while(k--){
int x;scanf("%d",&x);
s|=(1<<(x-1));
}printf("%lld\n",(dp[s]+md)%md);
}
return 0;
}
「JSOI2022模拟赛zjk」Festival
考虑 \(\text{min-max}\) 容后,即枚举一些位置,计算这些位置中存在一个被点亮了的期望时间。
然后考虑期望线性性,即计算 \(\sum_{i>0}P[\ i\ 步操作后这些位置都没有被点亮]\) 。
可以发现,计算这个概率时,最后一步操作需要满足点亮位置不是选中位置,倒数第二步操作需要满足点亮位置距离最近的选中位置距离大于 \(1\) ,\(\ldots\) ,考虑没有被选中的位置构成的段,设一段长度为 \(l\) ,则它在倒数第 \(1,2, \cdots\) 次操作中可以提供的方案数为 \(l, l-2, l-4, \cdots\) 。
因此可以求出倒数第 \(k\) 步满足条件的概率 \(pr_{k}\) ,求前缀积再求和即可。
上述过程仍然只和每一段长度有关,考虑枚举每一段长度,相当于枚举划分数。从这里开始,认为每一 段包含这一段结尾的一个选中位置,从而提供的方案数变为 \(l-1, l-3, l-5, \cdots\) 且段满足长度总和为 \(n\) 。
首先任意排列方案数为 \(\dfrac{\left(\sum c_{i}\right) !}{\prod c_{i} !}\) 。考虑第一段长度的期望,可以发现第一段长度为 \(i\) 的概率为 \(\dfrac{c_{i}}{\sum c_{i}}\) ,而 \(\sum i c_{i}=n\) ,因此最后的方案数即为: \(\dfrac{n *\left(\left(\sum c_{i}\right)-1\right) !}{\prod c_{i} !}\) 。
考虑将这个做法换成 \(dp\) 。上两部分分别给出了段数对应的期望时间和方案数。
前缀乘积的和可以看成,选择一个 \(k\) ,这之前的 \(pr_{k}\) 需要乘入贡献,之后的不需要。
考虑从小到大枚举长度,考虑设状态为,当前有 \(i\) 段,总长为 \(j\) ,当前的第一个 \(pr\) 是否乘入贡献。这样对应 \(dp_{i, j, 0 / 1 }\) 。
考虑计算一个 \(dp_{i, j}\) ,枚举长度为 \(1,2\) 的段数 \(x, y\) ,则可以发现此时第一个 \(pr\) 为 \(\dfrac{j-i}{n}\) ,这一步之后,长度为 \(1,2\) 的段不会再对 \(p r\) 有影响,而刲下的段可以看成长度全部减去 \(2\),然后变成做下一步之前的情况。因此转移形如:
初值为 \(d p_{0,0,0}=1\) 。最后,一个 \(d p_{i, n, 1}\) 对答案的贡献为 \(n *(i-1) ! *(-1)^{i-1}\) 。
但这样算出来少加了 \(1\) ,可以最后再解决。一些形式上类似的 \(dp\) 可以得到类似的效果。
在上一步的 \(dp\) 中,考虑分步枚举 \(x, y\) ,考虑先枚举 \(x\) ,从 \(d p_{i, j}\) 转移到状态 \(d p_{i-x, j-i}\) 并且这一步处理复杂度降至 \(O\left(n^{3}\right)\) 。
考虑记忆化搜索只处理可达的情况,且可以发现有很多转移是不需要的,实现后 \(n=500\) 只需要 \(30 \mathrm{~ms}\) ,极限数据需要 \(0.5 s\) 。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
int md;
inline int add(int x,int y){
return (x+=y)>=md?x-md:x;
}
inline int sub(int x,int y){
return (x-=y)<0?x+md:x;
}
inline int ksm(long long x,int tp,int s=1){
for(;tp;x=x*x%md,tp>>=1)if(tp&1)s=x*s%md;
return s;
}
int n,in;
int inv[1505];
int f[1505][1505],g[1505][1505];
int s1[1505],s2[1505];
int ans;
void dp(int L){
f[1][2*L+1]=ksm(in,L);
for(int i=L-1;i>=0;i--){
int x=2*i+1;
for(int j=0;j*x<=n;j++){
memset(g[j],0,(n+1)*sizeof(int));
}
g[0][n]=1ll*f[0][n]*n%md;
g[1][2*i+1]=ksm(in,i);
for(int j=1;j*x<=n;j++){
memset(s1,0,j*x*sizeof(int));
for(int k=j*x;k<=n;k++){
g[j][k]=(g[j][k]+1ll*f[j][k]*(k-2*i*j))%md;
if(k+x<=n)g[j+1][k+x]=(g[j+1][k+x]+1ll*f[j][k]*(j+1))%md;
s1[k]=(s1[k-1]+1ll*f[j][k]*2*j)%md;
}
for(int k=j*x;k<=n;k++){
g[j][k]=add(g[j][k],sub(s1[k-1],s1[k-x]));
}
if(j>1){
memset(s1,0,j*x*sizeof(int));
memset(s2,0,j*x*sizeof(int));
for(int k=j*x;k<=n;k++){
s1[k]=(s1[k-1]+1ll*f[j][k]*(j-1)%md*(x-1+k))%md;
s2[k]=(s2[k-1]+1ll*f[j][k]*(j-1))%md;
}
for(int k=j*x;k<=n;k++){
g[j-1][k]=(g[j-1][k]-1ll*(s2[k]-s2[k-x])*k+s1[k]-s1[k-x])%md;
if(g[j-1][k]<0)g[j-1][k]+=md;
}
}
else{
for(int k=j*x;k<=n;k++){
if(n-k<x)g[0][n]=(g[0][n]+1ll*f[j][k]*(x-1-n+k))%md;
}
}
}
for(int j=0;j*x<=n;j++){
memcpy(f[j],g[j],(n+1)*sizeof(int));
}
}
}
int main(){
freopen("festival.in","r",stdin);
freopen("festival.out","w",stdout);
scanf("%d %d",&n,&md);
inv[1]=1;
for(int i=2;i<=n;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
in=inv[n];
dp(n/2-1);
int s=f[0][n];
for(int i=1;i<=n;i++){
s=(s+1ll*f[i][n]*inv[i])%md;
}
printf("%d\n",(n/2+1-s+md)%md);
return 0;
}