【题解】P6773 命运
题面
前言
前置知识:DP,线段树合并
口齿不清的云落又双叒叕上线了……
第四道黑题~
正文
集训期间,shr 说:“这是一道线段树合并优化 DP”
其实 DP 是很好想到的,毕竟求方案数嘛~
但是怎么 DP,或者说怎么设置状态并转移?
比较朴素的做法:依照题意,列出状态
设
条件:对于一组约束
然后……
Game Over!
根本做不下去嘛 qwq
汲取一些失败经验
无法转移,究其原因,是
树形 DP 有一个很强的性质,由儿子向父亲转移
而对于上述 DP,试图进行一次转移
注意到
回到树形 DP 最基础的性质,由子孙转移向祖先
由深度大的向深度低的转移
嗯——深度?
突然发现一个神奇的小性质
对于一组约束集
这个感性理解下即可
那么既然这个性质与
设
加粗部分是无后效性的一个重要设计
对于未赋值的边,权值赋为
可能会有人问,那么
……那这组约束
考虑转移
设
大分讨!
- 边
赋值为
此时
- 边
赋值为
- 如果
中深度最大的约束是 的,那么就和第一种情况大差不差, 子树无法提供更严格的约束,方程如下:
- 否则,
子树会贡献更严格的约束,最深的限制可以有更浅的限制转移而来,方程如下:
把三种情况依照加法原理合并,得出:
就这么个东西,太抽象了
看到一堆西格玛号,云落就提不起继续做下去的欲望
先给它整理一下,有:
注意到西格玛号的结构是极相似的,稍微代换一下
记
原式简化为
至此,我们已经有了一个
考虑优化
注意到
定义两个“变”量
式子是若干项相加,线段树合并可以担任这个艰巨的优化任务
我们尝试将 DP 的第二维用线段树维护
第一项是个常数,第二项可以一边计算一边加入
与模板题(雨天的尾巴)相比,只多了两个系数与一个常数 ——火腿肠
这太抽象了
显然要对
我们看看 merge
函数的具体实现
-
若
都是空结点,略过 -
若
,有 不会变化, 全都是 ,让 更新一下,并给 的 更新 -
若
,同理 -
如果
,直接按照上述式子,把后两项加起来,并更新 -
否则,下传标记,递归地遍历每一棵子树(记得传参
) -
最后将从叶子结点接收到的信息上传,更新答案
-
返回合并后新的线段树的根结点编号
最后一个问题
如何统计答案?
显然
细节处理:
pushdown
需要注意标记下放的顺序,先乘后加
可以注意一下参考代码中 merge
函数
代码
#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;
}
后记
根本讲不明白啊,总感觉讲得非常糟糕
完结撒花!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现