min-max容斥

参考大佬博客:https://www.cnblogs.com/utopia999/p/9898393.html

基本就这俩式子:
max(S)= ∑ T ⊆ S ( − 1 ) ∣ T ∣ − 1 ∗ m i n ( T ) \sum_{T\subseteq S} (-1)^{|T|-1}*min(T) TS(1)T1min(T)
min的话上面min和max反一反…
还有一个推广的式子
kth-max(S)= ∑ T ⊆ S ( − 1 ) ∣ T ∣ − k ∗ ( ∣ T ∣ − 1 k − 1 ) ∗ m i n ( T ) \sum_{T\subseteq S}(-1)^{|T|-k}*\binom {|T|-1} {k-1}*min(T) TS(1)Tk(k1T1)min(T)
看上去很没有用,但其实它在算期望(或者lcm)的时候还是很厉害的。

lcm:

在这里插入图片描述

期望:

在这里插入图片描述
(上面这些均转载自开头大佬博客)
证明用到二项式反演,参见这名大佬博客:
https://blog.csdn.net/ez_2016gdgzoi471/article/details/81416333

例题:

T1:hdu4336 Card Collector

设max(s)为当前集合最后一个元素第一次取到的期望时间,min(s)为当前集合第一个元素第一次取到的时间,感性理解一下就是max和min的关系,而min-max容斥在期望意义下是成立的,所以直接上容斥即可,min(s)就是s中元素概率和的倒数。
代码:

#include<bits/stdc++.h>
#define db double
using namespace std;
const int N=25;

int n;
db a[N],ans=0;

void dfs(int pos,int cnt,db res)
{
	if(pos>n)
	{
		if(cnt==0)return;
		ans+=(cnt&1)?1.0/res:(-1.0)/res;
		return;
	}
	dfs(pos+1,cnt+1,res+a[pos]);
	dfs(pos+1,cnt,res);
}

int main()
{
	while(~scanf("%d",&n))
	{
		ans=0;
		for(int i=1;i<=n;i++)
			scanf("%lf",&a[i]);
		dfs(1,0,0);
		printf("%.6lf\n",ans);
	}
}

T2:bzoj4036 按位或

基本与之前那题相同,设max(s)为期望s中最后一个1最早出现时间,min(s)为s中第一个1最早出现时间就能上min-max容斥,不过这题的min稍微难算一点,min(s)是所有与s有交集的集合的概率和,那么就先取反变成sum减去是取反后的数的子集和,fwt就行了。

#include<bits/stdc++.h>
#define db double
using namespace std;
const int N=2e6+10;

int n,lim,cnt[N];
db a[N],sum=0;

void fwt(db *a)
{
	for(int l=2,md=1;l<=n;l<<=1,md<<=1)
		for(int i=0;i<n;i+=l)
			for(int j=0;j<md;j++)
				a[i+j+md]+=a[i+j];
}

int main()
{
	db ans=0,nw;
	scanf("%d",&n),n=1<<n,lim=n-1;
	for(int i=0;i<n;i++)
		scanf("%lf",&a[i]),sum+=a[i];
	cnt[1]=1;
	for(int i=2;i<n;i++)
		cnt[i]=cnt[i>>1]+(i&1);
	fwt(a);
	for(int i=1;i<n;i<<=1)
	{
		nw=sum-a[lim^i];
		if(nw==0)return puts("INF"),0;
	}
	for(int i=1;i<n;i++)
		nw=sum-a[lim^i],ans+=(cnt[i]&1)?1.0/nw:(-1.0)/nw;
	printf("%.8lf\n",ans);
}

T3:洛谷P4707重返现世

https://www.luogu.org/problemnew/show/P4707
根本不会.jpg
首先要用到min-max容斥的扩展式:
kth-max(S)= ∑ T ⊆ S ( − 1 ) ∣ T ∣ − k ∗ ( ∣ T ∣ − 1 k − 1 ) ∗ m i n ( T ) \sum_{T\subseteq S}(-1)^{|T|-k}*\binom {|T|-1} {k-1}*min(T) TS(1)Tk(k1T1)min(T)
其中的kth-max表示S集合里第k大的元素第一次出现的期望时间,min(S)表示S集合里最早出现的元素第一次出现的期望时间,min(S)= m / ∑ i ⊂ S p ( i ) m/\sum_{i\subset S}p(i) m/iSp(i)
p表示出现i的概率
但这题n=1000,2^n就不用想了,所以考虑dp:
dp(k,i,j)表示当前算的是kth-max,考虑到了第i个物品,当前集合所有元素的p之和为j的集合的 ∑ ( − 1 ) ∣ T ∣ − k ∗ ( ∣ T ∣ − 1 k − 1 ) \sum(-1)^{|T|-k}*\binom {|T|-1} {k-1} (1)Tk(k1T1)
那么最后的答案就是kth-max(S)= ∑ 1 &lt; = j &lt; = m d p ( K , n , j ) ∗ ( m / j ) \sum_{1&lt;=j&lt;=m}dp(K,n,j)*(m/j) 1<=j<=mdp(K,n,j)(m/j)
K是题目里读入的K。
而最终只要算题目给定的K,为什么还要开k这一维?是用于转移的:
考虑dp(k,i,j)是如何转移过来的:
第一种情况很简单,i这个位置上的元素没有放入集合,那么得到:
dp(k,i,j)+=dp(k,i-1,j)
第二种情况就难了,i这个位置上的元素放入了集合,那么它会导致
∑ ( − 1 ) ∣ T ∣ − k ∗ ( ∣ T ∣ − 1 k − 1 ) \sum(-1)^{|T|-k}*\binom {|T|-1} {k-1} (1)Tk(k1T1)这个式子发生变化,|T|变大1,对前半部分的贡献就是*(-1),而后半部分有关组合数贡献怎么算呢?考虑组合数的递推式 ( n m ) = ( n − 1 m ) + ( n − 1 m − 1 ) \binom n m=\binom {n-1} {m} + \binom {n-1} {m-1} (mn)=(mn1)+(m1n1)那么有 ( ∣ T ∣ − 1 k − 1 ) = ( ∣ T ∣ − 2 k − 1 ) + ( ∣ T ∣ − 2 k − 2 ) \binom {|T|-1} {k-1}=\binom {|T|-2} {k-1} + \binom{|T|-2}{k-2} (k1T1)=(k1T2)+(k2T2)
所以dp(k,i,j)+=(-1)*dp(k,i-1,j-p(i))+dp(k-1,i-1,j-p(i))

还有一个东西就是dp的初值,当T这个集合为空集的时候,发现坑爹的事情出现了,要算一个-1取多少的组合数,这东西肯定没法搞,所以考虑一下后面基于它的状态的情况来反推它的值,而当T这个集合元素个数为1且k=1的时候,之前0个元素的集合对它要产生1的贡献,而根据推得的贡献式子0个元素产生的贡献应该是dp(0,i-1,0)-dp(1,i-1,0),那么把dp(0,i,0)都赋成1即可(网上大部分题解里写的把dp(K,0,0)赋成-1个人太菜不太理解…)
数组开不下把i那维滚动即可
复杂度nmk,注意这里的k是n-k+1,因为题目要的是第k小,转化过来是第n-k+1大,所以复杂度是对的。
刚开始3维数组被卡常了…

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353;
const int N=1050,M=1e4+10;

int n,K,m,p[N],f[2][12][M],inv[M];

void Ad(int &x,int y)
{if((x+=y)>=mod)x-=mod;}
void Dw(int &x,int y)
{if((x-=y)<0)x+=mod;}

int main()
{
    int ans=0,nw,bf;
    scanf("%d%d%d",&n,&K,&m),K=n-K+1;
    for(int i=1;i<=n;i++)
        scanf("%d",&p[i]);
    for(int i=1;i<=n;i++)
    {
        nw=i&1,bf=nw^1;
        memset(f[nw],0,sizeof f[nw]);
        f[bf][0][0]=1;
        for(int k=1;k<=K;k++)
        {
            for(int j=0;j<=m;j++)
            {
                Ad(f[nw][k][j],f[bf][k][j]);
                if(j>=p[i])Ad(f[nw][k][j],f[bf][k-1][j-p[i]]),Dw(f[nw][k][j],f[bf][k][j-p[i]]);
            }
        }
    }
    inv[1]=1;
    for(int j=2;j<=m;j++)
        inv[j]=1LL*(mod-mod/j)*inv[mod%j]%mod;
    for(int j=1;j<=m;j++)
        Ad(ans,1LL*f[nw][K][j]*m%mod*inv[j]%mod);
    printf("%d\n",ans);
}

直接压掉第一维就过了…

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353;
const int N=1050,M=1e4+10;

int n,K,m,p[N],f[12][M],inv[M];

void Ad(int &x,int y)
{if((x+=y)>=mod)x-=mod;}
void Dw(int &x,int y)
{if((x-=y)<0)x+=mod;}

int main()
{
    int ans=0;
    scanf("%d%d%d",&n,&K,&m),K=n-K+1;
    for(int i=1;i<=n;i++)
        scanf("%d",&p[i]);
    for(int i=1;i<=n;i++)
    {
        f[0][0]=1;
        for(int k=K;k>=1;k--)
        {
            for(int j=m;j>=0;j--)
            {
                if(j>=p[i])Ad(f[k][j],f[k-1][j-p[i]]),Dw(f[k][j],f[k][j-p[i]]);
            }
        }
    }
    inv[1]=1;
    for(int j=2;j<=m;j++)
        inv[j]=1LL*(mod-mod/j)*inv[mod%j]%mod;
    for(int j=1;j<=m;j++)
        Ad(ans,1LL*f[K][j]*m%mod*inv[j]%mod);
    printf("%d\n",ans);
}

T4:「PKUWC2018」随机游走

链接:https://loj.ac/problem/2542
期望啥的忘光了…首先肯定要用min-max容斥,那么所求就是一个集合最早到达点的第一次到达期望时间,n范围很小,考虑暴力枚举集合,那么可以设一个f(x)表示从x号点最早走到集合内点期望时间,得到如下转移
f ( x ) = 0 &ThinSpace;&ThinSpace;&ThinSpace;&ThinSpace;&ThinSpace;&ThinSpace;&ThinSpace; 当 x ⊂ S 时 f(x)=0 \,\,\,\,\,\,\, 当x\subset S时 f(x)=0xS
f ( x ) = 1 d x ( 1 + f ( y ) + ∑ v ⊂ s o n ( x ) ( f ( v ) + 1 ) ) f(x)=\frac{1}{dx}(1+f(y)+\sum_{v\subset son(x)}(f(v)+1)) f(x)=dx1(1+f(y)+vson(x)(f(v)+1))
其中y表示x点的父亲,dx表示x的度数
这样发现一个点f(x)可以表示成多少f(y)+常数,子节点传到父节点后可以列方程同理把父节点转化成这样的形式,而根节点是没有父亲的,所以从下到上转移最后到根了就不会有多少f(y),变成一个只有常数的东西,就是我们要求的当前集合S的答案,访问到S集合内的点把两个东西都赋成0即可。
最后求得了min,跑一个fwt就能得到答案了。
复杂度2 ^ n * n * log (log是树形dp时候的逆元)
代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=20,mod=998244353;

int n,Q,lim,rt,d[N],k[N],b[N],ans[1<<18],cnt[1<<18];
bool inS[N];
vector<int>mp[N];

void Ad(int &x,int y)
{if((x+=y)>=mod)x-=mod;}
void Dw(int &x,int y)
{if((x-=y)<0)x+=mod;}
void Mul(int &x,int y)
{x=1LL*x*y%mod;}
int qpow(int x,int y)
{
	int res=1;
	while(y)
	{
		if(y&1)res=1LL*res*x%mod;
		x=1LL*x*x%mod,y>>=1;
	}
	return res;
}
int inv(int x)
{return qpow(x,mod-2);}

void dfs(int x,int y)
{
	if(inS[x]){k[x]=b[x]=0;return;}
	int tp=1,mo=inv(d[x]);
	b[x]=1,k[x]=(x==rt?0:mo);
	for(int i=0,v;i<mp[x].size();i++)
	{
		v=mp[x][i];
		if(v==y)continue;
		dfs(v,x),Dw(tp,1LL*k[v]*mo%mod),Ad(b[x],1LL*b[v]*mo%mod);
	}
	tp=inv(tp);
	Mul(k[x],tp),Mul(b[x],tp);
}

int fwt(int *a)
{
	for(int l=2,md=1;l<=lim;l<<=1,md<<=1)
		for(int i=0;i<lim;i+=l)
			for(int j=0;j<md;j++)
				Ad(a[i+j+md],a[i+j]);
}

int main()
{
	int u,v;
	scanf("%d%d%d",&n,&Q,&rt),lim=1<<n;
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&u,&v);
		mp[u].push_back(v);
		mp[v].push_back(u);
	}
	for(int i=1;i<=n;i++)d[i]=mp[i].size();
	for(int i=1;i<lim;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if((1<<(j-1))&i)inS[j]=1;
			else inS[j]=0;
		}
		dfs(rt,0),ans[i]=b[rt];
		//cerr<<i<<' '<<ans[i]<<'\n';
	}
	cnt[1]=1;
	for(int i=2;i<lim;i++)
		cnt[i]=cnt[i>>1]+(i&1);
	for(int i=1;i<lim;i++)
		if(cnt[i]%2==0)Mul(ans[i],mod-1);
	fwt(ans);
	int cc,res,tp;
	for(int i=1;i<=Q;i++)
	{
		res=0;
		scanf("%d",&cc);
		for(int j=1;j<=cc;j++)
			scanf("%d",&tp),res|=(1<<(tp-1));
		printf("%d\n",ans[res]);
	}
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值