树形dp
- 一般的状态定义方式:
f[u][j]
:所有只在以u
为根的子树中选,且总体积不超过j
的选法的集合
题目1:树的最长路径
最长路径也就相当于树的最大直径
给定一棵树,树中包含 n个结点(编号1~n)和 n−1条无向边,每条边都有一个权值。
现在请你找到树中的一条最长路径。
换句话说,要找到一条路径,使得使得路径两端的点的距离最远。
注意:路径中可以只包含一个点。
输入格式
第一行包含整数 n。
接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai和 bi之间存在一条权值为 ci的边。
输出格式
输出一个整数,表示树的最长路径的长度。
数据范围
1 ≤ n ≤ 1e4
,
1 ≤ ai, bi ≤ n
,
−1e5 ≤ ci ≤ 1e5
分析:
- dp本质上就是将所求问题划分为若干类,分别求出每一类中的最值,然后再求出最终的最值
- 用每个节点表示所挂到这个节点上的路径的集合
先求出每个子树中往下走的路径的最大长度。那么,所有挂在此子树根节点上的路径可以分为两大类:
- 从根节点一直往下走的最大路径长度
- 穿过根节点的最大路径长度(从下走到上再走到下)
求出这两类中的最大值,就是挂在此根节点上的路径的最大长度。
代码:
#include <iostream>
#include <cstring>
using namespace std;
// 无向图 M = 2 * N
const int N = 1e5 + 10, M = 2 * N;
int n;
int h[N], e[M], w[N], ne[M], idx;
int res;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
// 在dfs时传入两个参数,u表示当前遍历的节点,father表示当前遍历节点的父节点
// 确保在遍历u所指向的边时,不会出现往上走的情况
int dfs(int u, int father)
{
int dist = 0; // 计算以u为根节点时,自上而下路径权值和的最大值
int d1 = 0, d2 = 0; // 最长距离和次长距离初始化为0
// 因为当d1或者d2为负数时,通过这条路径一定不会出现最大权值和
// 因此为负数时就当这条路径不存在(路径不存在时权值为0),不进行计算
for (int i = h[u]; ~i; i = ne[i]) // ~i 等价于 i >= 0
{
int j = e[i];
// 确保在遍历u所指向的边时,不会出现往上走的情况
if (j == father) continue;
int d = dfs(j, u) + w[i];
dist = max(dist, d);
if (d >= d1) d2 = d1, d1 = d;
else if (d >= d2) d2 = d;
}
res = max(res, d1 + d2);
return dist;
}
int main()
{
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 1; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
// 任取1节点为根节点,由于根节点没有父节点,所以father = -1
dfs(1, -1);
printf("%d\n", res);
return 0;
}
题目二:数字转换
如果一个数 x的约数之和 y(不包括他本身)比他本身小,那么 x可以变成 y,y也可以变成 x。
例如,4可以变为 3,1可以变为 7。
限定所有数字变换在不超过 n的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。
输入格式
输入一个正整数 n。
输出格式
输出不断进行数字变换且不出现重复数字的最多变换步数。
数据范围
1 ≤ n ≤ 5e4
分析:
本题数字之间的转换实际上和树的最长路径类似,数字的最多变换次数即为权值为1的无向树的最长路径
每个数(节点)的约数之和只有一个且唯一,相当于每个节点只有一个父节点,可以构成树的形式
由于数和约数之间可以相互转换,就构成了无向树,而求不经过重复点的最多转换次数就变成了求无向树的最大直径问题。由于树结构需要我们自己创建,那么我们就可以为了创建为有向树节省空间。
如何把树构建出来:
求出每个数字的约数之和之后,创建从父节点(约数和)指向子节点(数字)的有向边,然后找出根节点进行计算最大路径长度,由于这些数之间通过有向边创建的树不一定有公共的根节点,所以这道题的数据结构实际上是由许多树组成的森林。需要将每个没有父节点的根节点记录下来,求得每棵树中转换的最大步数,再求max
由于1号节点为根节点,有些解法在最后dfs时只dfs了1号节点,答案也正确,所以就认为本题结构只有一棵树
实际上是因为题中要求的每个数的约数和都小于它本身,也就是每个数的父节点都小于它本身
所以最终以1号节点为根的树是最大的,所以只dfs1号点也能得出答案,但实际上不仅仅只有以1号点为根的树
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 50010;
int n;
int h[N], e[N], ne[N], idx;
int sum[N];
bool st[N]; // 记录哪些节点为根节点
int ans;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 建立的是有向树,所以参数中不需要包含父节点
int dfs(int u)
{
int d1 = 0, d2 = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
int d = dfs(j) + 1; // 树的边权为1
if (d >= d1) d2 = d1, d1 = d;
else if (d >= d2) d2 = d;
}
ans = max(ans, d1 + d2);
return d1;
}
int main()
{
scanf("%d", &n);
// 如果遍历每个数再分别求约数,那么总的时间复杂度为O(n * n ^ 1/2)
// 但如果遍历每个数字,将这个数字加入到它的倍数的约数中去,时间复杂度为O(nlogn)
// 对于每个i,枚举i的倍数,将i加入到i的倍数的约数中去
// j从2开始,因为题目中要求约数的和不能包含数字本身
for (int i = 1; i <= n; i ++ )
for (int j = 2; j <= n / i; j ++ )
sum[i * j] += i;
// 初始化h数组
memset(h, -1, sizeof h);
// 因为一个数字只对应一个约数和
// 而一个约数和可能对应多个数字
// 所以要将约束和sum[i]作为数字i的父节点
// i从2开始枚举,因为当i等于1时,i的约数和为0,就加入了0节点
// 题目中要求在不超过n的正整数中转换,所以不能包含0节点
for (int i = 2; i <= n; i ++ )
if (sum[i] < i)
{
add(sum[i], i);
st[i] = true;
}
for (int i = 1; i <= n; i ++ )
if (!st[i])
dfs(i); // 求树的最大直径
printf("%d\n", ans);
return 0;
}
题目三:树的中心
给定一棵树,树中包含 n个结点(编号1~n)和 n−1条无向边,每条边都有一个权值。
请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。
输入格式
第一行包含整数 n。
接下来 n−1行,每行包含三个整数 ai,bi,ci,表示点 ai和 bi之间存在一条权值为 ci的边。
输出格式
输出一个整数,表示所求点到树中其他结点的最远距离。
数据范围
1 ≤ n ≤ 1e4
1 ≤ ai, bi ≤ n
1 ≤ ci ≤ 1e5
分析:
基本思路:
分别求出每个点到其他点的最长距离是多少,再取所有点中的min
根据dp的思想将所求的问题划分:
在无向树中每个点到其他点的最长距离可以分为两类:
-
向下走的最远距离(向子节点方向)
相当于以这个节点为根的子树的最大深度
-
向上走的最远距离(向父节点方向)
分为两种情况:
- 向上走到它的父节点后,父节点沿它的另一条分支往下走的最大深度
- 向上走到它的父节点后,继续向上走
这两类中的最长距离即为每个点到其他点的最长距离
注意根节点只存在向下走的最远距离,叶节点只存在向上走的最远距离
两遍dfs
:
- 从底部向上递归求出以每个节点为根节点的子树的最大深度(向下走的最远距离)
- 从顶部向下递归求出每个节点向上走的最远距离
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10010, M = 2 * N, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N];
int p1[N];
int up[N];
bool is_leaf[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int dfs_d(int u, int father)
{
d1[u] = d2[u] = -INF;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
int d = dfs_d(j, u) + w[i];
if (d >= d1[u])
{
d2[u] = d1[u];
d1[u] = d, p1[u] = j; // 记录最大路径长度值是走的j子节点的分支
}
else if (d >= d2[u]) d2[u] = d;
}
// 叶子节点没有子节点来更新向下的路径长度
if (d1[u] == -INF)
{
is_leaf[u] = true;
d1[u] = d2[u] = 0;
}
return d1[u]; // 返回路径长度的最大值
}
void dfs_u(int u, int father)
{
// 从上往下求up数组的值,根节点的up值为0
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
if (j == p1[u]) up[j] = max(up[u], d2[u]) + w[i];
else up[j] = max(up[u], d1[u]) + w[j];
// dfs下一层
dfs_u(j, u);
}
}
int main()
{
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 1; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs_d(1, -1);
dfs_u(1, -1);
// 当节点为根节点时只有向下的情况,当节点为叶节点时只有向上的情况
// 因为本题中存在负权边,而叶节点的d1和根节点的up都赋值为0,
// 所以在对叶节点或者根节点判断时可能出现错误,所以要进行特判
int res = d1[1];
for (int i = 2; i <= n; i ++ )
if (is_leaf[i]) res = min(res, up[i]);
else res = min(res, max(up[i], d1[i]));
printf("%d\n", res);
return 0;
}
题目四:有依赖的背包问题
有 N个物品和一个容量是 V的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如下图所示:
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V用空格隔开,分别表示物品个数和背包容量。
接下来有 N行数据,每行数据表示一个物品。
第 i行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。
输出格式
输出一个整数,表示最大价值。
数据范围
1 ≤ N, V ≤ 100
1 ≤ vi, wi ≤ 100
父节点编号范围:
- 内部结点:
1 ≤ pi ≤ N
- 根节点
pi = −1
分析:
dp
-
状态表示:
f[u][j]
- 集合:所有只考虑以第
u
个节点为根的子树(包括第i个节点),且总体积不超过j
的选法的集合 - 属性:最大价值
max
- 集合:所有只考虑以第
-
状态计算:
先枚举物品组,再枚举体积(从大到小枚举(每个物品组选或者不选为01背包)),最后枚举决策(选法或者体积)
// 本题物品以体积划分决策
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx;
int f[N][N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
for (int i = h[u]; ~i; i = ne[i]) // 循环物品组(子树)
{
int son = e[i];
dfs(son); // 递归 确保在处理以u为根的子树时,子树的节点信息已经初始化
// 要留出根节点的空间
for (int j = m - v[u]; j >= 0; j -- ) // 循环物品组的体积,从大到小循环
for (int k = 0; k <= j; k ++ ) // 从小到大循环决策(以不同的体积划分决策)
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
// 加上根节点的价值
for (int j = m; j >= v[u]; j -- ) f[u][j] = f[u][j - v[u]] + w[u];
// 将体积小于根节点体积的节点置为0
for (int j = 0; j < v[u]; j ++ ) f[u][j] = 0;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
int root;
for (int i = 1; i <= n; i ++ )
{
int p;
scanf("%d%d%d", &v[i], &w[i], &p);
if (p == -1) root = i;
else add(p, i);
}
dfs(root);
printf("%d\n", f[root][m]);
return 0;
}
题目五:二叉苹果树
有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。
这棵树共 N个节点,编号为 1至 N,树根编号一定为 1。
我们用一根树枝两端连接的节点编号描述一根树枝的位置。
一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。
这里的保留是指最终与1号点连通。
输入格式
第一行包含两个整数 N和 Q,分别表示树的节点数以及要保留的树枝数量。
接下来 N−1行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。
输出格式
输出仅一行,表示最多能留住的苹果的数量。
数据范围
1 ≤ Q < N ≤ 100
N ≠ 1
每根树枝上苹果不超过3e4
个。
分析:
由于最终保留的所有树枝都与1号点相连通,因此我们可以把所有的节点看作一个物品,若要保留此节点物品,就必须保留它的父节点物品(有依赖关系),把每条边的权值当成所指向的子节点的物品的价值,每个子节点的体积为1,最终需要保留的枝条的数量可以看作背包容量,那么这就转换为了有依赖的背包问题
dp
-
状态表示:
f[u][j]
- 集合:所有在以
u
为根的子树中选,选择j
条边的选法的集合 - 属性:选法的最大价值
max
- 集合:所有在以
-
状态计算:
// 将每棵子树看作一个物品组求解
// 分组背包问题循环顺序:
-
先枚举物品组,再枚举体积(从大到小枚举(每个物品组选或者不选为01背包)),最后枚举决策(选法或者体积)
// 本题物品以体积划分决策
f[u][j] = max(f[u][j], f[u][j - k - 1] + f[son][k] + w[i]);
-
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110, M = 2 * N; // 对于无向树,M边数要为有向树边数的二倍
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int f[N][N]; // 在以u为根的子树中选,且总体积不超过j的选法的集合
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u, int father)
{
for (int i = h[u]; ~i; i = ne[i]) // 枚举物品组
{
if (e[i] == father) continue;
dfs(e[i], u);
for (int j = m; j >= 0; j -- ) // 枚举体积,从大到小枚举
for (int k = 0; k < j; k ++ ) // 枚举决策
// 每个决策包含子树和连接到子树的边,所以k < j,并且剩余体积为j - k - 1
f[u][j] = max(f[u][j], f[u][j - k - 1] + f[e[i]][k] + w[i]);
}
}
int main()
{
scanf("%d%d", &n, &m);
// 初始化h数组
memset(h, -1, sizeof h);
// 前两个数a,b是它连接的节点的编号
// a不一定是b的父节点,所以要创建无向树
for (int i = 1; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
// 题目中给出树根编号一定为1
dfs(1, -1);
printf("%d\n", f[1][m]);
return 0;
}
题目六:战略游戏(树形dp + 状态机模型)
先不重不漏地分析清楚共有几个状态,每个状态是什么,在考虑状态间的状态转移关系
鲍勃喜欢玩电脑游戏,特别是战略游戏,但有时他找不到解决问题的方法,这让他很伤心。
现在他有以下问题。
他必须保护一座中世纪城市,这条城市的道路构成了一棵树。
每个节点上的士兵可以观察到所有和这个点相连的边。
他必须在节点上放置最少数量的士兵,以便他们可以观察到所有的边。
你能帮助他吗?
例如,下面的树:
只需要放置 1名士兵(在节点 1 处),就可观察到所有的边。
输入格式
输入包含多组测试数据,每组测试数据用以描述一棵树。
对于每组测试数据,第一行包含整数 N,表示树的节点数目。
接下来 N行,每行按如下方法描述一个节点。
节点编号:(子节点数目) 子节点 子节点 …
节点编号从 0到 N−1,每个节点的子节点数量均不超过 10,每个边在输入数据中只出现一次。
输出格式
对于每组测试数据,输出一个占据一行的结果,表示最少需要的士兵数。
数据范围
0 < N ≤ 1500
一个测试点所有 NN 相加之和不超过300650
。
分析:
类似于没有上司的舞会题目
没有上司的舞会:每条边上最多选择一个点,问最大权值
战略游戏:每条边上最少选择一个点,问最小覆盖数量
dp
-
状态表示
f[u][j], j = 0 / 1
- 集合:所有在以
u
为根的子树中选,且点u
的状态为j
的所有选法的集合 - 属性:所用士兵数量的最小值
min
- 集合:所有在以
-
状态计算:
f[u][0] = min(f[u][0], f[son1][1] + f[son2][1]);
f[u][1] = min(f[u][1], min(f[son1][1], f[son1][0]) + min(f[son2][1], f[son2][0]));
代码:
// 本题要求他们必须观察到所有的边
// 树形dp + 状态机模型
#include <iostream>
#include <cstring>
using namespace std;
// 输入时指明节点和子节点输入的先后顺序
const int N = 1510, M = N; // 有向树
int n;
int h[N], e[M], ne[M], idx;
bool st[N]; // 对于有向树,需要找到它的根节点
int f[N][2]; // 所有在以u为根节点的子树中选,且节点u的状态为j的选法的集合
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
f[u][1] = 1;
f[u][0] = 0; // 要记得初始化f[u][0]为0,因为本题中需要输入多组数据
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][1] += min(f[j][1], f[j][0]);
f[u][0] += f[j][1];
}
}
int main()
{
while (scanf("%d", &n) != -1)
{
// 输入多组数据,需要初始化
memset(h, -1, sizeof h);
idx = 0;
memset(st, false, sizeof st);
for (int i = 0; i < n; i ++ )
{
int a, cnt, b;
scanf("%d:(%d)", &a, &cnt);
while (cnt -- )
{
scanf("%d", &b);
add(a, b);
st[b] = true; // 标记b节点有父节点
}
}
int root = 0;
while (st[root]) ++ root;
dfs(root);
printf("%d\n", min(f[root][1], f[root][0]));
}
return 0;
}
题目七:皇宫看守(树形dp + 状态机模型)
先不重不漏地分析清楚共有几个状态,每个状态是什么,在考虑状态间的状态转移关系
太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。
皇宫各个宫殿的分布,呈一棵树的形状,宫殿可视为树中结点,两个宫殿之间如果存在道路直接相连,则该道路视为树中的一条边。
已知,在一个宫殿镇守的守卫不仅能够观察到本宫殿的状况,还能观察到与该宫殿直接存在道路相连的其他宫殿的状况。
大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。
可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。
帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。
输入格式
输入中数据描述一棵树,描述如下:
第一行 n,表示树中结点的数目。
第二行至第 n+1行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号 i,在该宫殿安置侍卫所需的经费 k,该结点的子结点数 m,接下来 m个数,分别是这个结点的 m个子结点的标号 r1, r2, …, rm。
对于一个 n个结点的树,结点标号在 1到 n之间,且标号不重复。
输出格式
输出一个整数,表示最少的经费。
数据范围
1 ≤ n ≤ 1500
分析:
战略游戏:每条边的两个端点处必须至少有一个点被覆盖(以边为单位),要求观察到所有的边,状态可以为101
皇宫看守:要么本点被覆盖,要么与本点相邻的点被覆盖(以点为单位),要求观察到所有的点,状态可以为1001
战略游戏中,每条边只有两个状态:要么让上面看到,要么让下面看到;
而本题中每个点有三个状态,要么让上面看到,要么让下面看到,要么让自己看到;
-
状态表示:
f[u][0]
表示点u
被父节点看到的最小花费,u
处没有放警卫f[u][1]
表示点u
被子节点看到的最小花费,u
处没有放警卫f[u][2]
表示在点u
处放警卫的所有摆放方案的最小花费,u
处有放警卫 -
状态计算:
f[u][0] = min(f[son][1], f[son][2]); son = son1、son2、……
f[u][2] = min(min(f[son][0], f[son][1]), f[son][2]); son = son1、son2、……
f[u][1] = min(f[k][2] + min(f[son][1], f[son][2])); son = son1、son2、…… son != k
代码:
// 本题要求他们观察到与该宫殿直接存在道路相连的其他宫殿的状况
#include <iostream>
#include <cstring>
using namespace std;
// 有向树
const int N = 1510, M = N;
int n;
int h[N], e[M], ne[M], idx;
int w[N]; // 在编号为i的宫殿安排侍卫所需的经费
bool st[N];
int f[N][3];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
f[u][2] = w[u];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += min(f[j][1], f[j][2]);
f[u][2] += min(min(f[j][0], f[j][1]), f[j][2]);
}
f[u][1] = 1e9; // 因为要求最小值,所以先将f[u][1]初始化为无穷大
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
f[u][1] = min(f[u][1], f[j][2] + f[u][0] - min(f[j][1], f[j][2]));
}
}
int main()
{
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 0; i < n; i ++ )
{
int a, k, cnt, b;
scanf("%d%d%d", &a, &k, &cnt);
w[a] = k;
while (cnt -- )
{
scanf("%d", &b);
add(a, b);
st[b] = true;
}
}
int root = 1;
while (st[root]) ++ root;
dfs(root);
printf("%d\n", min(f[root][1], f[root][2]));
return 0;
}
标签:idx,归纳,int,ne,dfs,树形,节点,DP,d1
From: https://blog.csdn.net/a123_120/article/details/142798048