【学习笔记】DP 优化 2:动态 DP
概述
又名 DDP,是一种在普通线性 DP 的基础上,可以解决带修或区间询问的 DP。
矩阵维护转移
用矩阵描述 DP 数组间的转移,由于矩阵具有结合律,可以先求出若干矩阵运算的结果再把 DP 初始值代入求结果。
广义矩阵乘法常见的有 \((+,\times),(\max/\min,+),(\gcd,\times)\)。
序列上 DDP
比较简单,多数借助线段树维护区间的 \([l,r]\) 转移矩阵,修改或查询时只需要修改 \(\log n\) 位置的转移矩阵,复杂度通常是 \(O(k^3n\log n)\),其中 \(k\) 是矩阵大小。
树上 DDP
比较困难,以 模板题 为例。
求树上最大独立集的 DP 比较简单,转移形如:
如果这样写出 \((\max,+)\) 转移矩阵,每次查询需要枚举儿子,不能接受。
另设 \(g_{u,0/1}\) 为不考虑 \(u\) 的重儿子的答案,那么转移:
这样转移矩阵:
于是每次修改只需要修改 \(O(\log n)\) 个转移矩阵,具体在轻边位置,也就是是经过的链顶向其父亲转移的矩阵。
可以先 DP 求出初始的 \(f,g\),在线段树上构造出转移矩阵。
考虑每次修改的操作,设当前修改位置 \(u\),对链顶 \(top\) 向其父亲 \(fa_{top}\) 转移的矩阵中,就需要改变 \(g_{fa_{top}}\) 来自 \(f_{top}\) 的部分。这个贡献具有可减性,因此可以先不修改求出 \(f_{top}\),在 \(g_{fa_{top}}\) 减去,之后再修改 \(u\) 位置的矩阵,使得整条重链发生改变,此时再求出 \(f_{top}\) 加到 \(g_{fa_{top}}\) 位置。对于 \(top=1\) 的重链,直接修改就是正确的。
这个操作需要求 \(f_{top}\),观察转移,发现只需要得到这条重链上的转移矩阵,于是要对每个重链维护链底 \(ed_{top}\)。
由于这个过程是从下至上,线段树上编号由大到小,所以维护矩阵应当是右乘。
(要注意区分修改位置是 \(u\),但产生的一系列贡献到了 \(top\),使得 \(fa_{top}\) 的矩阵部分发生改变。)
点击查看代码
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
朴素线性转移很好写出,状态设计成和 \(2016\) 的匹配情况以及是否匹配到了 \(2017\) 即可。
区间查询放到线段树上,矩阵是一个 \((\min,+)\),不转移的位置设成 \(\infty\) 即可。
Luogu-P7359 JZOI-1 旅行
设 \(f_{u,0/1},g_{u,0/1}\) 分别表示从父亲到儿子/从儿子到父亲走陆路/水路的最短时间,转移比较朴素,是 \((\max,+)\) 形式。
每次查询是对链查询,所以在儿子位置记录转移矩阵,倍增合并路径上所有矩阵就行了,复杂度 \(O(k^3n\log n)\),\(k=2\)。
Luogu-P8820 CSP-S 2022 数据传输
\(k=1\) 直接求路径点权和。
\(k=2\) 只会在路径上走,那么相当于每个位置可以选可以不选,只不过相邻两个位置至少选一个,于是设 \(f_{u,0/1}\) 表示到 \(u\) 节点且在不增加代价的情况下已经走了几步,即 \(f_{u,0}\) 为在 \(u\) 增加了代价,\(f_{u,1}\) 为在 \(u\) 的儿子位置增加了代价。转移比较简单:
直接倍增维护矩阵即可,算答案可以在 \(\mathrm{LCA}\) 处计算。
\(k=3\) 的特殊之处在于可能取到不在路径上的点,注意到偏离路径的目的一定是到达无法一次到达的节点,且一去一回需要多走 \(2\) 步,而每次最多走 \(k=3\) 步,于是偏离路径只有一种情况会出现:在路径上移动 \(4\) 步,且在中点位置偏离一次。
模仿 \(k=2\) 的 DP 定义,由于偏离后一定还需要再回到原路径上,因此可以认为偏离之后已经走了 \(1\) 步用在回到路径上,这样转移方程:
其中 \(mn_u\) 是 \(u\) 除 \(v\) 以外儿子代价的最小值,也就是偏离路径的代价。当转移到 \(\mathrm{LCA}\) 位置时,由于 \(\mathrm{LCA}\) 不在路径上,也有可能作为偏离路径的节点,这里转移矩阵要作更改,只需要在倍增最后一步特殊处理一下即可。
复杂度 \(O(k^3n\log n)\)。