翻转树边(换根dp)

题意

给定一个\(n\)个节点的树,节点编号为\(1 \sim n\)。树中的\(n−1\)条边均为单向边。

现在,我们需要选取一个节点作为中心点,并希望从中心点出发可以到达其他所有节点。

但是,由于树中的边均为单向边,所以在选定中心点后,可能无法从中心点出发到达其他所有节点。为此,我们需要翻转一些边的方向,从而使得所选中心点可以到达其他所有节点。

我们希望选定中心点后,所需翻转方向的边的数量尽可能少。请你确定哪些点可以选定为中心点,并输出所需的最少翻转边数量。

数据范围

\(2 \leq n \leq 2 \times 10^5\)

思路

本题是单向边,在进行树形dp的时候图是不连通的。基于这一点,我们可以建双向边,将原来的边权值设置为\(0\),表示这个方向不用翻转;将反向边的权值设置为\(1\),表示这个方向需要翻转。

后面的过程是比较明显的换根dp。首先考虑往下走,需要翻转多少条边。我们不妨设当前点为\(u\),子节点是\(j\),往下走需要翻转的边数为\(down_u\)。则,\(down_u = \sum\limits_j (down_j + w_{uj})\)

然后再考虑向上走需要翻转多少条边。我们不妨设当前点为\(u\),父节点为\(f\),往上走需要翻转的边数为\(up_u\)。则,\(up_u = up_f + down_j - (w_{fu} + down_u) + w_{uf}\)

最终答案为:\(up_u + down_u\)

代码

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

using namespace std;

const int N = 200010, M = 2 * N;

int n;
int h[N], e[M], w[M], ne[M], idx;
int down[N], up[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

void dfs1(int u, int fa)
{
    for(int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if(j == fa) continue;
        dfs1(j, u);
        down[u] += down[j] + w[i];
    }
}

void dfs2(int u, int fa)
{
    for(int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if(j == fa) continue;
        up[j] = up[u] + down[u] - (down[j] + w[i]) + w[i ^ 1];
        dfs2(j, u);
    }
}

int main()
{
    scanf("%d", &n);
    memset(h, -1, sizeof h);
    for(int i = 0; i < n - 1; i ++) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b, 0), add(b, a, 1);
    }
    dfs1(1, -1);
    dfs2(1, -1);
    int mi = 1e9;
    for(int i = 1; i <= n; i ++) {
        mi = min(mi, up[i] + down[i]);
    }
    printf("%d\n", mi);
    for(int i = 1; i <= n; i ++) {
        if(up[i] + down[i] == mi) {
            printf("%d ", i);
        }
    }
    printf("\n");
    return 0;
}
posted @ 2022-06-01 16:09  pbc的成长之路  阅读(55)  评论(0)    收藏  举报