【学习笔记】DP 优化 2:动态 DP
概述#
又名 DDP,是一种在普通线性 DP 的基础上,可以解决带修或区间询问的 DP。
矩阵维护转移#
用矩阵描述 DP 数组间的转移,由于矩阵具有结合律,可以先求出若干矩阵运算的结果再把 DP 初始值代入求结果。
广义矩阵乘法常见的有 。
序列上 DDP#
比较简单,多数借助线段树维护区间的 转移矩阵,修改或查询时只需要修改 位置的转移矩阵,复杂度通常是 ,其中 是矩阵大小。
树上 DDP#
比较困难,以 模板题 为例。
求树上最大独立集的 DP 比较简单,转移形如:
如果这样写出 转移矩阵,每次查询需要枚举儿子,不能接受。
另设 为不考虑 的重儿子的答案,那么转移:
这样转移矩阵:
于是每次修改只需要修改 个转移矩阵,具体在轻边位置,也就是是经过的链顶向其父亲转移的矩阵。
可以先 DP 求出初始的 ,在线段树上构造出转移矩阵。
考虑每次修改的操作,设当前修改位置 ,对链顶 向其父亲 转移的矩阵中,就需要改变 来自 的部分。这个贡献具有可减性,因此可以先不修改求出 ,在 减去,之后再修改 位置的矩阵,使得整条重链发生改变,此时再求出 加到 位置。对于 的重链,直接修改就是正确的。
这个操作需要求 ,观察转移,发现只需要得到这条重链上的转移矩阵,于是要对每个重链维护链底 。
由于这个过程是从下至上,线段树上编号由大到小,所以维护矩阵应当是右乘。
(要注意区分修改位置是 ,但产生的一系列贡献到了 ,使得 的矩阵部分发生改变。)
点击查看代码
struct Matrix{
int a[2][2];
Matrix(){
a[0][0]=a[0][1]=a[1][0]=a[1][1]=0;
}
Matrix operator*(const Matrix &rhs)const{
Matrix res;
res.a[0][0]=max(a[0][0]+rhs.a[0][0],a[0][1]+rhs.a[1][0]);
res.a[0][1]=max(a[0][0]+rhs.a[0][1],a[0][1]+rhs.a[1][1]);
res.a[1][0]=max(a[1][0]+rhs.a[0][0],a[1][1]+rhs.a[1][0]);
res.a[1][1]=max(a[1][0]+rhs.a[0][1],a[1][1]+rhs.a[1][1]);
return res;
}
};
int n,m;
int a[maxn];
struct edge{
int to,nxt;
}e[maxn<<1];
int head[maxn],cnt;
inline void add_edge(int u,int v){
e[++cnt].to=v,e[cnt].nxt=head[u],head[u]=cnt;
e[++cnt].to=u,e[cnt].nxt=head[v],head[v]=cnt;
}
int fa[maxn],dep[maxn],siz[maxn],son[maxn];
int top[maxn],ed[maxn],dfn[maxn],dfncnt,id[maxn];
int F[maxn][2],G[maxn][2];
void dfs1(int u,int f,int d){
fa[u]=f,dep[u]=d,siz[u]=1;
F[u][1]=a[u];
int maxson=-1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==f) continue;
dfs1(v,u,d+1);
siz[u]+=siz[v];
F[u][0]+=max(F[v][0],F[v][1]),F[u][1]+=F[v][0];
if(siz[v]>maxson) son[u]=v,maxson=siz[v];
}
}
void dfs2(int u,int t){
top[u]=t,ed[t]=u,dfn[u]=++dfncnt,id[dfncnt]=u;
G[u][1]=a[u];
if(!son[u]) return;
dfs2(son[u],t);
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
G[u][0]+=max(F[v][0],F[v][1]),G[u][1]+=F[v][0];
}
}
struct SegmentTree{
#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
Matrix Mat[maxn<<2];
inline void push_up(int rt){
Mat[rt]=Mat[rt<<1|1]*Mat[rt<<1];
}
void build(int rt,int l,int r){
if(l==r){
Mat[rt].a[0][0]=Mat[rt].a[1][0]=G[id[l]][0];
Mat[rt].a[0][1]=G[id[l]][1],Mat[rt].a[1][1]=-inf;
return;
}
build(lson),build(rson);
push_up(rt);
}
void update(int rt,int l,int r,int p){
if(l==r){
Mat[rt].a[0][0]=Mat[rt].a[1][0]=G[id[l]][0];
Mat[rt].a[0][1]=G[id[l]][1],Mat[rt].a[1][1]=-inf;
return;
}
if(p<=mid) update(lson,p);
else update(rson,p);
push_up(rt);
}
Matrix query(int rt,int l,int r,int pl,int pr){
if(pl<=l&&r<=pr) return Mat[rt];
if(pr<=mid) return query(lson,pl,pr);
else if(pl>mid) return query(rson,pl,pr);
else return query(rson,pl,pr)*query(lson,pl,pr);
}
#undef mid
#undef lson
#undef rson
}S;
inline void update(int u,int k){
G[u][1]+=k-a[u],a[u]=k;
while(top[u]!=1){
Matrix now=S.query(1,1,n,dfn[top[u]],dfn[ed[top[u]]]);
int f0=now.a[0][0],f1=now.a[0][1];
G[fa[top[u]]][0]-=max(f0,f1),G[fa[top[u]]][1]-=f0;
S.update(1,1,n,dfn[u]);
now=S.query(1,1,n,dfn[top[u]],dfn[ed[top[u]]]);
f0=now.a[0][0],f1=now.a[0][1];
G[fa[top[u]]][0]+=max(f0,f1),G[fa[top[u]]][1]+=f0;
u=fa[top[u]];
}
S.update(1,1,n,dfn[u]);
Matrix now=S.query(1,1,n,dfn[1],dfn[ed[1]]);
int f0=now.a[0][0],f1=now.a[0][1];
printf("%d\n",max(f0,f1));
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;++i) a[i]=read();
for(int i=1;i<n;++i){
int u=read(),v=read();
add_edge(u,v);
}
dfs1(1,0,0);
dfs2(1,1);
S.build(1,1,n);
for(int i=1;i<=m;++i){
int u=read(),k=read();
update(u,k);
}
return 0;
}
例题#
CodeForces-750E New Year and Old Subsequence *2600#
朴素线性转移很好写出,状态设计成和 的匹配情况以及是否匹配到了 即可。
区间查询放到线段树上,矩阵是一个 ,不转移的位置设成 即可。
Luogu-P7359 JZOI-1 旅行#
设 分别表示从父亲到儿子/从儿子到父亲走陆路/水路的最短时间,转移比较朴素,是 形式。
每次查询是对链查询,所以在儿子位置记录转移矩阵,倍增合并路径上所有矩阵就行了,复杂度 ,。
Luogu-P8820 CSP-S 2022 数据传输#
直接求路径点权和。
只会在路径上走,那么相当于每个位置可以选可以不选,只不过相邻两个位置至少选一个,于是设 表示到 节点且在不增加代价的情况下已经走了几步,即 为在 增加了代价, 为在 的儿子位置增加了代价。转移比较简单:
直接倍增维护矩阵即可,算答案可以在 处计算。
的特殊之处在于可能取到不在路径上的点,注意到偏离路径的目的一定是到达无法一次到达的节点,且一去一回需要多走 步,而每次最多走 步,于是偏离路径只有一种情况会出现:在路径上移动 步,且在中点位置偏离一次。
模仿 的 DP 定义,由于偏离后一定还需要再回到原路径上,因此可以认为偏离之后已经走了 步用在回到路径上,这样转移方程:
其中 是 除 以外儿子代价的最小值,也就是偏离路径的代价。当转移到 位置时,由于 不在路径上,也有可能作为偏离路径的节点,这里转移矩阵要作更改,只需要在倍增最后一步特殊处理一下即可。
复杂度 。
参考资料#
作者:SoyTony
出处:https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_DP_Optimization_2.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效