动态DP Luogu4719

(虽然左边似乎也有目录但是加上也好)。

Luogu4719

题面:

给定一颗 \(n\) 点的树。要求支持修改,啊,要求你求最大权独立集,如果没有修改点权的话应该是比较好做的吧,就是 \(f[i][0/1]\) 表示这个点选不选,选儿子不能选否则可以选,本质就是 没有上司的舞会 。如果需要套一个修改,事情就稍微麻烦了。

思路/题解:

这里主要介绍全局平衡二叉树的做法,考虑一次修改会影响到哪些点?只有子树内包含这个点的点会被修改,暨,只有一条链会被修改,那么每次全局的修改直接就是血亏。

基于只有链修改这样的事实,我们可以考虑使用树链剖分来进行修改。

具体来说,我们对每一个点维护 \(g[i][0]\) 以及 \(g[i][1]\) ,分别表示 \(i\) 为根的子树里,不选 \(i\)轻儿子 爱选不选的 \(dp\) 值。以及强制选 \(i\)轻儿子 不能选的 \(dp\) 值,啊也就是类似 \(dp\) ,不过我们刨掉 重儿子 罢了。

也就是从原来的

void dfs(int x, int fa) {
    f[x][1] = val[x];
    for(auto it : G[x]) {
        if (it == fa) continue;
        dfs(it, x);
        f[x][1] += f[it][0];
        f[x][0] += max(f[it][0],f[it][1]);
    }
}

变成了删除 重儿子\(dp\)

void dfs(int x, int fa) {
    f[x][1] = val[x];
    for(auto it : G[x]) {
        if (it == wson) continue; //!!!!!!!!!!!! new!
        if (it == fa) continue;
        dfs(it, x);
        f[x][1] += f[it][0];
        f[x][0] += max(f[it][0],f[it][1]);
    }
}

注意到一个事实,叶子结点的 \(g\) 数组其实就是实际的 \(f\) ,但是上面的都是错的因为你没有维护轻儿子里面的重儿子,所以你这不是仅刨除重儿子的 \(dp\) 值,欸欸欸先别打先别打,别着急啊,一点点来。

所以由于不能只靠这玩意 \(dp\) ,毕竟你的轻儿子,他里面的重儿子可不是你的重儿子,还是要的要的~所以考虑同时 \(dp\) 原来的 \(f\) 和如今的 \(g\) (这就是为什么一直不叫 \(f\) 的缘故)。

考虑一波 \(dp\) ,其实 \(f\) 是没变的, \(g\) 也就是变成了用 \(f\) 更新,具体看一下代码吧!

void dfs(int x, int fa) {
    f[x][1] = val[x];
   	g[x][1] = val[x]; 
    // 这两个初始化肯定没问题
    if (wson[x]) { // 如果有重儿子
        dfs(wson[x], x);
        f[x][0] += std::max(f[wson[x]][0], f[wson[x]][1]);
        f[x][1] += f[wson[x]][0];
        // 害,如果不选可以随便挑,否则如果选 x 儿子只能不选
    }// 重儿子单独拿出来更新一下 f
    for (int p = h[x], y; p; p = ne[p]) {
		if ((y = to[p]) != fa && (y != wson[x])) {
			// 找到一个不是爹而且不是重儿子的 
            dfs(y, x);
            dfs(it, x);
        	f[x][0] += std::max(f[y][0], f[y][1]);
            g[x][0] += std::max(f[y][0], f[y][1]);
            f[x][1] += f[y][0];
            g[x][1] += f[y][0];
            // 更新是一模一样的 呃毕竟只差重儿子贡献罢了
        }
    }
}

嘿,重头戏来了,接下来我们的 \(f\) 就没用了 ( 工具人!) ,他只用来更新一波初始的 \(g\) 。然后考虑叶子结点的 \(g\) 其实就是他的 \(f\) ,暨就是他的答案,一个事实是,重链的结尾必然是叶子,也就是说,假设我们对于一条重链,我们可以很快的从叶子推上去得到任何一个点的正确 \(dp\) 值,我们就可以做完这道题了,因为每一个点,我们找链底,必然是一个叶子,也就是我们对于一个点,总能找到一个叶子来推导他,而且在一条重链上,则如果我们可以对一条重链,很快的从下往上推导,那么由于叶子的初始值正确,则我们就可以很快的得到任何一个点的 \(dp\) 值,那么每次查询就是查询 \(1\)\(dp\) 值,这道题就做完了。

于是给我们一条链,怎么快速的重推一遍 \(f\) 呢?

考虑一手 \(g\)\(f\) !也就是对于一条重链,一个结点 \(x\)\(f\) 从儿子 \(y\)\(g\) 得到,具体的我们有 \(f[x][1] = f[y][0] + g[x][1], f[x][0] = \max(f[y][0], f[y][1]) + g[x][0]\) ,这个东西不难推导,因为本质就是加上了重儿子的 \(f\) 贡献,所以 \(g\) + 重儿子 \(f\) 就是实际 \(f\) ,但是你可能会说,啊可是我们没有 \(f\) 了呀,不不不,叶子结点的 \(g\) 不就是其 \(f\) 吗?如果知道其父亲的 \(g\) ,就可以推父亲,然后父亲的父亲,直到整条重链都被我们知道!

但是这样跳上去复杂度最坏是线性的啊。

哼哼,如果我们对矩阵乘法进行新定义,具体来说,定义为对和取 \(\max\) ,也就是 \(c[i][j] = \max_k (a[i][k] + b[k][j])\) 的话,这个东西的结合律可以手推一下,也不用太大的,因为接下来我们只用 \(2 \times 2\) 矩阵的乘法, 证明不算难就是写起来麻烦了一点,然后我们证明结合律之后 \(\ldots\) 哎好了,考虑上面那个方程改写成

\[f[x][1] = \max(f[y][0] + g[x][1], f[y][1] -\infin) \\ f[x][0] = \max(f[y][0] + g[x][0], f[y][1] + g[x][0]) \]

第一个就是套了一个负无穷,第二个就是把后面那个放到 \(\max\) 里面了。这不就是上面矩阵乘法的形式了吗?当然,实现的时候没必要用 \(f\) ,直接叶子节点的 \(g\) 也弄成 \(2 \times 2\) 的矩阵就行。

ct20iT.png

于是因为证明了结合律,所以可以用线段树维护树链剖分,然后就行了,至于更新,更新的点自然是好算的,只需要改一下自己的权值贡献就行了,由于我们的更新是从叶子开始的,所以在一个重链上的不用管,因为本身就没算他们的贡献,只需要在重链改变的时候,跳上去的时候,改变其 \(g\) 即可,也就是如果轻儿子被改变了,那就改变一下,至于改变多少,那就是删除原来的 \(f\) 加入现如今的 \(f\) ,你查询一下修改前的 \(f\) ,然后查询一下修改后的 \(f\) ,然后更新一下就行了。

另外,为了节省空间,我们可以只算 \(f\) ,然后通过新的一次 \(dfs\) 来减去重儿子的贡献即可。

但是注意到,我们操作的实质是个啥,本质就是跑一个 \(g\) 数组,然后从一个点开始,一直跳链,跳到新链就查询整条链的结果,然后来更新,查询本质也就查整条链结果。

每次我们都仅仅是查一整条链的结果,你却要用一个线段树维护任意位置可查询?是不是忒浪费了啊?

更一般的,考虑对于每一个链维护从链首到链尾的一个 \(Mat_g\) 矩阵就行了,这个矩阵里存着 \(g\) ,然而你发现一旦修改,你根本不知道怎么改!因为你只知道结果矩阵的话,你是没法删除这一个点的贡献的,至于你说对每一个链维护一个 \(pre\) 和一个 \(nxt\) ,这样每次修改就可以取两边和新的自己乘就行了。然后你咋修改 \(pre\)\(nxt\) ?暴力枚举当场爆炸?

看来不太行,但虽然这是一个失败的尝试,但是确实给了我们一定思路,如果 \(\ldots\) 是因为重链的 "链" 导致了修改复杂度很高,如果我们把它变成树呢?考虑将一个重链提起来,构造 \(BST\) ,也就是平衡树,用类似 \(LCT\) 的思想,把一个重链提起来,然后保证一个点,左子树都是深度比自己浅的点,右边都是深点,同时,我们从前往后找,找到轻子树总和大于一半的时候,我们把这个点作为中点递归处理两边,这么做的好处就是跳一条 \(LCT\) 实边也变成近乎子树大小翻倍,而虚边肯定是翻倍,因为你跨过重链了,如果不翻倍,那应该你是重儿子了。由此,至多向上跳 \(\log\) 次,于是类似点分树的暴力跳就完事了,修改的话,首先要清楚一个点维护啥,一个点维护的是左子树 乘 自己 乘 右子树,注意矩阵乘法顺序不能变,于是跳的过程中改变 \(pushup\) 上去就行了,如果跳过了一条重链,那就修改一下上面那个点,只选轻儿子的 \(dp\) 函数值。

\({\scr Code:}\)

可以离线且数据 \(1e5\) 版本:

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>

const int maxn = 1e5;
const int dinf = 0xcfcfcfcf;

struct Matrix {
    Matrix() { std::memset(a, 0xcf, sizeof a); }
    int a[2][2];
    int* operator[](const int x) { return a[x]; }
    const int* operator[](const int x) const { return a[x]; }

    inline int getans() { /* 对于某一个矩阵 ... 希望得到他的最大值 暨两个 dp 中的最优解*/ return std::max(a[0][0], a[1][0]); }

    inline void init(int x, int y) { a[0][0] = a[0][1] = x, a[1][0] = y, a[1][1] = dinf; } // x means not choose's ans, y means choose.

    friend Matrix operator*(const Matrix &a, const Matrix &b) {
        static Matrix c;
        c.init(dinf, dinf); //! 就是变成初始化那个样子啦 then we can get max ... 
        for (register int k = 0; k < 2; ++k) for (register int i = 0; i < 2; ++i) for (register int j = 0; j < 2; ++j) c[i][j] = std::max(c[i][j], a[i][k] + b[k][j]);
        return c;
    }
};

int v[maxn + 10];
int n, m;

int h[maxn + 10], tot = 1;
int to[maxn * 2 + 10], ne[maxn * 2 + 10];
inline void add(int x, int y) { ne[++tot] = h[x], to[h[x] = tot] = y; }

inline void input() {
    scanf("%d%d", &n, &m);
    for (register int i = 1; i <= n; ++i) scanf("%d", &v[i]);

    for (register int i = 1, u, v; i < n; ++i) {
        scanf("%d%d", &u, &v);
        // printf("u = %d, v = %d\n", u, v);
        add(u, v), add(v, u);
    }
}

int siz[maxn + 10], wson[maxn + 10];
int f[maxn + 10][2];
void dp(int x, int fax) {
    siz[x] = 1;
    f[x][1] = v[x], f[x][0] = 0;
    for (int p = h[x], y; p; p = ne[p]) {
        if (p != fax) {
            dp(y = to[p], p ^ 1);
            siz[x] += siz[y];
            if (siz[y] > siz[wson[x]]) wson[x] = y;
            f[x][0] += std::max(f[y][0], f[y][1]);
            f[x][1] += f[y][0];
        }
    }
    // printf("x = %d, wson = %d\n", x, wson[x]);
    // printf("x = %d, 0 = %d, 1 = %d\n", x, f[x][0], f[x][1]);
}

void deldp(int x, int fax) {
    if (wson[x]) {
        f[x][0] -= std::max(f[wson[x]][0], f[wson[x]][1]);
        f[x][1] -= f[wson[x]][0];
        deldp(wson[x], x);
        for (int p = h[x], y; p; p = ne[p]) {
            if ((y = to[p]) != fax && (y != wson[x])) {
                deldp(y, x);
            }
        }
    }
    // printf("x = %d, 0 = %d, 1 = %d\n", x, f[x][0], f[x][1]);
}

Matrix ddp[maxn + 10], sum[maxn + 10];
int rt;
int stk[maxn + 10], top, fa[maxn + 10];
bool used[maxn + 10];

int ls[maxn + 10], rs[maxn + 10];

inline void pushup(int x) {
    sum[x] = ddp[x];
    if (ls[x]) sum[x] = sum[ls[x]] * sum[x];
    if (rs[x]) sum[x] = sum[x] * sum[rs[x]];
}
int getchain(int l, int r) {
    if (l > r) return 0;
    int sum = 0;
    for (int i = l; i <= r; ++i) {
        int x = stk[i];
        sum += siz[x] - siz[wson[x]]; /* 轻子树 size */
    }
    int nowsum = 0;
    for (int i = l; i <= r; ++i) {
        int x = stk[i];
        nowsum += siz[x] - siz[wson[x]];
        if (nowsum * 2 >= sum) {
            ls[x] = getchain(l, i - 1), rs[x] = getchain(i + 1, r);
            fa[ls[x]] = x, fa[rs[x]] = x;
            pushup(x);
            return x;
        }
    }
    return r;
}
int build(int x) {
    for (int now = x, f = 0; now; f = now, now = wson[now]) {
        used[now] = true;
        for (int p = h[now], y; p; p = ne[p]) {
            y = to[p];
            if (used[y]) continue;
            if (y != f && y != wson[now]) {
                fa[build(y)] = now;
            }
        }
    }
    top = 0;
    for (int now = x; now; now = wson[now]) { stk[++top] = now; }
    return getchain(1, top);
}

inline void prework() {
    dp(1, 0);
    deldp(1, 0);
    for (int i = 1; i <= n; ++i) ddp[i].init(f[i][0], f[i][1]);
    rt = build(1);

    // for (int i = 1; i <= n; ++i) {
    //     printf("fa[%d] = %d\n", i, fa[i]);
    // }
}

#define isroot(x) ((ls[fa[x]] != x) && (rs[fa[x]] != x))
inline void upd(int x, int y) {
    f[x][1] += y - v[x]; 
    v[x] = y;

    int pref = 0, preg = 0, nxtf = 0, nxtg = 0;
    for (int now = x; now; now = fa[now]) {
        if (isroot(now)) pref = sum[now][0][0], preg = sum[now][1][0];
        ddp[now].init(f[now][0], f[now][1]), pushup(now);
        if (isroot(now)) nxtf = sum[now][0][0], nxtg = sum[now][1][0];
        if (isroot(now) && fa[now]) {
            f[fa[now]][0] -= std::max(pref, preg);
            f[fa[now]][0] += std::max(nxtf, nxtg);
            f[fa[now]][1] -= pref;
            f[fa[now]][1] += nxtf;
        }
    }
    printf("%d\n", sum[rt].getans());
}

inline void work() {
    for (int i = 1, x, y; i <= m; ++i) {
        scanf("%d%d", &x, &y);
        upd(x, y);
    }
}

signed main() {
    #pragma region judge
	#ifndef ONLINE_JUDGE
	#ifdef LOCAL
	freopen("e:/.IDE/vspz/.data/test_data.in", "r", stdin);
	freopen("e:/.IDE/vspz/.data/test_data.out", "w", stdout);
	#else
	#ifndef GDB
	freopen("base.in", "r", stdin);
	freopen("base.out", "w", stdout);
	#else
	freopen("e:/.IDE/vspz/.data/test_data.in", "r", stdin);
	#endif
	#endif
	#endif
	#pragma endregion
    
    input();
    prework();
    work();
}

强制在线且数据 \(1e6\) 版本:(其实就是改了一下数组大小以及加了一个异或)

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>

const int maxn = 1e6;
const int dinf = 0xcfcfcfcf;

struct Matrix {
    Matrix() { std::memset(a, 0xcf, sizeof a); }
    int a[2][2];
    int* operator[](const int x) { return a[x]; }
    const int* operator[](const int x) const { return a[x]; }

    inline int getans() {return std::max(a[0][0], a[1][0]); }

    inline void init(int x, int y) { a[0][0] = a[0][1] = x, a[1][0] = y, a[1][1] = dinf; }

    friend Matrix operator*(const Matrix &a, const Matrix &b) {
        static Matrix c;
        c.init(dinf, dinf);
        for (register int k = 0; k < 2; ++k) for (register int i = 0; i < 2; ++i) for (register int j = 0; j < 2; ++j) c[i][j] = std::max(c[i][j], a[i][k] + b[k][j]);
        return c;
    }
};

int v[maxn + 10];
int n, m;

int h[maxn + 10], tot = 1;
int to[maxn * 2 + 10], ne[maxn * 2 + 10];
inline void add(int x, int y) { ne[++tot] = h[x], to[h[x] = tot] = y; }

inline void input() {
    scanf("%d%d", &n, &m);
    for (register int i = 1; i <= n; ++i) scanf("%d", &v[i]);

    for (register int i = 1, u, v; i < n; ++i) {
        scanf("%d%d", &u, &v);
        add(u, v), add(v, u);
    }
}

int siz[maxn + 10], wson[maxn + 10];
int f[maxn + 10][2];
void dp(int x, int fax) {
    siz[x] = 1;
    f[x][1] = v[x], f[x][0] = 0;
    for (int p = h[x], y; p; p = ne[p]) {
        if (p != fax) {
            dp(y = to[p], p ^ 1);
            siz[x] += siz[y];
            if (siz[y] > siz[wson[x]]) wson[x] = y;
            f[x][0] += std::max(f[y][0], f[y][1]);
            f[x][1] += f[y][0];
        }
    }
}

void deldp(int x, int fax) {
    if (wson[x]) {
        f[x][0] -= std::max(f[wson[x]][0], f[wson[x]][1]);
        f[x][1] -= f[wson[x]][0];
        deldp(wson[x], x);
        for (int p = h[x], y; p; p = ne[p]) {
            if ((y = to[p]) != fax && (y != wson[x])) {
                deldp(y, x);
            }
        }
    }
}

Matrix ddp[maxn + 10], sum[maxn + 10];
int rt;
int stk[maxn + 10], top, fa[maxn + 10];
bool used[maxn + 10];

int ls[maxn + 10], rs[maxn + 10];

inline void pushup(int x) {
    sum[x] = ddp[x];
    if (ls[x]) sum[x] = sum[ls[x]] * sum[x];
    if (rs[x]) sum[x] = sum[x] * sum[rs[x]];
}
int getchain(int l, int r) {
    if (l > r) return 0;
    int sum = 0;
    for (int i = l; i <= r; ++i) {
        int x = stk[i];
        sum += siz[x] - siz[wson[x]]; /* 轻子树 size */
    }
    int nowsum = 0;
    for (int i = l; i <= r; ++i) {
        int x = stk[i];
        nowsum += siz[x] - siz[wson[x]];
        if (nowsum * 2 >= sum) {
            ls[x] = getchain(l, i - 1), rs[x] = getchain(i + 1, r);
            fa[ls[x]] = x, fa[rs[x]] = x;
            pushup(x);
            return x;
        }
    }
    return r;
}
int build(int x) {
    for (int now = x, f = 0; now; f = now, now = wson[now]) {
        used[now] = true;
        for (int p = h[now], y; p; p = ne[p]) {
            y = to[p];
            if (used[y]) continue;
            if (y != f && y != wson[now]) {
                fa[build(y)] = now;
            }
        }
    }
    top = 0;
    for (int now = x; now; now = wson[now]) { stk[++top] = now; }
    return getchain(1, top);
}

inline void prework() {
    dp(1, 0);
    deldp(1, 0);
    for (int i = 1; i <= n; ++i) ddp[i].init(f[i][0], f[i][1]);
    rt = build(1);
}

int lst;
#define isroot(x) ((ls[fa[x]] != x) && (rs[fa[x]] != x))
inline void upd(int x, int y) {
    f[x][1] += y - v[x]; 
    v[x] = y;

    int pref = 0, preg = 0, nxtf = 0, nxtg = 0;
    for (int now = x; now; now = fa[now]) {
        if (isroot(now)) pref = sum[now][0][0], preg = sum[now][1][0];
        ddp[now].init(f[now][0], f[now][1]), pushup(now);
        if (isroot(now)) nxtf = sum[now][0][0], nxtg = sum[now][1][0];
        if (isroot(now) && fa[now]) {
            f[fa[now]][0] -= std::max(pref, preg);
            f[fa[now]][0] += std::max(nxtf, nxtg);
            f[fa[now]][1] -= pref;
            f[fa[now]][1] += nxtf;
        }
    }
    printf("%d\n", lst = sum[rt].getans());
}

inline void work() {
    for (int i = 1, x, y; i <= m; ++i) {
        scanf("%d%d", &x, &y);
        x = lst ^ x;
        upd(x, y);
    }
}

signed main() {
    input();
    prework();
    work();
}

结语:

这题依旧是犯了个病,想当然的以为 \(init\) 的时候 \((1, 1)\) 不需要重新初始化为负无穷,导致重定义乘法的时候 \(c\)\(\min\) 出现了问题,导致了出现问题,把 \(c[1][1]\) 在乘法里单独赋值也可通过。

习题(bushi):

P5024保卫王国 ,较多种做法,珂以使用 \(ddp\) 解决。

posted @ 2021-04-09 16:12  Z_char  阅读(141)  评论(2)    收藏  举报