动态DP

动态 \(DP\)

引入:

一般来说,树上 \(DP\) 问题是不做改变的,只用计算一遍就行了。

但是给你说:更改一个点的权值,再去询问你答案。

这时候再算一遍?或者是只处理这个点的父亲部分?但是这样万一成链,那不是时间复杂度又爆了?

所以,我们引入了 动态 \(DP\) 这个概念。

解决问题:

直接说怎么做不好说,还是用例子解决。

例题:

给定一棵 \(n\) 个点的树。\(i\) 号点的点权为 \(a_i\)。有 \(m\) 次操作,每次操作给定 \(u,w\) 表示修改点 \(u\) 的权值为 \(w\)。你需要在每次操作之后求出这棵树的最大权独立集的权值大小。


考虑没有修改做法

最大权独立集可以理解成相连的点不能同时选,因此很容易转移:设 \(dp[i][0/1]\) 表示 \(i\) 点为根选择/不选择 \(i\) 时的最大值,则有转移方程:

\[dp[i][0]=\sum_j max(dp[j][0],dp[j][1]) \]

\[dp[i][1]=\sum_j dp[j][0]+val[i] \]

其中若 \(i\) 为叶子节点,则 \(dp[i][0]=0,dp[i][1]=val[i]\)

答案就是 \(max(dp[1][0],dp[1][1])\).


带上修改

这里,我们使用了重链剖分。因为有以下性质:

  1. 不会形成特别长的链,时间复杂度不会炸
  2. 重链的链尾都是叶子节点,且只有叶子节点无重儿子
  3. 一条重链所在的区间在 \(dfs\) 序上是连续的一段区间,可以用数据结构维护。

我们考虑一些微观问题:在一条链里,怎么支持快速修改和查询这条链的 \(DP\)

我们定义一个 \(g\) 数组:

\(g[i][1]\) 表示 \(i\) 点的轻儿子都不取的最大权独立集

\(g[i][0]\) 表示 \(i\) 点的轻儿子可取可不取形成的最大权独立集。

其中,对于叶子节点 \(g[i][0]=g[i][1]=0\) , \(j\)\(i\) 的重儿子,则有方程:

$$dp[i][0]=g[i][0]+max(dp[j][0],dp[j][1])$$

\[dp[i][1]=g[i][1]+a_i+dp[j][0] \]

第二个式子继续优化: \(g[i][1]\) 表示只考虑轻儿子取自己的最大权独立集,所以变成:

$$dp[i][1]=g[i][1]+dp[j][0]$$


进行区间维护

我们考虑像斐波那契一样,维护这个方程的矩阵:

\[\quad \begin{vmatrix} dp[j][0] & dp[j][1] \end{vmatrix} \quad \]

现在我们要从一个点的重儿子 \(j\) 转移到 \(i\) 上,也就是说我们需要构造出一个转移矩阵使: \( \quad \begin{vmatrix} dp[j][0] & dp[j][1] \end{vmatrix} \quad \) 能转移到 \( \quad \begin{vmatrix} dp[i][0] & dp[i][1] \end{vmatrix} \quad \)

好像并不能进行矩阵乘法。 但是,我们可以重新定义:

《他改变了矩乘》: 我们定义一个新的运算符 \(*\) ,对于矩阵 \(A,B\) ,定义 \(A*B\) 的结果为 \(C\) ,满足:

\[C[i][j]=\max\limits_k (A[i][k]+B[k][j]) \]

实现到代码中,则有:

struct matrix{
    int mat[2][2];
    matrix(){memset(mat,-0X3F,sizeof(mat));}
    inline matrix operator *(matrix a,matrix b){
        matrix c;
        for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
            c.mat[i][j]=max(c.mat[i][j],a.mat[i][k]+b.mat[k][j]);
        return c;
    }
};

为什么这个东西具有结合律?

口胡理解是: \(\max\)\(+\) 操作都是满足结合律的,所以这个也满足。


构造转移矩阵

我们对转移方程进行变形:

\(dp[i][0]=max(dp[j][0]+g[i][0],dp[j][1]+g[i][0])\)

\(dp[i][1]=max(dp[j][0]+g[i][1],-inf)\)

接着把已知的状态和要转移的状态写在一起,未知矩阵由 \(X\) 代替:

\[\quad \begin{vmatrix} dp[j][0] & dp[j][1] \end{vmatrix} \quad ∗ \quad \begin{vmatrix} X \end{vmatrix} \quad = \quad \begin{vmatrix} dp[i][0] & dp[i][1] \end{vmatrix} \quad \]

原来是个 \(1*2\) 的矩阵,形成 \(1*2\) 的矩阵,那么 \(X\) 应该是一个 \(2*2\) 的矩阵。

我们设矩阵左上,右上,左下,右下四个位置分别为: \(x_1,x_2,x_3,x_4\) ,把每个位置对应上去:

\(dp[i][0]=\max(dp[j][0]+x_1,dp[j][1]+x_3)\) , 所以 \(x_1=g[i][0]\) , \(x_3=g[i][0]\)

\(dp[i][1]=\max(dp[j][0]+x_2,dp[j][1]+x_4)\) , 所以 \(x_2=g[i][1]\) , \(x_4=-inf\)

重链剖分剖出的 \(DFS\) 序,由于先访问了链头,所以这个区间中,链头在区间左端,链尾在区间右端。

我们存储的初始信息在叶子节点(也就是链尾)上,因此我们的 “矩阵乘法” 应当是转移矩阵 \((g)\) 在前,要维护的值矩阵 \((dp)\) 在后。

因此,则有:

\[\quad \begin{vmatrix} g[i][0] & g[i][0] \\ g[i][1] & -inf \end{vmatrix} \quad ∗ \quad \begin{vmatrix} dp[j][0] \\ dp[j][1] \end{vmatrix} \quad = \quad \begin{vmatrix} dp[i][0] \\ dp[i][1] \end{vmatrix} \quad \]


总体流程

我们对于一条重链,我们的叶子节点就存储了最初始的值,链上每个节点都对应着一个转移矩阵。

因为这个转移矩阵满足结合率,且和重链信息没关系,对于一条重链,我们可以之间线段树维护区间乘积。

然后到了一条重链链头,因为这个点是它父亲的轻儿子,我们需要更新它父亲节点所在的点的转移矩阵。这样子一直跳到根节点就可以了。

细节部分:

  1. 对于一个点,查找其 \(dp\) 值,需要从这个点一直查到区间的链尾。
    因此,树剖时我们多维护一个 \(end[i]\) ( \(i\) 是一条重链的链头) ,表示\(i\) 为链头的这条链,链尾(叶子节点) 在 \(DFS\) 序上的位置

  2. 更新线段树上某个点的转移矩阵时,传入的如果是矩阵,递归下去常数太大。
    解决方法:在线段树外,维护一个矩阵组 \(val[i]\) , 表示每个点对应的转移矩阵,这样更新时,直接赋值进来即可。

代码:

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5,M=2e5+5,V=4e5+5,INF=0x7F7F7F7F;

struct matrix{
    int mat[2][2];
    matrix(){memset(mat,-0X3F,sizeof(mat));}
    inline matrix operator * (matrix b){
        matrix c;
        for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
            c.mat[i][j]=max(c.mat[i][j],mat[i][k]+b.mat[k][j]);
        return c;
    }
};


int n,m;
int head[N],nxt[M],ver[M],tot;
int A[N];
int fa[N],sizes[N],dep[N],dfn[N],top[N],id[N],son[N],End[N],cnt;
int dp[N][2];
matrix val[N];

struct SegTree{
    int L[V],R[V]; matrix M[V];
    void push_up(int x){M[x]=M[x<<1]*M[x<<1|1];}//更新就更新对应矩阵
    void build(int l,int r,int x){
        L[x]=l; R[x]=r;
        if(l==r){
            M[x]=val[dfn[L[x]]]; return;//赋值方式
        }
        int mid=l+r>>1;
        build(l,mid,x<<1); build(mid+1,r,x<<1|1);
        push_up(x);
    }
    void update(int pos,int x){
        if(L[x]==R[x]){
            M[x]=val[dfn[pos]];
            return;
        }
        int mid=(L[x]+R[x])>>1;
        if(pos<=mid) update(pos,x<<1);
        else update(pos,x<<1|1);
        push_up(x);
    }
    matrix query(int l,int r,int x){
        if(L[x]==l&&R[x]==r) return M[x];
        int mid=(L[x]+R[x])>>1;
        if(r<=mid) return query(l,r,x<<1);
        else if(l>mid) return query(l,r,x<<1|1);
        else return query(l,mid,x<<1)*query(mid+1,r,x<<1|1);//dp即矩阵
    }
}T;

void add(int x,int y){
    ver[++tot]=y; nxt[tot]=head[x];head[x]=tot;
}

void dfs1(int x,int father){
    sizes[x]=1;
    dep[x]=dep[father]+1;
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i];
        if(y==father) continue;
        fa[y]=x; 
        dfs1(y,x);
        sizes[x]+=sizes[y];
        if(sizes[y]>sizes[son[x]]||!son[x]) son[x]=y;

    }
}
void dfs2(int x,int topfather){
    id[x]=++cnt; dfn[cnt]=x;
    top[x]=topfather;
    End[topfather]=max(End[topfather],cnt);//end数组记录链头对应链尾位置
    //更新g数组,即更新val

    dp[x][0]=0; dp[x][1]=A[x];
    val[x].mat[0][0]=val[x].mat[0][1]=0;//记录每个点的矩阵
    val[x].mat[1][0]=A[x];

    if(son[x]!=0){
        dfs2(son[x],topfather);
        dp[x][0]+=max(dp[son[x]][0],dp[son[x]][1]);
        dp[x][1]+=dp[son[x]][0];
    }
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i];
        if(y==fa[x]||y==son[x]) continue;
        dfs2(y,y);

        dp[x][0]+=max(dp[y][0],dp[y][1]);
        dp[x][1]+=dp[y][0];
        val[x].mat[0][0]+=max(dp[y][0],dp[y][1]);//子儿子可选可不选的最大独立集
        val[x].mat[0][1]=val[x].mat[0][0];//同上
        val[x].mat[1][0]+=dp[y][0];//子儿子不能选
    }
}
void update_path(int x,int z){
    val[x].mat[1][0]+=z-A[x];//因为这个地方还存的有子儿子不能选的值,因此不能直接赋值为 z, 
    A[x]=z;//更新对应点的值
    matrix last,now;
    while(x!=0){//不断进行更新
        last=T.query(id[top[x]],End[top[x]],1);//
        T.update(id[x],1);//找到需要更改的地方,进行更新
        now=T.query(id[top[x]],End[top[x]],1);
        x=fa[top[x]];//一直找重链,直到顶端

        val[x].mat[0][0]+=max(now.mat[0][0],now.mat[1][0])-max(last.mat[0][0],last.mat[1][0]);
        //之前可选可不选-现在可选可不选
        val[x].mat[0][1]=val[x].mat[0][0];
        val[x].mat[1][0]+=now.mat[0][0]-last.mat[0][0];
        //之前不选-现在不选
    }
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) scanf("%d",&A[i]);
    for(int i=1,x,y;i<n;i++){
        scanf("%d%d",&x,&y); add(x,y); add(y,x);
    } 
    dfs1(1,0);   
    dfs2(1,1); 
    T.build(1,n,1);
    for(int i=1,x,y;i<=m;i++){
        scanf("%d%d",&x,&y);
        update_path(x,y);
        matrix ans=T.query(id[1],End[1],1);
        printf("%d\n",max(ans.mat[0][0],ans.mat[1][0]));
    }
    system("pause");
    return 0;
}
posted @ 2021-09-27 19:16  Evitagen  阅读(55)  评论(0编辑  收藏  举报