【题解】P6773 命运
题面
前言
前置知识:DP,线段树合并
口齿不清的云落又双叒叕上线了……
第四道黑题~
正文
集训期间,shr 说:“这是一道线段树合并优化 DP”
其实 DP 是很好想到的,毕竟求方案数嘛~
但是怎么 DP,或者说怎么设置状态并转移?
比较朴素的做法:依照题意,列出状态
设 \(f_{x,0/1}\) 表示对于所有满足如下条件的约束 \(u,v\),是/否保证所有约束被满足
条件:对于一组约束 \(u,v\),有 \(u\) 在 \(x\) 子树外且 \(v\) 在 \(x\) 子树内
然后……
Game Over!
根本做不下去嘛 qwq
汲取一些失败经验
无法转移,究其原因,是 \(0/1\) 维度所包含的信息量过大
树形 DP 有一个很强的性质,由儿子向父亲转移
而对于上述 DP,试图进行一次转移
注意到 \((x,fa_x)\) 是无法判断的,因为不知道 \(x\) 子树内的相关约束是否与边 \((x,fa_x)\) 有关
回到树形 DP 最基础的性质,由子孙转移向祖先
由深度大的向深度低的转移
嗯——深度?
突然发现一个神奇的小性质
对于一组约束集 \((u,v)\),满足下端点同时为 \(v\),则必须满足深度最大的 \(u\)
这个感性理解下即可
那么既然这个性质与 \(u\) 的最大深度有关,并结合上文没有前途的 DP 状态
设 \(f_{x,i}\) 表示表示 \(x\) 为根的子树中,两端点都在 \(x\) 子树内的限制已经被满足,而对于尚未被满足的 \((u,v)\), 且 \(u\) 在 \(x\) 子树外,\(v\) 在 \(x\) 子树内,最深的 \(u\) 的深度为 \(i\) 的方案数
加粗部分是无后效性的一个重要设计
对于未赋值的边,权值赋为 \(0\)
可能会有人问,那么 \(u,v\) 都在 \(x\) 子树外捏?
……那这组约束 \((u,v)\) 根本与 \(x\) 子树无关好叭╮(╯-╰)╭
考虑转移
设 \(x\) 的子结点为 \(y\),由 \(y\) 转移到 \(x\)
大分讨!
- 边 \((x,y)\) 赋值为 \(1\)
此时 \(y\) 子树内所有的约束都将被满足(因为转移的过程中 \(fa_y=x\)),显然有
\begin{equation}
f_{x,i} \gets \sum_{j=0}^{dep_x} f_{x,i} \times f_{y,j}
\nonumber
\end{equation}
- 边 \((x,y)\) 赋值为 \(0\)
- 如果 \(y\) 中深度最大的约束是 \(\le i\) 的,那么就和第一种情况大差不差,\(y\) 子树无法提供更严格的约束,方程如下:
\begin{equation}
f_{x,i} \gets f_{x,i} \times \sum_{j=0}^{i} f_{y,j}
\nonumber
\end{equation}
- 否则,\(y\) 子树会贡献更严格的约束,最深的限制可以有更浅的限制转移而来,方程如下:
\begin{equation}
f_{x,i} \gets f_{x,j} \times \sum_{j=0}^{i-1} f_{y,i}
\nonumber
\end{equation}
把三种情况依照加法原理合并,得出:
\begin{equation}
f_{x,i} \gets f_{x,i} \times \sum_{j=0}^{dep_x} f_{y,j} + f_{x,i} \times \sum_{j=0}^{i} f_{y,j} + f_{y,i} \times \sum_{j=0}^{i-1} f_{x,j}
\nonumber
\end{equation}
就这么个东西,太抽象了
看到一堆西格玛号,云落就提不起继续做下去的欲望
先给它整理一下,有:
\begin{equation}
f_{x,i} \gets f_{x,i} \times (\sum_{j=0}^{dep_x} f_{y,j} + \sum_{j=0}^{i} f_{y,j}) + f_{y,i} \times \sum_{j=0}^{i-1} f_{x,j}
\nonumber
\end{equation}
注意到西格玛号的结构是极相似的,稍微代换一下
记 \(g_{x,i}\) 表示 \(\sum_{j=0}^{i} f_{x,i}\)
原式简化为
\begin{equation}
f_{x,i} \gets f_{x,i} \times g_{y,{dep_x}} + f_{x,i} \times g_{y,i} + f_{y,i} \times g_{x,i-1}
\nonumber
\end{equation}
至此,我们已经有了一个 \(O(n^2)\) 的做法,可以拿到 \(64pts\)
考虑优化
注意到 \(g\) 在式子中表现为前缀和的形式
定义两个“变”量 \(su,sv\),分别记录 \(g_{x,i},g_{y,i}\) 的值
式子是若干项相加,线段树合并可以担任这个艰巨的优化任务
我们尝试将 DP 的第二维用线段树维护
第一项是个常数,第二项可以一边计算一边加入
与模板题(雨天的尾巴)相比,只多了两个系数与一个常数 ——火腿肠
这太抽象了
显然要对 \(x,y\) 的取值进行一波分讨
我们看看 merge
函数的具体实现
-
若 \(x,y\) 都是空结点,略过
-
若 \(x=0,y \neq 0\),有 \(su\) 不会变化,\(f_{x,i}\) 全都是 \(0\),让 \(sv\) 更新一下,并给 \(y\) 的 \(tag,sum\) 更新
-
若 \(x \neq 0,y=0\),同理
-
如果 \(l=r\),直接按照上述式子,把后两项加起来,并更新 \(su,sv\)
-
否则,下传标记,递归地遍历每一棵子树(记得传参 \(su,sv\))
-
最后将从叶子结点接收到的信息上传,更新答案
-
返回合并后新的线段树的根结点编号
最后一个问题
如何统计答案?
显然 \(f_{1,0}\)
细节处理:
pushdown
需要注意标记下放的顺序,先乘后加
可以注意一下参考代码中 merge
函数 \(l=r\) 的部分,有大坑!
代码
#include<iostream>
#include<vector>
#define int long long
using namespace std;
const int maxn=5e5+10,mod=998244353;
int n,m;
int head[maxn],tot;
struct Edge{
int to,nxt;
}e[maxn<<1];
int dep[maxn];
vector<int> p[maxn];
int rt[maxn],cnt;
struct Segment_tree{
struct node{
int l,r,sum,tag;
}tr[maxn<<5];
void pushup(int u){
tr[u].sum=(tr[tr[u].l].sum+tr[tr[u].r].sum)%mod;
return;
}
void pushdown(int u){
if(tr[u].tag==1){
return;
}
tr[tr[u].l].sum=tr[u].tag*tr[tr[u].l].sum%mod;
tr[tr[u].r].sum=tr[u].tag*tr[tr[u].r].sum%mod;
tr[tr[u].l].tag=tr[u].tag*tr[tr[u].l].tag%mod;
tr[tr[u].r].tag=tr[u].tag*tr[tr[u].r].tag%mod;
tr[u].tag=1;
}
void modify(int &u,int l,int r,int pos,int k){
if(u==0){
u=++tot;
}
if(l==r){
tr[u].tag=1;
tr[u].sum=k;
return;
}
int mid=l+r>>1;
pushdown(u);
if(pos<=mid){
modify(tr[u].l,l,mid,pos,k);
}else{
modify(tr[u].r,mid+1,r,pos,k);
}
pushup(u);
return;
}
int query(int u,int l,int r,int ql,int qr){
if(ql<=l&&qr>=r){
return tr[u].sum;
}
int mid=l+r>>1,res=0;
pushdown(u);
if(ql<=mid){
res=(res+query(tr[u].l,l,mid,ql,qr))%mod;
}
if(qr>mid){
res=(res+query(tr[u].r,mid+1,r,ql,qr))%mod;
}
return res;
}
int merge(int x,int y,int l,int r,int &su,int &sv){
if(x==0&&y==0){
return 0;
}
if(x==0){
sv=(sv+tr[y].sum)%mod;
tr[y].tag=(tr[y].tag*su)%mod;
tr[y].sum=(tr[y].sum*su)%mod;
return y;
}
if(y==0){
su=(su+tr[x].sum)%mod;
tr[x].tag=(tr[x].tag*sv)%mod;
tr[x].sum=(tr[x].sum*sv)%mod;
return x;
}
if(l==r){
int cu=tr[x].sum,cv=tr[y].sum;
sv=(sv+cv)%mod;
tr[x].sum=(tr[x].sum*sv+tr[y].sum*su)%mod;
su=(su+cu)%mod;
return x;
}
int mid=l+r>>1;
pushdown(x);
pushdown(y);
tr[x].l=merge(tr[x].l,tr[y].l,l,mid,su,sv);
tr[x].r=merge(tr[x].r,tr[y].r,mid+1,r,su,sv);
pushup(x);
return x;
}
}Tr;
inline void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
inline void dfs(int u,int fa){
dep[u]=dep[fa]+1;
int d=0;
for(int i:p[u]){
d=max(d,dep[i]);
}
Tr.modify(rt[u],0,n,d,1);
int su=0,sv=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa){
continue;
}
dfs(v,u);
su=0;
sv=Tr.query(rt[v],0,n,0,dep[u]);
rt[u]=Tr.merge(rt[u],rt[v],0,n,su,sv);
}
return;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n-1;i++){
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
cin>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
p[v].push_back(u);
}
dfs(1,0);
cout<<Tr.query(rt[1],0,n,0,0)<<endl;
return 0;
}
后记
根本讲不明白啊,总感觉讲得非常糟糕
完结撒花!