Loading

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

Page Views Count

概述

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

矩阵维护转移

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

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

序列上 DDP

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

树上 DDP

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

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

\[\begin{cases} f_{u,0}=\sum_{v\in \mathrm{son}(u)} \max(f_{v,0},f_{v,1})\\ f_{u,1}=a_u+\sum_{v\in \mathrm{son}(u)} f_{v,0} \end{cases}\]

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

另设 \(g_{u,0/1}\) 为不考虑 \(u\) 的重儿子的答案,那么转移:

\[\begin{cases} g_{u,0}=\sum_{v\in \mathrm{son}(u)\land v\neq son_u} \max(f_{v,0},f_{v,1})\\ g_{u,1}=a_u+\sum_{v\in \mathrm{son}(u)\land v\neq son_u} f_{v,0}\\ f_{u,0}=g_{u,0}+\max(f_{son_u,0},f_{son_u,1})\\ f_{u,1}=g_{u,1}+f_{son_u,0} \end{cases}\]

这样转移矩阵:

\[\begin{bmatrix}f_{son_u,0}&f_{son_u,1}\end{bmatrix}\times \begin{bmatrix}g_{u,0}&g_{u,1}\\g_{u,0}&-\infty\end{bmatrix}\to \begin{bmatrix}f_{u,0}&f_{u,1}\end{bmatrix} \]

于是每次修改只需要修改 \(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\) 的儿子位置增加了代价。转移比较简单:

\[\begin{cases} f_{u,0}=a_u+\min(f_{v,0},f_{v,1})\\ f_{u,1}=f_{v,0} \end{cases}\]

直接倍增维护矩阵即可,算答案可以在 \(\mathrm{LCA}\) 处计算。

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

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

\[\begin{cases} f_{u,0}=a_u+\min(f_{v,0},f_{v,1},f_{v,2})\\ f_{u,1}=\min(f_{v,0},f_{v,1}+mn_u)\\ f_{u,2}=f_{v,1} \end{cases}\]

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

复杂度 \(O(k^3n\log n)\)

参考资料

posted @ 2023-07-04 20:52  SoyTony  阅读(69)  评论(0编辑  收藏  举报