2025年6月8日树型动态规划
原文
https://www.beiweidoge.top/116.html
算法
树型DP
P1:旅游规划
题目描述tourism
W 市的交通规划出现了重大问题,市政府下定决心在全市各大交通路口安排疏导员来疏导密集的车流。但由于人员不足,W 市市长决定只在最需要安排人员的路口安排人员。
具体来说,W 市的交通网络十分简单,由 n 个交叉路口和 n−1 条街道构成,交叉路口路口编号依次为 0,1,⋯,n−1 。任意一条街道连接两个交叉路口,且任意两个交叉路口间都存在一条路径互相连接。
经过长期调查,结果显示,如果一个交叉路口位于 W 市交通网最长路径上,那么这个路口必定拥挤不堪。所谓最长路径,定义为某条路径 p=(v1,v2,v3,⋯,vk),路径经过的路口各不相同,且城市中不存在长度大于 k 的路径,因此最长路径可能不唯一。因此 W 市市长想知道哪些路口位于城市交通网的最长路径上。
输入
第一行一个整数 n;
之后 n−1 行每行两个整数 u,v,表示 u 和 v 的路口间存在着一条街道。
输出
输出包括若干行,每行包括一个整数——某个位于最长路径上的路口编号。为了确保解唯一,请将所有最长路径上的路口编号按编号顺序由小到大依次输出。
样例输入 复制
10
0 1
0 2
0 4
0 6
0 7
1 3
2 5
4 8
6 9
样例输出 复制
0
1
2
3
4
5
6
8
9
提示
数据范围与提示:
对于全部数据,1≤n≤2×10^5 。
解题思路
这道题目要求我们找出所有位于树的最长路径(即直径)上的节点。树的直径是指树中最长的路径,可能有多条。为了高效地找到所有位于直径上的节点,我们可以采用以下步骤:
- 两次DFS/BFS找直径端点:首先通过两次深度优先搜索(DFS)或广度优先搜索(BFS)找到树的两个端点,这两个端点之间的路径即为树的一条直径。
- 标记直径上的节点:找到直径的两个端点后,我们需要标记所有位于这条路径上的节点。可以通过从其中一个端点出发,记录路径上的所有节点。
- 处理多条直径的情况:如果存在多条直径,我们需要确保所有直径上的节点都被标记。这可以通过检查每个节点是否存在于任何一条直径上来实现。
标准程序解析
标准程序使用了动态规划的方法来计算每个节点的最长路径,并通过两次DFS来确定哪些节点位于最长路径上。具体步骤如下:
- 第一次DFS(dfs1):计算每个节点的最长路径(a[x])和次长路径(b[x]),同时更新全局最长路径(ma)。
- 第二次DFS(dfs2):计算每个节点向上(即向父节点方向)的最长路径(c[x]),并确定该节点是否位于最长路径上。
- 输出结果:检查每个节点的最长路径组合是否等于全局最长路径,如果是,则输出该节点。
补充做题思路
- 理解树的直径:树的直径是树中最长的路径,可以通过两次DFS或BFS找到。第一次找到距离任意节点最远的节点,第二次从该节点出发找到最远的节点,这两点之间的路径即为直径。
- 标记直径上的节点:找到直径的两个端点后,可以通过遍历路径来标记所有节点。如果存在多条直径,需要确保所有直径上的节点都被标记。
- 优化处理:标准程序通过动态规划高效地计算每个节点的最长路径,避免了多次遍历,适用于大规模数据。
优化后的代码思路
- 两次DFS找直径端点:
- 第一次DFS从任意节点(如节点0)出发,找到距离最远的节点u。
- 第二次DFS从节点u出发,找到距离最远的节点v。u和v之间的路径即为直径。
- 标记直径上的节点:
- 从节点u出发,记录到节点v的路径上的所有节点。
- 如果存在多条直径,需要检查其他可能的直径路径上的节点。
- 输出结果:将所有标记的节点按升序输出。
标准程序
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 200010;
int n, i, ma, x, y, a[N], b[N], c[N];
vector <int> f[N];
void dfs1(int x, int fx) {int i, sx;for (i = 0; i < f[x].size(); i++) {sx = f[x][i];if (sx == fx) continue;dfs1(sx, x);if (a[x] < a[sx] + 1) b[x] = a[x], a[x] = a[sx] + 1;else if (b[x] < a[sx] + 1) b[x] = a[sx] + 1;}ma = max(ma, a[x] + b[x]);
}
void dfs2(int x, int fx) {int i, sx;for (i = 0; i < f[x].size(); i++) {sx = f[x][i];if (sx == fx) continue;if (a[sx] + 1 == a[x]) c[sx] = max(c[x], b[x]) + 1;else c[sx] = max(c[x], a[x]) + 1;dfs2(sx, x);}
}
int main() {cin >> n;for (i = 1; i < n; i++) cin >> x >> y, f[x].pb(y), f[y].pb(x);dfs1(0, -1);dfs2(0, -1);for (i = 0; i < n; i++)if (a[i] + b[i] + c[i] - min(a[i], min(b[i], c[i])) == ma)cout << i << "\n";
}
P2:电话网络
题目描述
题目描述
Farmer John决定为他的所有奶牛都配备手机,以此鼓励她们互相交流。不过,为此FJ必须在奶牛们居住的N(1 <= N <= 10,000)块草地中选一些建上无线电通讯塔,来保证任意两块草地间都存在手机信号。所有的N块草地按1…N顺次编号。
所有草地中只有N-1对是相邻的,不过对任意两块草地A和B(1 <= A <= N; 1 <= B <= N; A != B),都可以找到一个以A开头以B结尾的草地序列,并且序列中相邻的编号所代表的草地相邻。无线电通讯塔只能建在草地上,一座塔的服务范围为它所在的那块草地,以及与那块草地相邻的所有草地。
请你帮FJ计算一下,为了建立能覆盖到所有草地的通信系统,他最少要建多少座无线电通讯塔。
输入格式:
- 第1行: 1个整数,N
- 第2…N行: 每行为2个用空格隔开的整数A、B,为两块相邻草地的编号
输入样例 (tower.in):
5
1 3
5 2
4 3
3 5
输入说明:
Farmer John的农场中有5块草地:草地1和草地3相邻,草地5和草地2、草地4和草地3,草地3和草地5也是如此。更形象一些,草地间的位置关系大体如下:(或是其他类似的形状)
4 2 | | 1--3--5
输出格式:
- 第1行: 输出1个整数,即FJ最少建立无线电通讯塔的数目
输出样例 (tower.out):
2
输出说明:
FJ可以选择在草地2和草地3,或是草地3和草地5上建通讯塔。
解题思路
这道题目要求我们在树形结构的草地网络中,选择最少数目的通讯塔,使得所有草地都被覆盖。通讯塔的覆盖范围包括其所在的草地及其相邻的草地。这是一个典型的树形动态规划问题,可以通过状态转移来求解。
方法思路
- 问题分析:我们需要在树中选择一些节点(草地)建立通讯塔,使得每个节点要么被选为通讯塔,要么其相邻节点中有通讯塔。这是一个最小支配集问题,但覆盖范围略有不同。
- 动态规划状态定义:
dp[u][0]
:在节点u建立通讯塔时,子树u的最小通讯塔数目。dp[u][1]
:在节点u不建立通讯塔,但其父节点建立通讯塔时,子树u的最小通讯塔数目。dp[u][2]
:在节点u不建立通讯塔,且其父节点也不建立通讯塔时,子树u的最小通讯塔数目(此时必须有一个子节点建立通讯塔)。
- 状态转移:
dp[u][0]
:u建立通讯塔,所以子节点可以选择建立或不建立,取最小值。dp[u][1]
:u不建立通讯塔,但其父节点建立通讯塔,所以子节点不能被父节点覆盖,必须选择子节点建立或不建立,但不能依赖父节点。dp[u][2]
:u不建立通讯塔且父节点也不建立通讯塔,所以至少有一个子节点必须建立通讯塔,其余子节点可以选择建立或不建立。
- 初始化与结果:根节点的初始状态需要特别处理,最终结果为根节点建立通讯塔或不建立但满足覆盖条件的最小值。
代码解释
- 输入处理:读取节点数目和相邻关系,构建树的邻接表表示。
- DFS遍历:从根节点开始进行深度优先搜索,计算每个节点的三种状态的最小通讯塔数目。
dp[u][0]
:选择当前节点u,累加子节点的最小数目。dp[u][1]
:不选择当前节点u,但父节点选择,子节点不能依赖父节点覆盖。dp[u][2]
:不选择当前节点u且父节点也不选择,必须有一个子节点被选择,其余子节点选择最小数目。
- 结果输出:根节点的状态
dp[1][0]
和dp[1][2]
中的最小值即为答案,确保根节点被覆盖。
这种方法确保在树形结构中高效地计算出最少数目的通讯塔,覆盖所有节点。
标准程序
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 10010;
int n, i, x, y, dp[N][3];
vector <int> f[N];
void dfs(int x, int fx) {int i, sx, mi = 2e9;dp[x][0] = 1;for (i = 0; i < f[x].size(); i++) {sx = f[x][i];if (sx == fx) continue;dfs(sx, x);dp[x][0] += min(dp[sx][0], min(dp[sx][1], dp[sx][2]));dp[x][1] += min(dp[sx][0], dp[sx][2]);dp[x][2] += min(dp[sx][0], dp[sx][2]);mi = min(mi, dp[sx][0] - min(dp[sx][0], dp[sx][2]));}dp[x][2] += mi;
}
int main() {cin >> n;for (i = 1; i < n; i++) cin >> x >> y, f[x].pb(y), f[y].pb(x);dfs(1, -1);cout << min(dp[1][0], dp[1][2]);
}
P3:家族保险
题目描述
题目描述
乐乐家是一个庞大的家族,家族成员关系构成了一棵树,最上面的根节点是老祖宗乐乐,树上的边表示存在父子关系。如果某个人购买了保险,他的保险会对自己和自己的几代后代有效。乐乐想让你帮他算算,有多少人有保险呢?
输入格式
- 第一行两个整数 ( N, M )(( 1 \leq N, M \leq 300000 )),表示总人数 ( N ) 和保险共买了 ( M ) 份。
- 第二行 ( N-1 ) 个数 ( P_i ),表示第 ( i ) 个人的父亲是 ( P_i )。
- 接下来 ( M ) 行,每行两个整数 ( X_i ) 和 ( Y_i ),表示第 ( X_i ) 个人买了保险,保险覆盖了他和他的后面 ( Y_i ) 代。
输出格式 - 一行一个整数表示有多少人有保险。
输入1
7 3
1 2 1 3 3 3
1 1
1 2
4 3
输出1
4
输入2
10 10
1 1 3 1 2 3 3 5 7
2 1
5 1
4 3
6 3
2 1
7 3
9 2
1 2
6 2
8 1
输出2
10
样例1解释
- 第一个人买了保险,覆盖了他和他的亲儿子,所以1、2、4三人有保险。
- 第一个人又买了保险,覆盖了他和后面两代,所以3作为孙子也有保险。
- 第四个人买了保险,覆盖了他和后面3代,他没有子孙,所以只有4有了保险。
- 所以1、2、3、4四人有了保险。
解题思路
问题分析
我们需要计算家族树中被保险覆盖的节点数量。每个保险覆盖购买者及其Y_i代后代。关键在于高效处理大量数据(N,M≤300,000),避免超时。
关键观察
- 树结构特性:家族关系构成一棵树,可以递归处理每个节点的覆盖情况
- 保险叠加效应:同一节点可能被多个保险覆盖,只需记录最大覆盖代数
- 高效遍历:使用深度优先搜索(DFS)遍历树,同时传递覆盖信息
算法选择
- 预处理阶段:
- 构建家族树(邻接表表示)
- 记录每个节点的最大覆盖代数(取所有保险中的最大值)
- 遍历阶段:
- 使用DFS遍历树
- 维护当前剩余覆盖代数
- 标记被覆盖的节点
- 统计结果:
- 统计所有被标记的节点
复杂度分析
- 时间复杂度:O(N)(每个节点仅被访问一次)
- 空间复杂度:O(N)(存储树结构和标记数组)
标准程序
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 300010;
int i, n, m, x, y, ans, g[N], fl[N];
vector <int> f[N];
void dfs(int fx, int lt) {int i, sx;lt = max(lt, g[fx]);for (i = 0; i < f[fx].size(); i++) {sx = f[fx][i];if (lt > 0) fl[sx] = 1;dfs(sx, lt - 1);}
}
int main() {cin >> n >> m;for (i = 2; i <= n; i++) cin >> x, f[x].pb(i);for (i = 1; i <= m; i++) cin >> x >> y, g[x] = max(g[x], y), fl[x] = 1;dfs(1, 0);for (i = 1; i <= n; i++) ans += fl[i];cout << ans;
}