翻转树边
翻转树边
给定一个 个节点的树。
节点编号为 。
树中的 条边均为单向边。
现在,我们需要选取一个节点作为中心点,并希望从中心点出发可以到达其他所有节点。
但是,由于树中的边均为单向边,所以在选定中心点后,可能无法从中心点出发到达其他所有节点。
为此,我们需要翻转一些边的方向,从而使得所选中心点可以到达其他所有节点。
我们希望选定中心点后,所需翻转方向的边的数量尽可能少。
请你确定哪些点可以选定为中心点,并输出所需的最少翻转边数量。
输入格式
第一行包含整数 。
接下来 行,每行包含两个整数 ,表示存在一条从 到 的单向边。
输出格式
第一行输出一个整数,表示所需的最少翻转边数量。
第二行以升序顺序输出所有可选中心点(即所需翻转边数量最少的中心点)的编号。
数据范围
前三个测试点满足 。
所有测试点满足 ,,。
输入样例1:
3 2 1 2 3
输出样例1:
0 2
输入样例2:
4 1 4 2 4 3 4
输出样例2:
2 1 2 3
解题思路
这是一个树形dp的题。
暴力的做法是,选择一个点进行dfs,遍历每一个子节点,如果这条边是正方向的(由根节点指向子节点),那么这条边的权值为,否则(反方向)边的权值为。dfs的时候对边权求和,dfs一遍后权值和就是要翻转边的数量。但这种做法的话需要对每个结点进行一次dfs,总的时间复杂度就是,会超时。
我们的做法是,定义一个数组,表示从结点往下走,走完以为根的子树,所需要翻转的边的数目,也就是边权和。对于根节点,先把子节点的求出来,然后再相加,就得到根节点的(当然还要加上根节点到子节点这条边的权值)。
对于任何一个结点,要求以它为根的树,到所有点的边权和是多少,除上面求得的往下走的之外,还需要知道从这个结点往上走完所有结点的边权和,设为。
比如有下图:
我们用过结点把整棵树分成两个部分,一个是从向下走的所有结点,另一部分是从向上走的所有结点。其中已经算出来了。
要算,需要从走到,而也有两种情况,一部分是往上走,即;一部分是往下走,即,包含两个部分,一种是以为子树的向下走的部分,以及从其他子树向下走的部分。因此在算时,应该用减去以为子树向下走的部分。因此,其中在减去后,还需要减去从到这条边的权值,最后还要加上从到这条边的权值,表明从往上走到。
在算时是从下往上算,即先算儿子再算根。是从上往下算,因为在算儿子的时,需要用到父节点的。因此要写两个dfs来实现。
这种实现方式和另外一个题目很像,旅游规划:https://www.cnblogs.com/onlyblues/p/15998496.html,也是两次树形dp,一次从下到上,一次从上到下。
另外,在这题的代码中,dfs的参数不是从上一个结点来的编号,而是边的编号。这是因为我们在求的时候,需要快速知道某条边的反向边是什么。根据我们的加边函数可以发现,和互为一对反向边,和互为一对反向边,以此类推,即如果有某条边的编号为,那么这条边的反向边的编号为。
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总的写法,其实不一定要传入边的编号,也可以是从上一个来的结点的编号。因为边的权重只有和,如果我们知道某一条边的权值,那么反向边的权值就求是这条边的权值取非就可以了。
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2021-03-30 是否同一棵二叉搜索树