PKUWC2018 随机算法 和 猎人杀

随机算法

我们知道,求任意图的最大独立集是一类NP完全问题,目前还没有准确的多项式算法,但是有许多多项式复杂度的近似算法。

例如,小 C 常用的一种算法是:

  1. 对于一个 \(n\) 个点的无向图,先等概率随机一个 \(1\ldots n\) 的排列 \(p[1\ldots n]\)

  2. 维护答案集合 \(S\) ,一开始 \(S\) 为空集,之后按照 \(i=1\ldots n\) 的顺序,检查 \(\{p[i]\}\cup S\) 是否是一个独立集,如果是的话就令 \(S=\{p[i]\}\cup S\)

  3. 最后得到一个独立集 \(S\) 作为答案。

小 C 现在想知道,对于给定的一张图,这个算法的正确率,输出答案对 \(998244353\) 取模

对于 \(100\%\) 的数据,有\(1\leq n\leq 20,0\leq m\leq \frac{n\times (n-1)}{2}\),保证给定的图没有重边和自环。

题解

直接做的话设 \(dp(s,t)\) 表示排列出现了 \(s\) 中的点,独立集为 \(t\) 的概率。\(O(3^nn)\) 显然不行。

构造:不随机 \(1\sim n\) 的排列,每次独立随机一个点看是否能加入独立集。如果不能则继续随机,直到能加入为止。

易证这两种方式是等价的。虽然我们只维护了 \(t\),但 \(s\) 中的点再次出现时会直接跳过,非 \(s\) 中但与 \(t\) 相邻的点对排列顺序与独立集都无影响,所以与 \(t\) 不相邻的点会等概率出现。即两种方式的所有点第一次出现的顺序等价。

那么现在不需要知道 \(s\),即哪些点被选过了。记 \(f(t)\) 表示独立集为 \(t\) 的概率,转移就在与 \(t\) 不相邻的点中等概率随机。

统计答案:每个点集作为独立集的概率×[大小=最大独立集大小]。

时间复杂度 \(O(2^n n)\)

CO int N=20;
int e[N],f[1<<N];

int main(){
	int n=read<int>();
	for(int i=0;i<n;++i) e[i]|=1<<i;
	for(int m=read<int>();m--;){
		int u=read<int>()-1,v=read<int>()-1;
		e[u]|=1<<v,e[v]|=1<<u;
	}
	f[0]=1;
	for(int s=0;s<1<<n;++s)if(f[s]){
		int cnt=0;
		for(int i=0;i<n;++i) cnt+=!(e[i]&s);
		cnt=fpow(cnt,mod-2);
		for(int i=0;i<n;++i)if(!(e[i]&s))
			f[s|1<<i]=add(f[s|1<<i],mul(f[s],cnt));
	}
	int siz=0;
	for(int s=0;s<1<<n;++s)if(f[s])
		siz=max(siz,popcount(s));
	int ans=0;
	for(int s=0;s<1<<n;++s)if(popcount(s)==siz)
		ans=add(ans,f[s]);
	printf("%d\n",ans);
	return 0;
}

猎人杀

猎人杀是一款风靡一时的游戏“狼人杀”的民间版本,他的规则是这样的:

一开始有 \(n\) 个猎人,第 \(i\) 个猎人有仇恨度 \(w_i\) ,每个猎人只有一个固定的技能:死亡后必须开一枪,且被射中的人也会死亡。

然而向谁开枪也是有讲究的,假设当前还活着的猎人有 \([i_1\ldots i_m]\),那么有 \(\frac{w_{i_k}}{\sum_{j = 1}^{m} w_{i_j}}\) 的概率是向猎人 \(i_k\) 开枪。

一开始第一枪由你打响,目标的选择方法和猎人一样(即有 \(\frac{w_i}{\sum_{j=1}^{n}w_j}\) 的概率射中第 \(i\) 个猎人)。由于开枪导致的连锁反应,所有猎人最终都会死亡,现在 \(1\) 号猎人想知道它是最后一个死的的概率。

答案对 \(998244353\) 取模。

对于 \(100\%\) 的数据,有 \(w_i>0\),且 \(1\leq \sum\limits_{i=1}^{n}w_i \leq 100000\)

题解

题目描述中给出的随机方式相当于还是在随机排列,这是不好做的。

构造:不随机排列,每次独立随机选一个人(无论死活)。如果死了则继续随机,直到活着为止。

易证这两种方式是等价的。由于随机到死人的时候不管,所以概率分布还是一样的。

\[P(i)=\frac{kill}{sum}P(i)+\frac{w_i}{sum}\\ P(i)=\frac{w_i}{sum-kill} \]

有了这个构造之后我们再来看这道题。现在我们需要 \(1\) 号猎人死之前所有猎人都死了。这个显然不好做,于是考虑容斥。

\(1\) 号死之前 \(s\) 集合里的人没死,则答案为

\[\sum_s(-1)^{|s|}\sum_{i=0}^\infty(1-P(s)-P(1))^iP(1)\\ =\sum_s(-1)^{|s|}\frac{w_1}{w_s+w_1} \]

注意到 \(\sum w\leq 10^5\),所以可以求出 \(cnt_k\) 表示所有满足 \(w_s=k\)\(s\) 的容斥系数 \((-1)^{|s|}\) 的和。

上生成函数,求 \(\prod(1-x^w)\) 即可。分治NTT解决,时间复杂度 \(O(n \log^2 n)\)

CO int N=2*131072;
int omg[2][N],rev[N];

void NTT(poly&a,int dir){
	int lim=a.size(),len=log2(lim);
	for(int i=0;i<lim;++i) rev[i]=rev[i>>1]>>1|(i&1)<<(len-1);
	for(int i=0;i<lim;++i)if(i<rev[i]) swap(a[i],a[rev[i]]);
	for(int i=1;i<lim;i<<=1)
		for(int j=0;j<lim;j+=i<<1)for(int k=0;k<i;++k){
			int t=mul(omg[dir][N/(i<<1)*k],a[j+i+k]);
			a[j+i+k]=add(a[j+k],mod-t),a[j+k]=add(a[j+k],t);
		}
	if(dir==1){
		int ilim=fpow(lim,mod-2);
		for(int i=0;i<lim;++i) a[i]=mul(a[i],ilim);
	}
}
poly operator*(poly a,poly b){
	int n=a.size()-1,m=b.size()-1;
	int lim=1<<(int)ceil(log2(n+m+1));
	a.resize(lim),NTT(a,0);
	b.resize(lim),NTT(b,0);
	for(int i=0;i<lim;++i) a[i]=mul(a[i],b[i]);
	NTT(a,1),a.resize(n+m+1);
	return a;
}

vector<int> a[2*N];
int tot,h[2*N],top;

IN bool cmp(int i,int j){
	return a[i].size()>a[j].size();
}
int main(){
	omg[0][0]=1,omg[0][1]=fpow(3,(mod-1)/N);
	omg[1][0]=1,omg[1][1]=fpow(omg[0][1],mod-2);
	for(int i=2;i<N;++i){
		omg[0][i]=mul(omg[0][i-1],omg[0][1]);
		omg[1][i]=mul(omg[1][i-1],omg[1][1]);
	}
	int n=read<int>(),w=read<int>();
	for(int i=2;i<=n;++i){
		int w=read<int>();
		a[++tot].resize(w+1);
		a[tot][0]=1,a[tot][w]=mod-1;
		h[++top]=tot;
	}
	make_heap(h+1,h+top+1,cmp);
	while(top>=2){
		int x=h[1];pop_heap(h+1,h+top+1,cmp),--top;
		int y=h[1];pop_heap(h+1,h+top+1,cmp),--top;
		a[++tot]=a[x]*a[y];
		h[++top]=tot,push_heap(h+1,h+top+1,cmp); // edit 1: cmp
	}
	int x=h[1],ans=0;
	for(int i=0;i<(int)a[x].size();++i)
		ans=add(ans,mul(mul(w,fpow(i+w,mod-2)),a[x][i]));
	printf("%d\n",ans);
	return 0;
}

posted on 2020-01-03 08:08  autoint  阅读(216)  评论(0编辑  收藏  举报

导航