【动态规划】换根DP

\(f[u]\) 为以1为根时,u节点的子树中的答案。
\(g[u]\) 为以1为根时,u节点向父亲p方向看,包括父亲p但不包括u节点的子树中的答案。

换根dp的时候一定要画一张图,简单说就是画一个众字形的图,v节点在最左下角,其父亲是u节点,父亲的父亲是“众”字最顶端的节点,那么最上面的人和右边的人就是g[u],除去v节点以外的所有部分就是g[v],所以要处理的就是u节点的贡献以及v的兄弟节点的贡献。下面的实例代码中,sum没有加上u节点本身的贡献,所以直接减去contribution(v)就可以得到v的所有兄弟节点的贡献。记得要g[v]算上u节点本身的贡献。那么f[v]表示v往下看,g[v]表示除了v往下看的所有部分(包括u和v的所有兄弟),一般来说他们是加起来的。

则答案一般为 \(f[u]+g[u]\) 除非计算代价的时候有特殊的函数贡献,如下面的例题

当维护g的运算是一个群时,可以直接用逆运算去掉当前节点u的贡献。

void dfs1(int u, int p) {
    f[u] = 1;
    for(int &v : G[u]) {
        if(v == p)
            continue;
        dfs1(v, u);
        f[u] += f[v];
    }
}

void dfs2(int u, int p) {
    if(p == 0)
        g[u] = 0;
    int sum = 0;
    for(int &v : G[u]) {
        if(v == p)
            continue;
        sum += contribution(v);
    }
    for(int &v : G[u]) {
        if(v == p)
            continue;
        g[v] = g[u] + sum - contribution(v);
        dfs2(v, u);
    }
}

当维护的运算是不存在逆元的半群时,要用奇奇怪怪的方法计算。例如假如是要求最大值,那么就维护第1大和第2大。

struct MAX2 {

    int max1, max2;

    void Init() {
        max1 = -INF, max2 = -INF;
    }

    // Insert value v
    void Insert(int v) {
        if(v > max2) {
            max2 = v;
            if(max2 > max1)
                swap(max1, max2);
        }
    }

    // Remove value v and query max
    int RemoveMax(int v) {
        if(v == max1)
            return max2;
        return max1;
    }

};

https://codeforces.com/contest/708/problem/C

题意:给一棵树,对于每个点,问是否可以在(你自己想要怎么操作就怎么操作)一次操作之后使得删去这个点之后剩下的连通块的大小都不超过n/2。操作为删除一条边然后加上一条边,并保证操作之后仍然是一棵树。

题解:设siz[u]为以1为根时,子树的siz。设 X[u] 为 以 u 为根时,最大的子树的siz,设Y1[u] 为以 1为根时,u节点内的不超过n/2的最大的子树的siz,设 Y2[u] 为从以 1 为根换到以u为根时,从u节点方向往下看,看到p节点内的不超过n/2的最大的子树的siz。答案为X[u]<=n/2,或者以u为根时,那棵唯一的最大的子树减去其不超过n/2的最大的子树(并把这个子树直接连接到u上面)之后的siz不超过n/2。

麻烦的是Y2[u],Y2[u]=n-siz[u]\leq n/2 ? n-siz[u] : max(Y2[p],\max_{v\in ch[p],v\neq u}Y1[v]) ,后面这个遍历p节点的子树的所有Y1[v],然后除去Y1[u]之后的最大值,需要用上面的MAX2数据结构来维护。


https://codeforces.com/contest/1882/problem/D

这个是换根dp里面比较可怕的类型,首先是contribution里引入了siz的因素,第二就是答案并不是f[u]+g[u]了,还跟黑白有关。

简单说:f[u][0]表示把子树u染成0色的代价,f[u][1]表示把子树u染成1色的代价,小心点维护可以做到。

然后开始换根,换根dp的时候一定要画一张图!


int n;
int a[200005];
int bi[200005];
vector<int> G[200005];

ll ans[200005];
int siz[200005];
ll f[200005][2];      // 以1为根时,把以u为根节点的子树全部染成颜色c的代价
ll g[200005][2];      // 以1为根时,除去以u为根节点的剩余部分全部染成颜色c的代价

ll contribution (int v, int c) {
    // 把v子树染成颜色c的代价
    return min (f[v][c], f[v][1 - c] + siz[v]);
}


void dfs (int u, int p) {
    // 把u为根的子树全部染色为0或者染色为1的最小花费
    ll sum[2] = {};
    siz[u] = 1;
    for (int v : G[u]) {
        if (v == p)
            continue;
        dfs (v, u);
        siz[u] += siz[v];
        sum[0] += contribution (v, 0);  // 把v直接染成0
        sum[1] += contribution (v, 1);  // 把v直接染成1
    }
    // 现在的sum[0]和sum[1]不包括根节点的颜色,表示把除了u以外的所有点都染成c色的代价
    if (bi[u] == 0) {
        f[u][0] = min (sum[0] + 0, sum[1] + siz[u] - 1);
        f[u][1] = min (sum[1] + siz[u] - 1 + siz[u], sum[0] + siz[u]);
    } else {
        // 这种题都是完全对称的
        f[u][1] = min (sum[1] + 0, sum[0] + siz[u] - 1);
        f[u][0] = min (sum[0] + siz[u] - 1 + siz[u], sum[1] + siz[u]);
    }
}

void dfs2 (int u, int p) {
    if (p == 0) {
        g[u][0] = 0;
        g[u][1] = 0;
    }
    ll sum[2] = {};
    for (int &v : G[u]) {
        if (v == p)
            continue;
        sum[0] += contribution (v, 0);
        sum[1] += contribution (v, 1);
    }
    for (int &v : G[u]) {
        if (v == p)
            continue;
        if (bi[u] == 0) {
            // 画个图
            g[v][0] = min (g[u][0] + (sum[0] - contribution (v, 0)),
                           g[u][1] + (sum[1] - contribution (v, 1)) + (n - siz[v] - 1));
            g[v][1] = min (g[u][0] + (sum[0] - contribution (v, 0)) + (n - siz[v]),
                           g[u][1] + (sum[1] - contribution (v, 1)) + (n - siz[v] - 1) + (n - siz[v]));
        } else {
            // 这种题都是完全对称的
            g[v][1] = min (g[u][0] + (sum[0] - contribution (v, 0) + (n - siz[v] - 1)),
                           g[u][1] + (sum[1] - contribution (v, 1)));
            g[v][0] = min (g[u][0] + (sum[0] - contribution (v, 0)) + (n - siz[v] - 1) + (n - siz[v]),
                           g[u][1] + (sum[1] - contribution (v, 1)) + (n - siz[v]));
        }
        dfs2 (v, u);
    }
}


void solve() {
    RD (n);
    RDN (a, n);
    for (int i = 1; i <= n; ++i) {
        G[i].clear ();
    }
    for (int i = 1; i <= n - 1; ++i) {
        int u, v;
        RD (u, v);
        G[u].push_back (v);
        G[v].push_back (u);
    }
    for (int i = 1; i <= n; ++i) {
        ans[i] = 0;
    }
    for (int b = 0; b < 20; ++b) {
        for (int i = 1; i <= n; ++i) {
            bi[i] = (a[i] >> b) & 1;
            f[i][0] = 0;
            f[i][1] = 0;
            g[i][0] = 0;
            g[i][1] = 0;
        }
        dfs (1, 0);
        dfs2 (1, 0);
        for (int i = 1; i <= n; ++i) {
            if (bi[i] == 0) {
                ll sum0 = f[i][0] + g[i][0];
                ll sum1 = f[i][1] + g[i][0];    // 由于i节点本身是0色,所以f[i][1]的时候就会把g[i][0]染成1色
                ll ansb = min (sum0, sum1) * 1LL * (1LL << b);
                ans[i] += ansb;
            } else {
                ll sum0 = f[i][0] + g[i][1];
                ll sum1 = f[i][1] + g[i][1];    // 同上,对称
                ll ansb = min (sum0, sum1) * 1LL * (1LL << b);
                ans[i] += ansb;
            }
        }
    }
    WTN (ans, n);
}
posted @ 2021-01-27 10:26  purinliang  阅读(103)  评论(0编辑  收藏  举报