PKUWC2018 Minimax
给定一棵二叉树,叶子节点有权值,权值互不相同。每个非叶节点上有个概率
p
i
p_i
pi,表示这个节点有
p
i
p_i
pi 的概率是子节点权值中较大值,
1
−
p
i
1-p_i
1−pi 的概率是较小值。求 1 号点是每个权值的概率。
设 f i , j f_{i,j} fi,j 表示节点 i i i 权值为第 j j j 大的数的概率。那么 f i , j = f v , j ( ( 1 − p i ) ∑ k > j f v , k + p i ∑ k < j f v , k ) f_{i,j}=f_{v,j}\big( (1-p_i)\sum_{k>j}f_{v,k}+p_i \sum_{k<j}f_{v,k}\big) fi,j=fv,j((1−pi)∑k>jfv,k+pi∑k<jfv,k)
考虑用线段树合并维护这个 DP。发现我们到一个区间 [ l , r ] [l,r] [l,r] 的时候,只需要分别维护两棵树上小于 l l l 的概率和,以及大于 r r r 的概率和,merge 到一个区间只有一个节点的时候,打上区间乘的标记即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353,N=300010;
struct edge{
int to,next;
}ed[N<<1];
int ls[N*20],rs[N*20],c[N],rt[N],cnt[N],sz,head[N],son[N][2],tot,nn;
ll sum[N*20],tag[N*20],pre[N],p[N];
inline void add_edge(int from,int to)
{
ed[++sz].to=to;
ed[sz].next=head[from];
head[from]=sz;
}
inline int read()
{
char c=getchar();int x=0,flag=1;
while(!isdigit(c)){if(c=='-') flag=-1;c=getchar();}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*flag;
}
void Insert(int &root,int l,int r,int x)
{
root=++tot;
tag[tot]=1;
sum[root]=1;
if(l==r) return;
int mid=l+r>>1;
if(x<=mid) Insert(ls[root],l,mid,x);
else Insert(rs[root],mid+1,r,x);
}
void push_down(int x)
{
if(tag[x]^1)
{
tag[ls[x]]=tag[ls[x]]*tag[x]%mod;
tag[rs[x]]=tag[rs[x]]*tag[x]%mod;
sum[ls[x]]=sum[ls[x]]*tag[x]%mod;
sum[rs[x]]=sum[rs[x]]*tag[x]%mod;
tag[x]=1;
}
}
void push_up(int x)
{
sum[x]=sum[ls[x]]+sum[rs[x]];
sum[x]%=mod;
}
int merge(int x,int y,ll pp,ll mx1,ll mn1,ll mx2,ll mn2)
{
if(!x||!y)
{
mx1%=mod;
mx2%=mod;
mn1%=mod;
mn2%=mod;
if(x)
{
ll tmp=((1-pp)*mx2+pp*mn2)%mod;
sum[x]=tmp*sum[x]%mod;
tag[x]=tag[x]*tmp%mod;
return x;
}
if(y)
{
ll tmp=((1-pp)*mx1+pp*mn1)%mod;
sum[y]=tmp*sum[y]%mod;
tag[y]=tag[y]*tmp%mod;
return y;
}
return 0;
}
push_down(x);
push_down(y);
int xls=sum[ls[x]],xrs=sum[rs[x]];
int yls=sum[ls[y]],yrs=sum[rs[y]];
ls[x]=merge(ls[x],ls[y],pp,mx1+xrs,mn1,mx2+yrs,mn2);
rs[x]=merge(rs[x],rs[y],pp,mx1,mn1+xls,mx2,mn2+yls);
push_up(x);
return x;
}
void dfs(int u)
{
if(!cnt[u]) Insert(rt[u],1,nn,p[u]);
else if(cnt[u]==1) dfs(son[u][0]),rt[u]=rt[son[u][0]];
else dfs(son[u][0]),dfs(son[u][1]),rt[u]=merge(rt[son[u][0]],rt[son[u][1]],p[u],0,0,0,0);
}
ll qpow(int a,int b)
{
ll ans=1,base=a;
while(b)
{
if(b&1) ans=ans*base%mod;
base=base*base%mod;
b>>=1;
}
return ans;
}
ll query(int root,int l,int r,int x)
{
if(l==r) return sum[root];
int mid=l+r>>1;
push_down(root);
if(x<=mid) return query(ls[root],l,mid,x);
else return query(rs[root],mid+1,r,x);
push_up(root);
}
int main()
{
int n=read();read();
ll inv=qpow(10000,mod-2);
for(int i=2;i<=n;i++)
{
int u=read();
son[u][cnt[u]++]=i;
}
for(int i=1;i<=n;i++)
{
p[i]=read();
if(!cnt[i]) c[++nn]=p[i];
else p[i]=1ll*inv*p[i]%mod;
}
sort(c+1,c+nn+1);
for(int i=1;i<=n;i++) if(!cnt[i])
{
int tmp=lower_bound(c+1,c+nn+1,p[i])-c;
pre[tmp]=p[i];
p[i]=tmp;
}
dfs(1);
ll ans=0;
for(int i=1;i<=nn;i++)
{
ll gg=query(rt[1],1,nn,i);
ans+=gg*gg%mod*pre[i]%mod*i%mod;
}
cout<<(ans%mod+mod)%mod;
return 0;
}
PKUWC2018 随机算法
对于一种求一般图最大独立集的算法:每次随机一个排列,按顺序把点加入,如果和之间的独立集没有边就加入独立集,否则弃掉。求这种方法的正确率。
n
≤
20
n\leq 20
n≤20
容易想到 n ⋅ 3 n n\cdot 3^n n⋅3n 的 DP,即状压每个点是否尝试加入过、是否在独立集里。思考为什么要记是否在独立集里,是因为加入一个点的时候我们要判断是否能够算入加入独立集里。那我们干脆在向独立集里加入一个元素的时候就把它的邻接点删除并且统计它们的贡献不就行了?
设 f i , S f_{i,S} fi,S 表示计算过 S S S 的贡献,当前独立集大小为 i i i 的方案数。枚举一个点,加入独立集,并把它的邻接点也加入 S S S。观察到第一维是不需要的,因为我们只需要计算取最大独立集的方案数。因此复杂度为 O ( n ⋅ 2 n ) O(n\cdot 2^n) O(n⋅2n)。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353;
int num[1<<21],g[100];
ll fac[100],de[100];
typedef pair <int,ll> P;
P f[1<<21];
inline int read()
{
char c=getchar();int x=0,flag=1;
while(!isdigit(c)){if(c=='-') flag=-1;c=getchar();}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*flag;
}
ll qpow(int a,int b)
{
ll ans=1,base=a;
while(b)
{
if(b&1) ans=ans*base%mod;
base=base*base%mod;
b>>=1;
}
return ans;
}
int main()
{
int n=read(),m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
g[u]|=1<<v-1;
g[v]|=1<<u-1;
}
fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
de[n]=qpow(fac[n],mod-2);
for(int i=n-1;i>=0;i--) de[i]=de[i+1]*(i+1)%mod;
for(int i=1;i<(1<<n);i++) num[i]=num[i>>1]+(i&1);
for(int i=1;i<=n;i++) g[i]|=1<<i-1;
f[0]=P(0,1);
for(int i=0;i<(1<<n);i++)
{
for(int j=1;j<=n;j++)
{
if(!(i&(1<<j-1)))
{
int s=i|g[j];
if(f[s].first==f[i].first+1)
{
int k=num[g[j]]-num[g[j]&i]-1;
(f[s].second+=f[i].second*fac[n-num[i]-1]%mod*de[n-num[i]-1-k])%=mod;
}
else if(f[s].first<f[i].first+1)
{
int k=num[g[j]]-num[g[j]&i]-1;
f[s].second=f[i].second*fac[n-num[i]-1]%mod*de[n-num[i]-1-k]%mod;
f[s].first=f[i].first+1;
}
}
}
}
cout<<f[(1<<n)-1].second*de[n]%mod;
return 0;
}
PKUWC2019
给定一棵树,每个点有颜色,定义一种颜色的虚树为这种颜色的点两两路径的并。对于每个
k
≤
n
k\leq n
k≤n,求有多少大小为
k
k
k 的颜色集合,使得这个集合中的每一种颜色构成的虚树存在一个公共点。
考虑选出一个颜色集合以后,为了它不被重复计算,我们想要找到一个唯一确定的点,使得枚举这个点的时候能计算这个集合的答案。我们强制这个点只能选在某个颜色的根处。容易发现颜色集合的根一定在上下的一条链上,并且选择的点只能是最下面的根。这样我们确定了:在每种方案的最深的根计算答案。记以第 i i i 个点为根的颜色有 a i a_i ai 种,根在 i i i 向上的链上并且点 i i i 在虚树里的颜色有 b i b_i bi 种。那么在 i i i 这个点对 k k k 的贡献是 a i ( b i k − 1 ) a_i{b_i\choose k-1} ai(k−1bi)。记 h j = ∑ a i = j b i h_j=\sum_{a_i=j} b_i hj=∑ai=jbi,发现是一个卷积的式子。NTT 即可。