翻转树边
翻转树边
给定一个 $n$ 个节点的树。
节点编号为 $1 \sim n$。
树中的 $n−1$ 条边均为单向边。
现在,我们需要选取一个节点作为中心点,并希望从中心点出发可以到达其他所有节点。
但是,由于树中的边均为单向边,所以在选定中心点后,可能无法从中心点出发到达其他所有节点。
为此,我们需要翻转一些边的方向,从而使得所选中心点可以到达其他所有节点。
我们希望选定中心点后,所需翻转方向的边的数量尽可能少。
请你确定哪些点可以选定为中心点,并输出所需的最少翻转边数量。
输入格式
第一行包含整数 $n$。
接下来 $n−1$ 行,每行包含两个整数 $a,b$,表示存在一条从 $a$ 到 $b$ 的单向边。
输出格式
第一行输出一个整数,表示所需的最少翻转边数量。
第二行以升序顺序输出所有可选中心点(即所需翻转边数量最少的中心点)的编号。
数据范围
前三个测试点满足 $2 \leq n \leq 5$。
所有测试点满足 $2 \leq n \leq 2 \times {10}^{5}$,$1 \leq a,b \leq n$,$a \ne b$。
输入样例1:
3 2 1 2 3
输出样例1:
0 2
输入样例2:
4 1 4 2 4 3 4
输出样例2:
2 1 2 3
解题思路
这是一个树形dp的题。
暴力的做法是,选择一个点进行dfs,遍历每一个子节点,如果这条边是正方向的(由根节点指向子节点),那么这条边的权值为$0$,否则(反方向)边的权值为$1$。dfs的时候对边权求和,dfs一遍后权值和就是要翻转边的数量。但这种做法的话需要对每个结点进行一次dfs,总的时间复杂度就是$O \left( n^2 \right)$,会超时。
我们的做法是,定义一个数组$down \left[ i \right]$,表示从结点$i$往下走,走完以$i$为根的子树,所需要翻转的边的数目,也就是边权和。对于根节点,先把子节点的$down$求出来,然后再相加,就得到根节点的$down$(当然还要加上根节点到子节点这条边的权值)。
对于任何一个结点$u$,要求以它为根的树,到所有点的边权和是多少,除上面求得的往下走的$down \left[ u \right]$之外,还需要知道从这个结点往上走完所有结点的边权和,设为$up \left[ u \right]$。
比如有下图:
我们用过结点$u$把整棵树分成两个部分,一个是从$u$向下走的所有结点,另一部分是从$u$向上走的所有结点。其中$down \left[ u \right]$已经算出来了。
要算$up \left[ u \right]$,需要从$u$走到$fa$,而$fa$也有两种情况,一部分是往上走,即$up \left[ fa \right]$;一部分是往下走,即$down \left[ fa \right]$,$down \left[ fa \right]$包含两个部分,一种是以$u$为子树的向下走的部分,以及从其他子树向下走的部分。因此在算$up \left[ fa \right]$时,应该用$down \left[ fa \right]$减去以$u$为子树向下走的部分。因此$up \left[ u \right ] = up \left[ fa \right ] + down \left[ fa \right] - down \left[ u \right ] - w + \hat{w}$,其中在减去$down \left[ u \right ]$后,还需要减去从$fa$到$u$这条边的权值$w$,最后还要加上从$u$到$fa$这条边的权值$\hat{w}$,表明从$u$往上走到$fa$。
在算$down$时是从下往上算,即先算儿子再算根。$up$是从上往下算,因为在算儿子的$up$时,需要用到父节点的$up$。因此要写两个dfs来实现。
这种实现方式和另外一个题目很像,旅游规划:https://www.cnblogs.com/onlyblues/p/15998496.html,也是两次树形dp,一次从下到上,一次从上到下。
另外,在这题的代码中,dfs的参数$from$不是从上一个结点来的编号,而是边的编号。这是因为我们在求$up$的时候,需要快速知道某条边的反向边是什么。根据我们的加边函数可以发现,$0$和$1$互为一对反向边,$2$和$3$互为一对反向边,以此类推,即如果有某条边的编号为$x$,那么这条边的反向边的编号为$x \oplus 1$。
AC代码如下:
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 using namespace std; 5 6 const int N = 2e5 + 10, M = N << 1; 7 8 int head[N], e[M], wts[M], ne[M], idx; 9 int down[N], up[N]; 10 11 void add(int v, int w, int wt) { 12 e[idx] = w, wts[idx] = wt, ne[idx] = head[v], head[v] = idx++; 13 } 14 15 // from在这题不代表src是从哪个结点编号来的,而是从哪一条边的编号来的,即from是边的编号 16 void dfs_down(int src, int from) { 17 for (int i = head[src]; i != -1; i = ne[i]) { 18 if (i != (from ^ 1)) { // from表示根节点是从from这条边来的,那么form这条反向边就应该是from异或1 19 dfs_down(e[i], i); 20 down[src] += down[e[i]] + wts[i]; 21 } 22 } 23 } 24 25 void dfs_up(int src, int from) { 26 for (int i = head[src]; i != -1; i = ne[i]) { 27 if (i != (from ^ 1)) { 28 up[e[i]] = up[src] + down[src] - down[e[i]] - wts[i] + wts[i ^ 1]; 29 dfs_up(e[i], i); // 求出子节点的up值后,继续求出以子节点为子树的所有结点的up 30 } 31 } 32 } 33 34 int main() { 35 int n; 36 scanf("%d", &n); 37 38 memset(head, -1, sizeof(head)); 39 for (int i = 0; i < n - 1; i++) { 40 int v, w; 41 scanf("%d %d", &v, &w); 42 add(v, w, 0), add(w, v, 1); // 原本是从v到w的单向边,v到w的权值为0,w到v的权值为1 43 } 44 45 dfs_down(1, -1); 46 dfs_up(1, -1); 47 48 int ret = N; 49 for (int i = 1; i <= n; i++) { 50 ret = min(ret, down[i] + up[i]); 51 } 52 printf("%d\n", ret); 53 for (int i = 1; i <= n ;i++) { 54 if (down[i] + up[i] == ret) printf("%d ", i); 55 } 56 57 return 0; 58 }
当然,上面是y总的写法,其实不一定要传入边的编号,也可以是从上一个来的结点的编号。因为边的权重只有$0$和$1$,如果我们知道某一条边的权值,那么反向边的权值就求是这条边的权值取非就可以了。
AC代码如下:
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 using namespace std; 5 6 const int N = 2e5 + 10, M = N << 1; 7 8 int head[N], e[M], wts[M], ne[M], idx; 9 int down[N], up[N]; 10 11 void add(int v, int w, int wt) { 12 e[idx] = w, wts[idx] = wt, ne[idx] = head[v], head[v] = idx++; 13 } 14 15 void dfs_down(int src, int pre) { 16 for (int i = head[src]; i != -1; i = ne[i]) { 17 if (e[i] != pre) { 18 dfs_down(e[i], src); 19 down[src] += down[e[i]] + wts[i]; 20 } 21 } 22 } 23 24 void dfs_up(int src, int pre) { 25 for (int i = head[src]; i != -1; i = ne[i]) { 26 if (e[i] != pre) { 27 up[e[i]] = up[src] + down[src] - down[e[i]] - wts[i] + !wts[i]; 28 dfs_up(e[i], src); 29 } 30 } 31 } 32 33 int main() { 34 int n; 35 scanf("%d", &n); 36 37 memset(head, -1, sizeof(head)); 38 for (int i = 0; i < n - 1; i++) { 39 int v, w; 40 scanf("%d %d", &v, &w); 41 add(v, w, 0), add(w, v, 1); 42 } 43 44 dfs_down(1, -1); 45 dfs_up(1, -1); 46 47 int ret = N; 48 for (int i = 1; i <= n; i++) { 49 ret = min(ret, down[i] + up[i]); 50 } 51 printf("%d\n", ret); 52 for (int i = 1; i <= n ;i++) { 53 if (down[i] + up[i] == ret) printf("%d ", i); 54 } 55 56 return 0; 57 }
参考资料
AcWing 4381. 翻转树边(AcWing杯 - 全国联赛):https://www.acwing.com/video/3756/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/16079151.html