文章目录
1、没有上司的舞会(最大独立集问题)
集合
f(u,0)、f(u,1)
f(u,0)
:所有从以u为根的子树中选择的方案,选择并且不选u这个点的方案
f(u,1)
:所有从以u为根的子树中选择的方案,选择并且选u这个点的方案
集合划分
状态计算
f(u, 0) = sum(max(f(si, 0), f(si, 1)))
f(u, 1) = sum(f(si, 0))
(因为选了根结点,子结点就不可以选择)
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
int happy[N];
int h[N], e[N], ne[N], idx;
int f[N][2];
bool has_father[N]; // 判断每个结点是否有父结点
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
f[u][1] = happy[u]; // 选择u这个结点
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &happy[i]);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ ) //读入n-1条边
{
int a, b;
scanf("%d%d", &a, &b);
has_father[a] = true;
add(b, a);
}
// 因为这个题没有告诉我们根结点是谁,所以需要自己寻找
int root = 1;
while (has_father[root]) root ++ ;
dfs(root);
printf("%d\n", max(f[root][0], f[root][1]));
return 0;
}
补充:LeetCode 337 打家劫舍 III
这个题与上一题一模一样,但是要注意代码的实现。
2、树的最长路径 / 树的直径
最经典的问题是不带权值的树的直径,也就是边数最长的两个结点之间的距离。
常用做法是:
1. 任取一点作为起点,找到距离该点最远的一个点u。然后再BFS
(能使用BFS就不使用DFS,因为考试C++栈仅有1M大小,如果DFS层数过多,会爆栈)
2. 再找到距离u最远的一个点v。然后继续BFS。
3. 那么u和v之间的路径就是一条最长直径。
结论:任取树中一个点
a
,找到距离a
最远的点u
,则u
一定是某条直径上的端点。如果证明了u
是某条直径的端点,从u
开始求出来的距离u
最远的点v
,u
和v
之间的路径一定就是树最长路径 / 直径。
证明:
反证法:若距离任意一点a
的最远点u
不是任意一条直径上的某一个端点,假设树的真正直径为b
与c
之间的连线。下面分情况讨论:
- 若连线
au
与连线bc
之间没有交点。在au
和bc
上各取一个点分别为x
与y
,由于树是连通的,即a、u、b、c
四个点之间可以相互连通,那么x
与y
之间也可以相互连通,所以就有如下图。
又由我们之前的假设可知: ① ≥ ② + ③ ①\ge②+③ ①≥②+③,即 ① + ② ≥ ① − ② ≥ ③ ①+②\ge①-②\ge③ ①+②≥①−②≥③,所以路径byxu
的长度大于等于路径bc
的长度,与假设矛盾;
- 若连线
au
与连线bc
之间有交点。同理, ① ≥ ② ①\ge② ①≥②,那么路径bxu
的长度大于等于路径bc
的长度,与假设矛盾。证毕。
现在讨论扩展之后,即加上权值之后的树的最长路径问题。
按照定义,我们要求出树的所有直径,然后在这所有直径中取一个最小值即可。从集合角度思考,需要先将这些直接分成若干类,在这每一类中去寻找一个最大值,最后在所有最大之中取一个最大值即可。
路径分类
任取一个点,我们将其作为根结点,对于每一条直径,我们将其归类到这条路径中最高的点的这一类中。于是我们可以以每个点来分类,所以集合划分就是所有路径上最高的点是该点的路径的集合。
如何求出所有挂到该点上的所有路径长度的最大值呢?可以先将该结点的所有子结点往下走的所有路径长度最大值先求出来。对于挂在该点上的所有路径可分为两大类:
- 一直往下走,直到走到叶子结点。它的路径最大长度就是枚举每一个子结点往下走的路径的最大距离,然后加上到根结点的
1
即可; - 路径穿过了该结点,往左子树或者右子树下面走。该结点的路径最大长度为从这两个方向下去的所有最长路径长度的最长的一条路径和第二长的一条路径的和。所以可以先求出每一个子结点的路径最大值,再将所有的最大值中取一个最大值即可。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e4 + 10, M = N << 1; //初始不确定树的拓扑结构,因此要建立双向边
int n;
int h[N], e[M], w[M], ne[M], idx;
int f1[N], f2[N], res;
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) //father表示u的父节点,因为该图为无向图,并且迭代过程中不能回到父节点,所以要特殊标记
{
f1[u] = f2[u] = 0; //以节点u为根的子树中,从子树某个节点到u的最长路为f1,次长路径为f2
for (int i = h[u]; i != -1; i = ne[i]) // 遍历邻边
{
int j = e[i];
if (j == father) continue; //如果是父节点, 则不走
dfs(j, u);
if (f1[j] + w[i] >= f1[u]) f2[u] = f1[u], f1[u] = f1