树形DP.


树的最长路径

给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

现在请你找到树中的一条最长路径。

换句话说,要找到一条路径,使得使得路径两端的点的距离最远。

注意:路径中可以只包含一个点。

输入格式
第一行包含整数 n。

接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

输出格式
输出一个整数,表示树的最长路径的长度。

数据范围
1 ≤ n ≤ 10000,
1 ≤ ai,bi ≤ n,
−105 ≤ ci ≤ 105

输入样例:

6
5 1 6
1 4 5
6 3 9
2 6 8
6 1 7

输出样例:

22

在这里插入图片描述
在这里插入图片描述
本图中f[x]表示x的子节点到x的最长距离,而非以x为顶点的最长路径.因为本题的路径是两条边,所以f[x]表示其中一条边的最大的值,f[y],f[z]同理,两条边加起来即为以u为顶点最长路径
在这里插入图片描述
注:本题的答案不一定为以u为顶点的路径,因为u到x,y,z的距离可能均为负数,所以答案可以是以x,y或者z为顶点的路径
本题采用dfs的方法来求解,所以是求解方式自下而上的

补充
在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <cstring>
using namespace std;
const int N = 10010, M = N * 2;
int n;
int h[N], e[M], w[M], ne[M], idx;
int ans;
void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
} 
int dfs(int u, int father) // 当前节点 父节点 
{
	int dist = 0; // 当前节点向下走的最大长度 
	int d1 = 0, d2 = 0;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int j = e[i];
		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; 
	}
	ans = max(ans, d1 + d2);
	return dist;
}
int main()
{
	cin >> n;
	memset(h, -1, sizeof h);
	for (int i = 0; i < n - 1; i ++ ) {
		int a, b, c;
		cin >> a >> b >> c;
		add(a, b, c);
		add(b, a, c);
	}
	dfs(1, -1);
	cout << ans << endl;
	return 0;
}

笔记学习:
作者:灰之魔女
链接:https://www.acwing.com/solution/content/29832/
来源:AcWing


树的中心

给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。

输入格式
第一行包含整数 n。

接下来 n−1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

输出格式
输出一个整数,表示所求点到树中其他结点的最远距离。

数据范围
1 ≤ n ≤ 10000,
1 ≤ ai,bi ≤ n,
1 ≤ ci ≤ 105
输入样例:

5 
2 1 1 
3 2 1 
4 3 1 
5 1 1

输出样例:

2

在这里插入图片描述

#include <iostream>
#include <cstring>
using namespace std;
const int N = 10010, M = N * 2, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N], p1[N], 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 != -1; 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;
		}
		else if (d > d2[u]) d2[u] = d;
	}
	if (d1[u] == -INF) {
		d1[u] = d2[u] = 0;
		is_leaf[u] = true;
	}
	return d1[u];
}
void dfs_u(int u, int father)
{
	for (int i = h[u]; i != -1; i = ne[i]) {
		int j = e[i];
		if (j == father) continue;
		if (p1[u] == j) up[j] = max(up[u], d2[u]) + w[i];
		else up[j] = max(up[u], d1[u]) + w[i];
		dfs_u(j, u);
	}
}
int main()
{
	cin >> n;
	memset(h, -1, sizeof h);
	for (int i = 0; i < n - 1; i ++ ) {
		int a, b, c;
		cin >> a >> b >> c;
		add(a, b, c);
		add(b, a, c);
	}
	dfs_d(1, -1);
	dfs_u(1, -1);
	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(d1[i], up[i]));
	cout << res << endl;
	return 0;
} 

数字转换

如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。

例如,4 可以变为 3,1 可以变为 7。

限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。

输入格式
输入一个正整数 n。

输出格式
输出不断进行数字变换且不出现重复数字的最多变换步数。

数据范围
1 ≤ n ≤ 50000

输入样例:

7

输出样例:

3

样例解释
一种方案为:4→3→1→7。


整数分解 求约数:试除法,把从1到 sqrt(n) 全部判断一遍。
筛法:求数的全部倍数

本题就等价于找出一个森林中,直径最长的树,并求出该树的直径

#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)
{
	st[u] = true;
	int dist = 0;
	int d1 = 0, d2 = 0;
	for (int i = h[u]; ~ i; i = ne[i]) {
		int j = e[i];
		if (!st[j]) {
			int d = dfs(j);
			dist = max(dist, d);
			if (d > d1) d2 = d1, d1 = d;
			else if (d > d2) d2 = d; 
		}
	}
	ans = max(ans, d1 + d2);
	return dist + 1;
}
int main()
{
	cin >> n;
	
	// 筛法求约数 
	for (int i = 1; i <= n; i ++ ) 
		for (int j = 2; j <= n / i; j ++ )
			sum[i * j] += i;
	// 构建树 
	memset(h, -1, sizeof h);
	for (int i = 2; i <= n; i ++ )
		if (sum[i] < i)
			add(sum[i], i);
	
	// DP 求树林中最长树的长度 
	for (int i = 1; i <= n; i ++ )
		if (!st[i])
			dfs(i);
	
	cout << ans << endl;
	 
	return 0;
}

二叉苹果树

有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。

这棵树共 N 个节点,编号为 1 至 N,树根编号一定为 1。

我们用一根树枝两端连接的节点编号描述一根树枝的位置。

一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。

这里的保留是指最终与1号点连通。

输入格式
第一行包含两个整数 N 和 Q,分别表示树的节点数以及要保留的树枝数量。

接下来 N−1 行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。

输出格式
输出仅一行,表示最多能留住的苹果的数量。

数据范围
1 ≤ Q < N ≤ 100.
N ≠ 1,
每根树枝上苹果不超过 30000 个。

输入样例:

5 2
1 3 1
1 4 10
2 3 20
3 5 20

输出样例:

21

算法分析

树形dp + 分组背包问题

有依赖背包问题的改编

树枝上苹果的数量可以理解成该边的权值

状态表示

  • f[u][j]:表示所有以u为树根的子树中选,选j条边的最大价值

状态计算

  • 每一棵子树看出一组背包,若需要选择该子树son,则根结点u到子树son的边一定用上,因此能用上的总边数一定减1,总共可以选择j条边时,当前子树son分配的最大边数是j - 1,对于任意一棵子树有

  • f[u][j] = max(f[u][j], f[u][j - 1 - k] + f[son][k]+ w[i])

#include <iostream>
#include <cstring>
using namespace std;
const int N = 110, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int f[N][N]; // 第i个节点为根节点 选择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 + 1 <= j; k ++ ) // 枚举决策 预留一条连向父节点的边
                f[u][j] = max(f[u][j], f[u][j - k - 1] + f[e[i]][k] + w[i]);
    }
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ ) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }
    dfs(1, -1);
    cout << f[1][m] << endl;
    return 0;
} 

笔记学习:
作者:小呆呆
链接:https://www.acwing.com/solution/content/8035/
来源:AcWing


战略游戏

鲍勃喜欢玩电脑游戏,特别是战略游戏,但有时他找不到解决问题的方法,这让他很伤心。

现在他有以下问题。

他必须保护一座中世纪城市,这条城市的道路构成了一棵树。

每个节点上的士兵可以观察到所有和这个点相连的边。

他必须在节点上放置最少数量的士兵,以便他们可以观察到所有的边。

你能帮助他吗?

例如,下面的树:
1463_1.jpg.gif
只需要放置 1 名士兵(在节点 1 处),就可观察到所有的边。

输入格式
输入包含多组测试数据,每组测试数据用以描述一棵树。

对于每组测试数据,第一行包含整数 N,表示树的节点数目。

接下来 N 行,每行按如下方法描述一个节点。

节点编号:(子节点数目) 子节点 子节点 …

节点编号从 0 到 N−1,每个节点的子节点数量均不超过 10,每个边在输入数据中只出现一次。

输出格式
对于每组测试数据,输出一个占据一行的结果,表示最少需要的士兵数。

数据范围
0<N≤1500,
一个测试点所有 N 相加之和不超过 300650。

输入样例:

4
0:(1) 1
1:(2) 2 3
2:(0)
3:(0)
5
3:(3) 1 4 2
1:(1) 0
2:(0)
0:(0)
4:(0)

输出样例:

1
2

这是基础课原题 AcWing 285. 没有上司的舞会

显然我们不能 暴力枚举 树中所有结点 选(1) 与 不选(0) 的状态(时间复杂度 为 O(2n))

因此,会想到采用 分治 思想:
考虑以结点 u 为 根节点 的子树,该子树所有的 方案,可以由当前结点 放 或 不放哨兵 进行划分

这也是一种 状态机模型,我简略的绘制一下这个状态机:
在这里插入图片描述

  • 如果当前结点放置了哨兵(1),则该子树中,连向他的边的另一端,可以放置(1),也可以不放置(0)
  • 如果当前结点没有放哨兵(0),则该子树中,连向他的边的另一端,必须可以放置(1)

这样就可以轻易想到 状态的定义 了,具体如下:
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1510;
int n;
int h[N], e[N], ne[N], idx;
int f[N][2]; // 第i个节点 状态是0/1 的最少士兵数量 
bool st[N]; // 每个节点的状态 用于寻找根节点 
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
    f[u][0] = 0;
    f[u][1] = 1;
    for (int i = h[u]; ~ i; i = ne[i]) {
        int j = e[i];
        dfs(j);
        f[u][0] += f[j][1];
        f[u][1] += min(f[j][1], f[j][0]);
    }
}
int main()
{
    while (scanf("%d", &n) == 1) {
        memset(h, -1, sizeof h);
        memset(st, 0, sizeof st);
        idx = 0;
        for (int i = 0; i < n; i ++ ) {
            int id, cnt;
            scanf("%d:(%d)", &id, &cnt);
            while (cnt -- ) {
                int a;
                cin >> a;
                add(id, a); 
                st[a] = true;
            }
        }
        int root = 0;
        while (st[root]) root ++ ;
        dfs(root);
        cout << min(f[root][0], f[root][1]) << endl; 
    } 
    return 0;
} 

笔记学习:
作者:彩色铅笔
链接:https://www.acwing.com/solution/content/66365/
来源:AcWing


皇宫看守

太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。

皇宫各个宫殿的分布,呈一棵树的形状,宫殿可视为树中结点,两个宫殿之间如果存在道路直接相连,则该道路视为树中的一条边。

已知,在一个宫殿镇守的守卫不仅能够观察到本宫殿的状况,还能观察到与该宫殿直接存在道路相连的其他宫殿的状况。

大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。

可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。

帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。

输入格式
输入中数据描述一棵树,描述如下:

第一行 n,表示树中结点的数目。

第二行至第 n+1 行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号 i,在该宫殿安置侍卫所需的经费 k,该结点的子结点数 m,接下来 m 个数,分别是这个结点的 m 个子结点的标号 r1,r2,…,rm

对于一个 n 个结点的树,结点标号在 1 到 n 之间,且标号不重复。

输出格式
输出一个整数,表示最少的经费。

数据范围
1 ≤ n ≤ 1500

输入样例:

6
1 30 3 2 3 4
2 16 2 5 6
3 5 0
4 4 0
5 11 0
6 5 0

输出样例:

25

样例解释:
在2、3、4结点安排护卫,可以观察到全部宫殿,所需经费最少,为 16 + 5 + 4 = 25。

分析

本题是 AcWing 323. 战略游戏【树形DP+状态机模型】 的改版题

在 战略游戏 中,题目要求的放置方案是 每条边 都被 观察 到,而本题则是要求 每个点 都被 观察 到

因此我们还是同样使用 树形DP 来求解方案

本题的要求相对于 战略游戏 ,稍稍增加了一些 难度,原因如下:

如果只是要求 每条边 被 观察 到,那么我们在处理 父节点 时,枚举到一个 子节点 就可以直接进行讨论

  1. 父节点 放置 哨兵,所有子节点都 可放可不放 哨兵
  2. 父节点 不放 哨兵,所有子节点 都要放置 哨兵

但是在本题的 要求 中,每条边 变成了 每个点 就会出现如下三种情况:

  1. 父节点 放置 哨兵,所有子节点都 可放可不放 哨兵
  2. 父节点 不放 哨兵,但是他至少有一个 子节点 放置哨兵,观察住了他
  3. 父节点 不放 哨兵,但 父节点 的 父节点 放置哨兵观察,则 子节点 可放可不放 哨兵

这样每个结点就有 三种情况 要转移,简略状态机模型如下:

  1. 被父节点观察 (0)
  2. 被子节点观察 (1)
  3. 被自己来观察 (2)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1510;
int n;
int h[N], e[N], w[N], ne[N], idx;
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];
    int sum = 0;
    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]);
        sum += min(f[j][1], f[j][2]);
    }
    f[u][1] = 1e9;
    for (int i = h[u]; ~ i; i = ne[i]) {
        int j = e[i];
        f[u][1] = min(f[u][1], sum - min(f[j][1], f[j][2]) + f[j][2]);
    }
}
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i ++ ) {
        int id, cost, cnt;
        cin >> id >> cost >> cnt;
        w[id] = cost;
        while (cnt -- ) {
            int ver;
            cin >> ver;
            add(id, ver);
            st[ver] = true;
        }
    }
    int root = 1;
    while (st[root]) root ++ ;
    dfs(root);
    cout << min(f[root][1], f[root][2]) << endl;
    return 0;
}

笔记学习:
作者:彩色铅笔
链接:https://www.acwing.com/solution/content/66594/
来源:AcWing


没有上司的舞会

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1 ≤ i ≤ N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

输入格式
第一行一个整数 N。

接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。

输出格式
输出最大的快乐指数。

数据范围
1 ≤ N ≤ 6000,
−128 ≤ Hi ≤ 127

输入样例:

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

输出样例:

5

算法分析
树形dp

状态表示

  • f[u][0]:所有以u为根的子树中选择,并且不选u这个点的方案
  • f[u][1]:所有以u为根的子树中选择,并且选u这个点的方案
  • 属性:Max
    在这里插入图片描述
    时间复杂度 O(n)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6010;
int n, w[N];
int h[N], e[N], ne[N], idx;
int f[N][2];
bool st[N];
void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; 
}
void dfs(int u)
{
	f[u][1] = w[u];
	for (int i = h[u]; ~ i; i = ne[i]) { // 遍历当前节点的全部子节点的状态 ~i等价于i!=-1 
		int j = e[i];
		dfs(j);
		f[u][1] += f[j][0];
		f[u][0] += max(f[j][0], f[j][1]);
	} 
}
int main()
{
	cin >> n;
	for (int i = 0; i < n; i ++ ) cin >> w[i];
	memset(h, -1, sizeof h);
	while (n -- ) {
		int a, b;
		cin >> a >> b;
		add(b, a);
		st[a] = true;
	}
	int root = 1;
	while (st[root]) root ++ ;
	dfs(root);
	cout << max(f[root][0], f[root][1]) << endl;
	return 0;
} 

笔记学习:
作者:小呆呆
链接:https://www.acwing.com/solution/content/7920/
来源:AcWing

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值