模版 动态 dp

模版 动态 dp

终于来写这个东西了。。

LG 模版:给定 n 个点的数,点有点权, $ m $ 次修改点权,求修改完后这个树的最大独立集大小。

我们先来考虑朴素的最大独立集的 dp
\[
dp[u][0] = \sum_v max\{dp[v][1],dp[v][0]\}\\dp[u][1] = val[u] + \sum_v dp[v][0]
\]
现在我们就拥有了一个 $ O(nm) $ 的做法,但是明显它不优秀。

既然支持修改,我们可以考虑树剖(或者LCT),现在树被我们剖成了重链和轻链。此时发现我们其实可以先对轻子树算出 dp 值并且累计到当前根,再去算重链的影响,这样有什么好处呢?好处在于重链我们可以单独算,这样的话 dp 转移就是连续的。同时,当你修改一个点,它所影响的也仅仅是它到根的很多重链,不会影响到这路径上虚儿子的贡献。

但是如果按照原来的 dp 转移,修改点权仍然很困难。为此定义了一种新的矩阵乘法,让两个矩阵相乘时的乘法改成加法,加法改成最大值,也就是:
\[
(A\times B)_{i,j} = \text{MAX}_k\{A_{i,k} + B_{k,j}\}
\]
不难发现这样定义矩阵乘法后矩阵乘法仍然满足结合律。

然后我们考虑对于一个点 $ u $ ,假设它已经算完了轻儿子的贡献,计作 $ g[u][0/1] $ ,考虑从重儿子转移,那么这个点的转移矩阵就是
\[
\begin{bmatrix} g[u][0]&g[u][0]\\g[u][1]&-\infin \end{bmatrix}\times \begin{bmatrix}dp[u][0]\\dp[u][1]\end{bmatrix}
\]
为什么呢?考虑 $ dp[u][0] $ 可以从什么转移过来,是由 $ dp[u][0] $ 和 $ dp[u][1] $ 中选择较大的和 $ g[u][0] $ 加起来得到的,而 $ dp[u][1] $ 由 $ dp[u][0] + g[u][1] $ 得到,故最后一个位置填 $ -\infin $。

写到这里感觉 LCT 会比树剖好写的多,所以后面默认使用 LCT 了。

然后我们考虑对每个链维护它的转移矩阵的乘积。而 $ g $ 的维护就是对一个链的顶端的父亲更新 $ g $ 即可,体现在 LCT 中就是一个点维护它 splay 里面的矩阵的积。注意这里的矩阵乘法的顺序,对一个链做的时候应该从上乘到下,所以 pushup 的时候应该先左后中最后右。

如果我们需要得到一个点的 dp 值,必须把它 Access 并且 旋转到根,类似 Qtree V 的做法,不然它内部的值是假的(是子树或者splay子树内的乘积).

修改权值,比较简单的方法是 LCT 直接 旋转到根 然后直接修改 val 和矩阵就可以了。

感觉比 Red Blue Tree 好写,不用拿 BST 维护虚儿子信息。。(然后估计码一天)

代码还是很好看的(虽然调起来很烦)

#include "iostream"
#include "algorithm"
#include "cstring"
#include "cstdio"
using namespace std;
#define MAXN 100006
#define max( a , b ) ( (a) > (b) ? (a) : (b) )
#define inf 0x3f3f3f3f
int n , m;
int w[MAXN];

int head[MAXN] , to[MAXN << 1] , nex[MAXN << 1] , ecn;
void ade( int u , int v ) {
     to[++ ecn] = v , nex[ecn] = head[u] , head[u] = ecn;
}

struct mtrx {
#define N 2
    int A[2][2];
    inline void in( int a , int b ) { A[0][0] = A[0][1] = a , A[1][0] = b , A[1][1] = -inf; }
    inline int re( ) { return max( A[0][0] , A[1][0] ); }
    inline mtrx operator * ( const mtrx& a ) const {
        mtrx ret;
        for( int i = 0 ; i < 2 ; ++ i ) for( int j = 0 ; j < 2 ; ++ j )
            ret.A[i][j] = max( A[i][0] + a.A[0][j] , A[i][1] + a.A[1][j] );
        return ret;
    }
};
int ch[MAXN][2] , fa[MAXN] , dp[MAXN][2];
mtrx G[MAXN];
inline bool notroot( int u ) {
    return ch[fa[u]][0] == u || ch[fa[u]][1] == u;
}
inline void pu( int u ) {
    G[u].in( u[dp][0] , u[dp][1] );
    if( ch[u][0] ) G[u] = G[ch[u][0]] * G[u];
    if( ch[u][1] ) G[u] = G[u] * G[ch[u][1]];
}
inline void rotate( int u ) {
    int f = fa[u] , g = fa[f] , w = ch[f][1]==u , k = ch[u][w^1];
    if(notroot( f )) ch[g][ch[g][1]==f] = u;ch[f][w] = k , ch[u][w^1] = f;
    fa[f] = u , fa[k] = f , fa[u] = g;
    pu( f ) , pu( u );
}
//void rotate( int x ) {
//    int f = fa[x] , g = fa[f] , w = ch[fa[x]][1] == x;
//    int wf = ch[g][1]==f , k = ch[x][w^1];
//    if( notroot(f) ) ch[g][wf] = x; ch[f][w] = k , ch[x][w^1] = f;
//    fa[f] = x , fa[k] = f , fa[x] = g;
//    pu( f ) , pu( x );
//}
void splay( int x ) {
    int f , g;
    while( notroot( x ) ) {
        f = fa[x] , g = fa[f];
        if( notroot( f ) )
            rotate( (ch[f][0]==x)^(ch[g][0]==f) ? x : f );
        rotate( x );
    }
}
void access( int x ) {
    for( int p = 0 ; x ; ( p = x , x = fa[x] ) ) {
        splay(x);
        if( ch[x][1] ) // Heavy -> Light
            x[dp][0] += G[ch[x][1]].re() , x[dp][1] += G[ch[x][1]].A[0][0];
        if( p ) // Light -> Heavy
            x[dp][0] -= G[p].re() , x[dp][1] -= G[p].A[0][0];
        ch[x][1] = p;
        pu(x);
    }
}
void pre( int u , int f ) {
    dp[u][1] = w[u];
    for( int i = head[u] ; i ; i = nex[i] ) {
        int v = to[i];
        if( v == f ) continue;
        pre( v , u );
        fa[v] = u;
        u[dp][0] += max( v[dp][0] , v[dp][1] );
        u[dp][1] += v[dp][0];
    }
    G[u].in( u[dp][0] , u[dp][1] );
}

int main() {
//    freopen("in.in","r",stdin);
    cin >> n >> m;
    for( int i = 1 ; i <= n ; ++ i ) scanf("%d",&w[i]);
    for( int i = 1 , u , v ; i < n ; ++ i )
        scanf("%d%d",&u,&v) , ade( u , v ) , ade( v , u );
    pre( 1 , 1 );
//    for( int i = 1 ; i <= n ; ++ i ) printf("%d\n",G[i].re());
    int u , v;
    while( m-- ) {
        scanf("%d%d",&u,&v);
        access( u );
        splay( u );
        dp[u][1] += v - w[u] , w[u] = v;
        pu( u );
        printf("%d\n",G[u].re());
//        for( int i = 1 ; i <= n ; ++ i ) printf("%d ",fa[i]);
//        puts("");
    }
}

一个例题 NOIP 2018 保卫王国

其实就是板子题,每次询问,强制选本质上就是权值 inf 强制不选择就是 -inf

中间挂了几次。。这题 longlong 得注意。。(矩阵返回值没开LongLong然后wa了两发。。)

#include "iostream"
#include "algorithm"
#include "cstring"
#include "cstdio"
using namespace std;
#define MAXN 400006
#define max( a , b ) ( (a) > (b) ? (a) : (b) )
#define inf (1ll<<60)
int n , m;
int w[MAXN];

int head[MAXN] , to[MAXN << 1] , nex[MAXN << 1] , ecn;
void ade( int u , int v ) {
    to[++ ecn] = v , nex[ecn] = head[u] , head[u] = ecn;
}

struct mtrx {
#define N 2
    long long A[2][2];
    inline void in( long long a , long long b ) { A[0][0] = A[0][1] = a , A[1][0] = b , A[1][1] = -inf; }
    inline long long re( ) { return max( A[0][0] , A[1][0] ); }
    inline mtrx operator * ( const mtrx& a ) const {
        mtrx ret;
        for( int i = 0 ; i < 2 ; ++ i ) for( int j = 0 ; j < 2 ; ++ j )
                ret.A[i][j] = max( A[i][0] + a.A[0][j] , A[i][1] + a.A[1][j] );
        return ret;
    }
};
int ch[MAXN][2] , fa[MAXN]; long long dp[MAXN][2];
mtrx G[MAXN];
inline bool notroot( int u ) {
    return ch[fa[u]][0] == u || ch[fa[u]][1] == u;
}
inline void pu( int u ) {
    G[u].in( u[dp][0] , u[dp][1] );
    if( ch[u][0] ) G[u] = G[ch[u][0]] * G[u];
    if( ch[u][1] ) G[u] = G[u] * G[ch[u][1]];
}
inline void rotate( int u ) {
    int f = fa[u] , g = fa[f] , w = ch[f][1]==u , k = ch[u][w^1];
    if(notroot( f )) ch[g][ch[g][1]==f] = u;ch[f][w] = k , ch[u][w^1] = f;
    fa[f] = u , fa[k] = f , fa[u] = g;
    pu( f ) , pu( u );
}
void splay( int x ) {
    int f , g;
    while( notroot( x ) ) {
        f = fa[x] , g = fa[f];
        if( notroot( f ) )
            rotate( (ch[f][0]==x)^(ch[g][0]==f) ? x : f );
        rotate( x );
    }
}
void access( int x ) {
    for( int p = 0 ; x ; ( p = x , x = fa[x] ) ) {
        splay(x);
        if( ch[x][1] ) // Heavy -> Light
            x[dp][0] += G[ch[x][1]].re() , x[dp][1] += G[ch[x][1]].A[0][0];
        if( p ) // Light -> Heavy
            x[dp][0] -= G[p].re() , x[dp][1] -= G[p].A[0][0];
        ch[x][1] = p;
        pu(x);
    }
}
void pre( int u , int f ) {
    dp[u][1] = w[u];
    for( int i = head[u] ; i ; i = nex[i] ) {
        int v = to[i];
        if( v == f ) continue;
        pre( v , u );
        fa[v] = u;
        u[dp][0] += max( v[dp][0] , v[dp][1] );
        u[dp][1] += v[dp][0];
    }
    G[u].in( u[dp][0] , u[dp][1] );
}
long long S;
void mdfy( int u , long long x ) {
    access( u ) , splay( u );
    dp[u][1] += x;
    pu( u );
}
int main() {
    freopen("defense.in","r",stdin);
    freopen("defense.out","w",stdout);
    cin >> n >> m;
    scanf("%*s");
    for( int i = 1 ; i <= n ; ++ i ) scanf("%d",&w[i]) , S += w[i];
    for( int i = 1 , u , v ; i < n ; ++ i )
        scanf("%d%d",&u,&v) , ade( u , v ) , ade( v , u );
    pre( 1 , 1 );
//    for( int i = 1 ; i <= n ; ++ i ) printf("%d\n",G[i].re());
    int u , v , x1 , x2;
    while( m-- ) {
        scanf("%d%d%d%d",&u,&x1,&v,&x2);
        mdfy( u , x1 ? -inf : inf ) , mdfy( v , x2 ? -inf : inf );
        S += ( ( x1 ^ 1 ) + ( x2 ^ 1 ) ) * inf;
        printf("%lld\n",S - G[v].re() > 1e10 ? -1 : S - G[v].re());
        S -= ( ( x1 ^ 1 ) + ( x2 ^ 1 ) ) * inf;
        mdfy( u , x1 ? inf : -inf ) , mdfy( v , x2 ? inf : -inf );
    }
}

原文地址:https://www.cnblogs.com/yijan/p/ddp.html

时间: 2024-10-13 10:24:54

模版 动态 dp的相关文章

4712: 洪水 基于链分治的动态DP

国际惯例的题面:看起来很神的样子......如果我说这是动态DP的板子题你敢信?基于链分治的动态DP?说人话,就是树链剖分线段树维护DP.既然是DP,那就先得有转移方程.我们令f[i]表示让i子树中的叶子节点全部与根不联通,所需要的最小代价,v[i]为输入的点权.显然f[i]=min(v[i],sigma(f[soni])),边界条件是,如果i是叶子节点,则f[i]=v[i].我们需要用链分治去维护这个DP,所以要把DP拆成重链和轻链独立的形式.我们还是用f[i]表示让i子树中的叶子节点全部与根

[动态dp]线段树维护转移矩阵

背景:czy上课讲了新知识,从未见到过,总结一下. 所谓动态dp,是在动态规划的基础上,需要维护一些修改操作的算法. 这类题目分为如下三个步骤:(都是对于常系数齐次递推问题) 1先不考虑修改,不考虑区间,直接列出整个区间的dp方程.这个是基础,动态dp无论如何还是dp(这一步是一般是重点) 2.列出转移矩阵.由于有很多修改操作,我们将数据集中在一起处理,还可以利用矩阵结合律,并且区间比较好提取,(找一段矩阵就好了),修改也方便. 3.线段树维护矩阵.对于修改,我们就是在矩阵上进行修改,对于不同的

UOJ268 [清华集训2016] 数据交互 【动态DP】【堆】【树链剖分】【线段树】

题目分析: 不难发现可以用动态DP做. 题目相当于是要我求一条路径,所有与路径有交的链的代价加入进去,要求代价最大. 我们把链的代价分成两个部分:一部分将代价加入$LCA$之中,用$g$数组保存:另一部分将代价加在整条链上,用$d$数组保存. 这时候我们可以发现,一条从$u$到$v$的路径的代价相当于是$d[LCA(u,v)]+\sum_{x \in edge(u,v)}g[x]$. 如果是静态的,可以用树形DP解决. 看过<神奇的子图>的同学都知道,叶子结点是从它的儿子中取两个最大的出来,所

bzoj 4712 洪水——动态DP

题目:https://www.lydsy.com/JudgeOnline/problem.php?id=4712 因为作为动态DP练习而找到,所以就用动态DP做了,也没管那种二分的方法. 感觉理解似乎加深了. 果然初始权值也都是非负的. 所以 dp[cr] 表示当前子树与自己的叶子都断开了的最小代价,则 dp[cr]=min{ sigma dp[v] , w[cr] }(v是cr的直接孩子). 但这样的话,修改的时候需要把自己到根的路径都走一遍.不过查询是O(1)的,所以考虑分配一下. 走到根的

uoj#268. 【清华集训2016】数据交互(动态dp+堆)

传送门 动态dp我好像还真没咋做过--通过一个上午的努力光荣的获得了所有AC的人里面的倒数rk3 首先有一个我一点也不觉得显然的定理,如果两条路径相交,那么一定有一条路径的\(LCA\)在另一条路径上 于是我们可以对于每一个点记录两个值,一个\(a_i\)表示\(LCA\)在\(i\)点的所有路径的权值之和,一个是\(b_i\),表示经过点\(i\)且\(LCA\)不在点\(i\)的所有路径的权值之和 那么对于一条路径\((u,v)\),它的权值就是\(b_{LCA(u,v)}+\sum_{i\

回文串 --- 动态dp UVA 11584

题目链接: https://cn.vjudge.net/problem/34398/origin 本题的大意其实很简单,就是找回文串,大致的思路如下: 1. 确定一个回文串,这里用到了自定义的check函数原理如下: 传入le, ri两个值(定义从1开始), s+1 = aaadbccb. a a a d b c c b 1 2 3 4 5 6 7 8 比如,le = 5, ri = 8. 则s[5] == s[8]成立 le++ ri-- 再比较 s[6] == s[7]? 成立 le++,

SPOJ GSS3 (动态dp)

题意 题目链接 Sol 这题可以动态dp做. 设\(f[i]\)表示以\(i\)为结尾的最大子段和,\(g[i]\)表示\(1-i\)的最大子段和 那么 \(f[i] = max(f[i - 1] + a[i], a[i])\) \(g[i] = max(g[i - 1], f[i])\) 发现只跟前一项有关,而且\(g[i]从\)f[i]$转移过来的那一项可以直接拆开 那么构造矩阵 \[ \begin{bmatrix} a_{i} & -\infty & \dots a_{i} \\ a

LG4719 【模板】动态dp

题意 题目描述 给定一棵\(n\)个点的树,点带点权. 有\(m\)次操作,每次操作给定\(x,y\),表示修改点\(x\)的权值为\(y\). 你需要在每次操作之后求出这棵树的最大权独立集的权值大小. 输入输出格式 输入格式: 第一行,\(n,m\),分别代表点数和操作数. 第二行,\(V_1,V_2,...,V_n\),代表\(n\)个点的权值. 接下来\(n-1\)行,\(x,y\),描述这棵树的\(n-1\)条边. 接下来\(m\)行,\(x,y\),修改点\(x\)的权值为\(y\).

动态 DP 总结

目录 例题1:模拟赛题 代码: 例题2 例题3:带修改树上最大独立集. 注:部分参考 https://www.luogu.org/blog/gkxx-is-here/what-the-hell-is-ddp 动态DP,就是一个十分简单的DP加了一个修改操作. 先看些例题: 例题1:模拟赛题 [问题描述] 某高校教学楼有 n 层,每一层有 2 个门,每层的两个门和下一层之间的两个门之 间各有一条路(共 4 条),相同层的 2 个门之间没有路.现给出如下两个操作: 0 x y : 查询第 x 层到第