动态 DP 学习笔记

似乎 NOIP2019D2T3 把这个东西宣传的很到位啊。

我才不会告诉你我已经学过一遍了 早就忘了。

bzoj4712 洪水 为例。


首先我们有一个暴力 DP 的式子。

\(dp[x]\) 表示以 \(x\)为根的子树的最小代价,\(v[x]\) 表示每个点的权值。

于是转移就是:
\[ dp[x] = \min(v[x], \sum_{y\in child_x} dp[y]) \]

不用动态 DP 的做法

实际上这道题有一个不用动态 DP 的做法。

不想看的可以直接跳过。

戳我跳过

因为保证了所有的 \(to\) 都是非负数,那么也就是说,所有的 \(v[x]\) 都是只增不减的,那么也就意味着,所有的 \(dp[x]\) 也是只增不减的。

那么,每个点之多发生一次 \(\sum\limits_{y\in child_x} dp[y]\)\(\leq v[x]\) 变成 \(> v[x]\)(被修改的那个点除外)。

于是这种情况也是最多发生 \(n+m\) 次的。加上 \(m\) 是因为每一次修改会有可能破坏一次。

所以如果我们可以得到一种很好的想法:找到所有的 \(\sum\limits_{y\in child_x} dp[y]\)\(v[x]\) 的大小关系发生改变的点,然后暴力修改。

可以发现,从被修改的点的 \(dp\) 值被增加的量开始,其所有的祖先,最下面会有一段和这个增量相同。直到某一个点的 \(\sum\limits_{y\in child_x} dp[y]\)\(v[x]\) 的大小关系发生改变,或者到 \(\sum\limits_{y\in child_x} dp[y] \geq v[x]\) 停止。所以我们要做的就是通过数据结构维护最近的一次 \(\sum\limits_{y\in child_x} dp[y]\)\(v[x]\) 的大小关系发生改变的情况。

可以直接维护每个点的 \(v[x] - \sum\limits_{y\in child_x} dp[y]\) 的结果,然后进行树链剖分, 在重链上可以直接进行线段树二分找出这样的点。

于是整个题目可以在总 \(O((n+m)\log^2 n)\) 的复杂度完成。

问题

某毒瘤:“听说你可以用均摊分析的方法过掉?”

然后轻轻的敲下了 vim problem.tex,然后找到 n<=200000,保证任意to都为非负数 这一行,将光标停在了那个逗号上,优雅地按下了可怕的 d$,那句“保证任意to都为非负数”瞬间消失得无影无踪。

动态 DP

如上文所述,如果不能保证 \(to\) 为非负数,那么我们之前的均摊分析就不成立,复杂度就直接炸没影了。

我们考虑使用一种叫作动态 DP 的科技来解决这个问题。

我们回顾一下上面的 DP 式子:
\[ dp[x] = \min(v[x], \sum_{y\in child_x} dp[y]) \]
如果我们把这棵树进行树链剖分的话,我们先定义一些方便的符号:\(child_x\) 表示 \(x\) 的所有孩子的集合;\(son_x\) 表示 \(x\) 的重儿子(这个名字怎么充满的封建的味道),\(light_x\) 表示 \(x\) 的轻儿子集合。

那么,如果我们令 \(h[x]\) 表示 \(x\) 的所有轻儿子的 \(dp\) 值之和,即 \(h[x] = \sum\limits_{y\in light_x} dp[y]\)。则原式可以改写为:
\[ dp[x] = \min(v[x], h[x]+dp[son_x]) \]
类似于一般的递推,我们考虑用一种新式的矩阵来表示这种关系:

对于两个矩阵 \(A\)\(B\),定义一种新的乘法运算,令 \(C = A\times B\),则
\[ C_{i, j} = \min_{k=1}^n A_{i, k} + B_{k, j} \]
那么,我们上面的运算可以被用矩阵乘法的形式表示为:
\[ \begin{bmatrix}h[x] & v[x] \\ 0 & 0\end{bmatrix} \times \begin{bmatrix}dp[son_x] \\ 0\end{bmatrix} = \begin{bmatrix}dp[x] \\ 0\end{bmatrix} \]
类似于矩阵快速幂,我们来证明一下我们定义的这一种矩乘具有结合律。

对于矩阵大小为 \(a \times b\) 的矩阵 \(A\),大小为 \(b \times c\) 的矩阵 \(B\),大小为 \(c\times d\) 的矩阵 \(C\),求证:\((A\times B)\times C = A \times (B \times C)\)

证明:

\(E = A\times B\)\(F = B \times C\)


\[ E_{i, j} = \min_{k=1}^b A_{i, k} + B_{k, j}\\ F_{i, j} = \min_{k=1}^c B_{i, k} + C_{k, j} \]
然后
\[ E\times C = \min_{l=1}^c\{\min_{k=1}^b A_{i, k} + B_{k, l} + C_{l,j}\}\\ \begin{align*} A\times F &= \min_{l=1}^b\{A_{i,l} + \min_{k=1}^c B_{i, k} + C_{k, j}\}\\ &= \min_{k=1}^c\{\min_{l=1}^b A_{i, l} + B_{i, k} + C_{k, j}\}\\ &= \min_{l=1}^c\{\min_{k=1}^b A_{i, k} + B_{i, l} + C_{l, j}\}\\ \end{align*} \]
容易发现 \(E\times C = A \times F\),即 \((A\times B) \times C = A \times (B \times C)\)


\(V_x = \begin{bmatrix}h[x]&v[x]\\0&0 \end{bmatrix}\)

我们可以发现,如果我们把一条重链上面的点的 \(V\) 矩阵从某一个点 \(x\) 开始,按照深度从上到下的顺序乘在一起,最后再乘上一个 \(\begin{bmatrix}0\\0\end{bmatrix}\),那么我们就可以得到重链顶端的 \(x\)\(dp\) 值了。

上面就是查询的方法了。


下面我们考虑修改应该怎么做。

修改会影响到的 \(dp\) 值是从根到 \(x\) 的所有点的。——但是我们实际上不需要考虑所有的 \(dp\) 值,因为 \(dp\) 值本身没有被存储下来。需要考虑的是影响到了哪些 \(h[x]\)

每一个点的重儿子的 \(dp\) 值不影响 \(h[x]\) 值,被影响的只有每一条重链之间的那一个轻边。所以直接在树剖向上跳的时候用新的 \(dp\) 值更新一下就好了。


然后这样的时间复杂度就是一个树链剖分的复杂度 \(O(q\log^2n)\)

代码如下:

#include<bits/stdc++.h>

#define fec(i, x, y) (int i = head[x], y = g[i].to; i; i = g[i].ne, y = g[i].to)
#define dbg(...) fprintf(stderr, __VA_ARGS__)
#define File(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
#define fi first
#define se second
#define pb push_back

template<typename A, typename B> inline char smax(A &a, const B &b) {return a < b ? a = b, 1 : 0;}
template<typename A, typename B> inline char smin(A &a, const B &b) {return b < a ? a = b, 1 : 0;}

typedef long long ll; typedef unsigned long long ull; typedef std::pair<int, int> pii;

template<typename I> inline void read(I &x) {
    int f = 0, c;
    while (!isdigit(c = getchar())) c == '-' ? f = 1 : 0;
    x = c & 15;
    while (isdigit(c = getchar())) x = (x << 1) + (x << 3) + (c & 15);
    f ? x = -x : 0;
}

#define lc o << 1
#define rc o << 1 | 1

const int N = 200000 + 7;
const ll INF = 0x3f3f3f3f3f3f3f3f;

int n, m, dfc;
int a[N];
int dep[N], f[N], siz[N], son[N], dfn[N], pre[N], top[N], bot[N];
ll dp[N], h[N];

struct Edge { int to, ne; } g[N << 1]; int head[N], tot;
inline void addedge(int x, int y) { g[++tot].to = y, g[tot].ne = head[x], head[x] = tot; }
inline void adde(int x, int y) { addedge(x, y), addedge(y, x); }

struct Matrix {
    ll a[2][2];
    inline Matrix() { memset(a, 0, sizeof(a)); }
    inline Matrix operator * (const Matrix &b) {
        Matrix c;
        c.a[0][0] = std::min(a[0][0] + b.a[0][0], a[0][1] + b.a[1][0]);
        c.a[0][1] = std::min(a[0][0] + b.a[0][1], a[0][1] + b.a[1][1]);
        c.a[1][0] = std::min(a[1][0] + b.a[0][0], a[1][1] + b.a[1][0]);
        c.a[1][1] = std::min(a[1][0] + b.a[0][1], a[1][1] + b.a[1][1]);
        return c;
    }
} v[N], t[N << 2];

inline void dfs1(int x, int fa = 0) {
    dep[x] = dep[fa] + 1, f[x] = fa, siz[x] = 1;
    for fec(i, x, y) if (y != fa) dfs1(y, x), siz[x] += siz[y], siz[y] > siz[son[x]] && (son[x] = y);
}
inline void dfs2(int x, int pa) {
    top[x] = pa, dfn[x] = ++dfc, pre[dfc] = x;
    if (!son[x]) return (void)(bot[x] = x);
    dfs2(son[x], pa), bot[x] = bot[son[x]];
    for fec(i, x, y) if (y != f[x] && y != son[x]) dfs2(y, y);
}
inline void dfs3(int x, int fa = 0) {
    for fec(i, x, y) if (y != fa) {
        dfs3(y, x);
        dp[x] += dp[y];
        if (y != son[x]) h[x] += dp[y];
    }
    if (!son[x]) dp[x] = h[x] = INF;
    smin(dp[x], a[x]);
}

inline void build(int o, int L, int R) {
    if (L == R) return t[o].a[0][0] = h[pre[L]], t[o].a[0][1] = a[pre[L]], (void)0;
    int M = (L + R) >> 1;
    build(lc, L, M), build(rc, M + 1, R);
    t[o] = t[lc] * t[rc];
}
inline void qadd(int o, int L, int R, int x) {
    if (L == R) return t[o].a[0][0] = h[pre[L]], t[o].a[0][1] = a[pre[L]], (void)0;
    int M = (L + R) >> 1;
    if (x <= M) qadd(lc, L, M, x);
    else qadd(rc, M + 1, R, x);
    t[o] = t[lc] * t[rc];
}
inline Matrix qsum(int o, int L, int R, int l, int r) {
    if (l <= L && R <= r) return t[o];
    int M = (L + R) >> 1;
    if (r <= M) return qsum(lc, L, M, l, r);
    if (l > M) return qsum(rc, M + 1, R, l, r);
    return qsum(lc, L, M, l, r) * qsum(rc, M + 1, R, l, r);
}

inline ll qry(int x) {
    const Matrix &tmp = qsum(1, 1, n, dfn[x], dfn[bot[x]]);
    assert(!tmp.a[1][0] && !tmp.a[1][1]);
    return std::min(tmp.a[0][0], tmp.a[0][1]);
}
inline void upd(int x, int k) {
    a[x] += k;
    while (top[x] != 1) {
        h[f[top[x]]] -= qry(top[x]);
        qadd(1, 1, n, dfn[x]);
        h[f[top[x]]] += qry(top[x]);
        x = f[top[x]];
    }
    qadd(1, 1, n, dfn[x]);
}

inline void work() {
    dfs1(1), dfs2(1, 1), dfs3(1), build(1, 1, n);
    read(m);
    while (m--) {
        static char opt[5];
        int x, y;
        scanf("%s", opt);
        if (*opt == 'Q') read(x), printf("%lld\n", qry(x));
        else read(x), read(y), upd(x, y);
    }
    
}

inline void init() {
    read(n);
    for (int i = 1; i <= n; ++i) read(a[i]);
    int x, y;
    for (int i = 1; i < n; ++i) read(x), read(y), adde(x, y);
}

int main() {
#ifdef hzhkk
    freopen("hkk.in", "r", stdin);
#endif
    init();
    work();
    fclose(stdin), fclose(stdout);
    return 0;
}

转载于:https://www.cnblogs.com/hankeke/p/ddp.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值