翻转树边

翻转树边

给定一个 $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/

posted @ 2022-03-30 22:29  onlyblues  阅读(49)  评论(0编辑  收藏  举报
Web Analytics