【学习笔记】DP 优化 2:动态 DP

Page Views Count

概述#

又名 DDP,是一种在普通线性 DP 的基础上,可以解决带修或区间询问的 DP。

矩阵维护转移#

用矩阵描述 DP 数组间的转移,由于矩阵具有结合律,可以先求出若干矩阵运算的结果再把 DP 初始值代入求结果。

广义矩阵乘法常见的有 (+,×),(max/min,+),(gcd,×)

序列上 DDP#

比较简单,多数借助线段树维护区间的 [l,r] 转移矩阵,修改或查询时只需要修改 logn 位置的转移矩阵,复杂度通常是 O(k3nlogn),其中 k 是矩阵大小。

树上 DDP#

比较困难,以 模板题 为例。

求树上最大独立集的 DP 比较简单,转移形如:

{fu,0=vson(u)max(fv,0,fv,1)fu,1=au+vson(u)fv,0

如果这样写出 (max,+) 转移矩阵,每次查询需要枚举儿子,不能接受。

另设 gu,0/1 为不考虑 u 的重儿子的答案,那么转移:

{gu,0=vson(u)vsonumax(fv,0,fv,1)gu,1=au+vson(u)vsonufv,0fu,0=gu,0+max(fsonu,0,fsonu,1)fu,1=gu,1+fsonu,0

这样转移矩阵:

[fsonu,0fsonu,1]×[gu,0gu,1gu,0][fu,0fu,1]

于是每次修改只需要修改 O(logn) 个转移矩阵,具体在轻边位置,也就是是经过的链顶向其父亲转移的矩阵。

可以先 DP 求出初始的 f,g,在线段树上构造出转移矩阵。

考虑每次修改的操作,设当前修改位置 u,对链顶 top 向其父亲 fatop 转移的矩阵中,就需要改变 gfatop 来自 ftop 的部分。这个贡献具有可减性,因此可以先不修改求出 ftop,在 gfatop 减去,之后再修改 u 位置的矩阵,使得整条重链发生改变,此时再求出 ftop 加到 gfatop 位置。对于 top=1 的重链,直接修改就是正确的。

这个操作需要求 ftop,观察转移,发现只需要得到这条重链上的转移矩阵,于是要对每个重链维护链底 edtop

由于这个过程是从下至上,线段树上编号由大到小,所以维护矩阵应当是右乘。

(要注意区分修改位置是 u,但产生的一系列贡献到了 top,使得 fatop 的矩阵部分发生改变。)

点击查看代码
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,+),不转移的位置设成 即可。

Luogu-P7359 JZOI-1 旅行#

fu,0/1,gu,0/1 分别表示从父亲到儿子/从儿子到父亲走陆路/水路的最短时间,转移比较朴素,是 (max,+) 形式。

每次查询是对链查询,所以在儿子位置记录转移矩阵,倍增合并路径上所有矩阵就行了,复杂度 O(k3nlogn)k=2

Luogu-P8820 CSP-S 2022 数据传输#

k=1 直接求路径点权和。

k=2 只会在路径上走,那么相当于每个位置可以选可以不选,只不过相邻两个位置至少选一个,于是设 fu,0/1 表示到 u 节点且在不增加代价的情况下已经走了几步,即 fu,0 为在 u 增加了代价,fu,1 为在 u 的儿子位置增加了代价。转移比较简单:

{fu,0=au+min(fv,0,fv,1)fu,1=fv,0

直接倍增维护矩阵即可,算答案可以在 LCA 处计算。

k=3 的特殊之处在于可能取到不在路径上的点,注意到偏离路径的目的一定是到达无法一次到达的节点,且一去一回需要多走 2 步,而每次最多走 k=3 步,于是偏离路径只有一种情况会出现:在路径上移动 4 步,且在中点位置偏离一次。

模仿 k=2 的 DP 定义,由于偏离后一定还需要再回到原路径上,因此可以认为偏离之后已经走了 1 步用在回到路径上,这样转移方程:

{fu,0=au+min(fv,0,fv,1,fv,2)fu,1=min(fv,0,fv,1+mnu)fu,2=fv,1

其中 mnuuv 以外儿子代价的最小值,也就是偏离路径的代价。当转移到 LCA 位置时,由于 LCA 不在路径上,也有可能作为偏离路径的节点,这里转移矩阵要作更改,只需要在倍增最后一步特殊处理一下即可。

复杂度 O(k3nlogn)

参考资料#

作者:SoyTony

出处:https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_DP_Optimization_2.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   SoyTony  阅读(132)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示