[NOI2020] 命运 题解
神仙题。
题意:给定一棵 \(n\) 个节点的树,需要给每条边赋 \(0/1\) 的权值,给定 \(m\) 个限制,一个限制 \((u,v)\) 保证 \(u\) 是 \(v\) 的祖先,要求 \(u\) 到 \(v\) 之间至少有一条边赋值为 \(1\),询问合法的方案数。\(n,m\le 5\times 10^5\)。
自己想到了 32 pts 的容斥做法,就是枚举哪些限制不满足,然后子集反演即可。
首先我们对于每个点 \(x\),只考虑 \(u\) 在子树外,\(v\) 在子树内的限制,因为 \(u\) 在子树内可以递归解决,\(v\) 在子树外则与 \(x\) 的决策无关。有一个关键观察,对于所有 \(x\) 处尚未满足的限制,最关键的是 \(u\) 的深度最大的那个,只要在 \((u_{max},x)\) 之间的边被填了 \(1\) 之后,\(x\) 这个点的所有限制都将被满足,否则这个限制一定不能满足。
设 \(f_{x,i}\) 表示,\(x\) 子树内都已符合要求,当前 \(x\) 处最深的 \(u\) 的深度为 \(i\) 的方案数,其中 \(f_{x,0}\) 表示所有限制都满足的方案数,初始值 \(f_{x,dep_{anc_x}}=1\),其中 \(anc_x\) 表示以 \(x\) 为 \(v\) 的限制中的祖先节点里最深的一个,没有则为 \(0\)。转移考虑合并子树,分两种情况。
1.\((x,v)\) 这条边填 \(1\),则 \(v\) 处所有限制都被满足,可以由任意位置转移过来,即 \(f'_{x,i} \leftarrow f_{x,i} \times \sum_{j=0}^{dep_x} f_{v,j}\)。
2.\((x,v)\) 边填 \(0\),则 \(v\) 有限制,\(f'_{u,i} \leftarrow f_{u,i}\times \sum_{j=0}^if_{v,j}\),也有可能是 \(v\) 的限制更深,则 \(f'_{u,i} \leftarrow f_{v,i} \times \sum_{j=0}^{i-1} f_{u,j}\),上界取 \(i-1\) 是因为取 \(i\) 前面算过了。这个式子是 max 卷积的一种形式。
直接做太暴力了,发现可以前缀和优化。令 \(e_{x,i} = \sum_{j=0}^i f_{x,j}\),则 \(f'_{u,i} \leftarrow f_{u,i}\times (e_{v,dep_x}+e_{v,i})+f_{v,i}\times e_{x,i-1}\)。
时间复杂度 \(O(n^2)\),开动态数组加离散化深度能得 64 pts。
这时候我们还能怎么优化呢?看到这种前缀/后缀和的转移,而且很多位置是没有值的,考虑线段树合并。
把 \(f_x\) 放到线段树上。先单点更新 \(dep_{anc_x}\) 位置为 \(1\),然后考虑怎么和儿子节点的线段树合并在一起。
因为转移需要两个数组的前缀和,所以我们合并的时候一边递归一边更新前缀和,先递归左子树,再递归右子树即可,由于有乘法维护一个区间乘的标记,直接对着转移式子模拟其操作即可。
时间复杂度 \(O(n\log n)\)。
话说这题的链上版本我好像做过来着,为什么没想出来呢。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N =5e5+10,mod=998244353;
int fa[N],dep[N],siz[N],anc[N],b[N],len,tmp[N];
int f[32*N],tag[32*N],ls[32*N],rs[32*N],tot,rt[N];
vector<int>g[N];
inline void add(int &x,int y){
x=(x+y>=mod)?x+y-mod:x+y;
}
void dfs(int x,int f){
dep[x]=dep[f]+1;fa[x]=f;
siz[x]=1;
for(auto v:g[x]){
if(v==f)continue;
dfs(v,x);
siz[x]+=siz[v];
}
}
void pushup(int p){
f[p]=(f[ls[p]]+f[rs[p]])%mod;
}
void pushdown(int p){
if(tag[p]!=1){
f[ls[p]]=(f[ls[p]]*tag[p])%mod;
tag[ls[p]]=(tag[ls[p]]*tag[p])%mod;
f[rs[p]]=(f[rs[p]]*tag[p])%mod;
tag[rs[p]]=(tag[rs[p]]*tag[p])%mod;
tag[p]=1;
}
}
void modify(int &p,int l,int r,int x,int v){
if(!p)p=++tot,tag[p]=1;
if(l==r){
tag[p]=1;
f[p]=v;
return;
}
pushdown(p);
int mid=(l+r)/2;
if(x<=mid)modify(ls[p],l,mid,x,v);
else modify(rs[p],mid+1,r,x,v);
pushup(p);
}
int query(int p,int l,int r,int ql,int qr){
if(!p)return 0;
if(ql<=l&&r<=qr)return f[p];
pushdown(p);
int mid=(l+r)/2,res=0;
if(ql<=mid)add(res,query(ls[p],l,mid,ql,qr));
if(qr>mid)add(res,query(rs[p],mid+1,r,ql,qr));
return res;
}
int merge(int u,int v,int l,int r,int &sum1,int &sum2){
if(!u&&!v)return 0;
if(!u){
add(sum2,f[v]);
f[v]=(f[v]*sum1)%mod;
tag[v]=(tag[v]*sum1)%mod;
return v;
}
if(!v){
add(sum1,f[u]);
f[u]=(f[u]*sum2)%mod;
tag[u]=(tag[u]*sum2)%mod;
return u;
}
if(l==r){
int f1=f[u];
add(sum2,f[v]);
f[u]=((f[u]*sum2)%mod+f[v]*sum1%mod)%mod;
add(sum1,f1);
return u;
}
pushdown(u);pushdown(v);
int mid=(l+r)/2;
ls[u]=merge(ls[u],ls[v],l,mid,sum1,sum2);
rs[u]=merge(rs[u],rs[v],mid+1,r,sum1,sum2);
pushup(u);
return u;
}
void dp(int x){
modify(rt[x],0,n,dep[anc[x]],1);
for(auto v:g[x]){
if(v==fa[x])continue;
dp(v);
int sum1=0,sum2=query(rt[v],0,n,0,dep[x]);
rt[x]=merge(rt[x],rt[v],0,n,sum1,sum2);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
cin>>m;
dfs(1,0);
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
anc[v]=(dep[u]>dep[anc[v]])?u:anc[v];
}
dp(1);
cout<<query(rt[1],0,n,0,0);
return 0;
}

浙公网安备 33010602011771号