PKUWC2018 随机算法 和 猎人杀
随机算法
我们知道,求任意图的最大独立集是一类NP完全问题,目前还没有准确的多项式算法,但是有许多多项式复杂度的近似算法。
例如,小 C 常用的一种算法是:
-
对于一个 \(n\) 个点的无向图,先等概率随机一个 \(1\ldots n\) 的排列 \(p[1\ldots n]\)。
-
维护答案集合 \(S\) ,一开始 \(S\) 为空集,之后按照 \(i=1\ldots n\) 的顺序,检查 \(\{p[i]\}\cup S\) 是否是一个独立集,如果是的话就令 \(S=\{p[i]\}\cup S\)。
-
最后得到一个独立集 \(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\)
题解
题目描述中给出的随机方式相当于还是在随机排列,这是不好做的。
构造:不随机排列,每次独立随机选一个人(无论死活)。如果死了则继续随机,直到活着为止。
易证这两种方式是等价的。由于随机到死人的时候不管,所以概率分布还是一样的。
有了这个构造之后我们再来看这道题。现在我们需要 \(1\) 号猎人死之前所有猎人都死了。这个显然不好做,于是考虑容斥。
记 \(1\) 号死之前 \(s\) 集合里的人没死,则答案为
注意到 \(\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;
}