【题解】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\)

大分讨!

  1. \((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}

  1. \((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 函数的具体实现

  1. \(x,y\) 都是空结点,略过

  2. \(x=0,y \neq 0\),有 \(su\) 不会变化,\(f_{x,i}\) 全都是 \(0\),让 \(sv\) 更新一下,并给 \(y\)\(tag,sum\) 更新

  3. \(x \neq 0,y=0\),同理

  4. 如果 \(l=r\),直接按照上述式子,把后两项加起来,并更新 \(su,sv\)

  5. 否则,下传标记,递归地遍历每一棵子树(记得传参 \(su,sv\)

  6. 最后将从叶子结点接收到的信息上传,更新答案

  7. 返回合并后新的线段树的根结点编号

最后一个问题

如何统计答案?

显然 \(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;
}

后记

根本讲不明白啊,总感觉讲得非常糟糕

完结撒花!

posted @ 2025-01-12 14:24  sunxuhetai  阅读(14)  评论(0)    收藏  举报