翻转树边

翻转树边

给定一个 n 个节点的树。

节点编号为 1n

树中的 n1 条边均为单向边。

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

但是,由于树中的边均为单向边,所以在选定中心点后,可能无法从中心点出发到达其他所有节点。

为此,我们需要翻转一些边的方向,从而使得所选中心点可以到达其他所有节点。

我们希望选定中心点后,所需翻转方向的边的数量尽可能少。

请你确定哪些点可以选定为中心点,并输出所需的最少翻转边数量。

输入格式

第一行包含整数 n

接下来 n1 行,每行包含两个整数 a,b,表示存在一条从 ab 的单向边。

输出格式

第一行输出一个整数,表示所需的最少翻转边数量。

第二行以升序顺序输出所有可选中心点(即所需翻转边数量最少的中心点)的编号。

数据范围

前三个测试点满足 2n5
所有测试点满足 2n2×1051a,bnab

输入样例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(n2),会超时。

  我们的做法是,定义一个数组down[i],表示从结点i往下走,走完以i为根的子树,所需要翻转的边的数目,也就是边权和。对于根节点,先把子节点的down求出来,然后再相加,就得到根节点的down(当然还要加上根节点到子节点这条边的权值)。

  对于任何一个结点u,要求以它为根的树,到所有点的边权和是多少,除上面求得的往下走的down[u]之外,还需要知道从这个结点往上走完所有结点的边权和,设为up[u]

  比如有下图:

  我们用过结点u把整棵树分成两个部分,一个是从u向下走的所有结点,另一部分是从u向上走的所有结点。其中down[u]已经算出来了。

  要算up[u],需要从u走到fa,而fa也有两种情况,一部分是往上走,即up[fa];一部分是往下走,即down[fa]down[fa]包含两个部分,一种是以u为子树的向下走的部分,以及从其他子树向下走的部分。因此在算up[fa]时,应该用down[fa]减去以u为子树向下走的部分。因此up[u]=up[fa]+down[fa]down[u]w+w^,其中在减去down[u]后,还需要减去从fau这条边的权值w,最后还要加上从ufa这条边的权值w^,表明从u往上走到fa

  在算down时是从下往上算,即先算儿子再算根。up是从上往下算,因为在算儿子的up时,需要用到父节点的up。因此要写两个dfs来实现。

  这种实现方式和另外一个题目很像,旅游规划:https://www.cnblogs.com/onlyblues/p/15998496.html,也是两次树形dp,一次从下到上,一次从上到下。

  另外,在这题的代码中,dfs的参数from不是从上一个结点来的编号,而是边的编号。这是因为我们在求up的时候,需要快速知道某条边的反向边是什么。根据我们的加边函数可以发现,01互为一对反向边,23互为一对反向边,以此类推,即如果有某条边的编号为x,那么这条边的反向边的编号为x1

  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总的写法,其实不一定要传入边的编号,也可以是从上一个来的结点的编号。因为边的权重只有01,如果我们知道某一条边的权值,那么反向边的权值就求是这条边的权值取非就可以了。

  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 @   onlyblues  阅读(55)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2021-03-30 是否同一棵二叉搜索树
Web Analytics
点击右上角即可分享
微信分享提示