声明
https://blog.csdn.net/no1_terminator/article/details/77824790
参考课件和讲授来自Accelerator
找树的直径
树的直径定义为一个树中最长的一条链。
- 一种做法比较显然,我们可以大力 DP,维护出一个节点向下的最长链F(x)和次长链G(x),保证F,G不出自同一个儿子,然后用二者加和来更新答案,同时更新父亲节点的最长链和次长链。
- 另外一种做法,则可以这样:随便选择一个点,然后找到距离这个点最远的点 A, 再以 A 为源点,找到距离 A 最远的一个点 B, AB 路径上的点就是直径。
对于第一种做法比较适合拓展到其他形式的 DP 上去,更新时维护次小的想法也比较适合推荐。同时可以求出任意一个子树的直径。
第二种方法我们可以拿出一些有利于解题的性质。
第二种方法不支持负权,这需要注意下。
找树的重心
对于一棵 n 个节点的无根树,找到一个点 A,使得把树变成以该点为根的有根树时,最大子树的结点数最小。A 叫做重心。
求法很简单,求 size 即可。
容易发现重心的各个儿子的 $ size<= n/2 $
支配集与独立集
1.求一个最大点集使得其中点的个数尽可能多且该集合中不存在两个点有边相连。
2.求一个最小点集使得树上的每个点都存在一个集合中的点与其相连。
两个问题很有代表性,这里讲一下解法。
第一个问题比较好解决,考虑令 f(x) 表示 x 点在集合中,以x 为根的子树能选取的最多点数,g(x) 表示x 点不在集合中,以 x 为根的子树能选取的最多点数。
考虑按照题意的合法性转移即可。
f(x) = ∑g(son)
g(x) =∑ max f(son), g(son)
第二个问题相对复杂,我们称选择的点能够“覆盖”与其相连的点,那么考虑一个点的合法状态有 3 种,分别设选择该点的状态为 f(x),这个点被儿子覆盖为 g(x), 这个点被父亲覆盖为h(x) 。f, h 函数的转移都很简单,对于 g, 分类讨论即可。
DP 的两种处理方法
前面默认我们都是使用了 DFS 来递归处理子树,然后回溯更新节点,但是实际操作中会存在问题。
Windows 下默认栈空间大小为 4Mb, Linux 下为 8Mb, 大量递归会堆栈溢出。
考虑这个转移的过程只需要所有的儿子都被更新完
。我们BFS 这颗树,然后按照bfs序倒过来处理 DP 是可以得到同样的效果的。
至此我们解决了这个问题。
树形 DP 为什么相较其他 DP 来比有难度
- 在树上进行,相较于序列,更新的方式更多,对思维难度和代码实现难度要求都更高。
- 对 DP 优化的考察更为明显,如何通过更优秀的状态表示将一个复杂度更高的动态规划降维。
- 背包问题的拓展以及树上背包。
接下来我们将通过习题来解决这些问题。
题
题
POJ 1463
给定一棵树,被选定的点可以覆盖所有和他向连的边,求覆盖所有的边需要最少多少个点? $n <=10^5 $。
下放到点即可
#include<cstdio>
#include<algorithm>
#include<string.h>
using namespace std;
const int MAX = 1500+9;
const int MAXM = 1000000;
int n;
int f[MAX], g[MAX];
//f[x]表示选定x,保证子树合法,子树x内最少需要选定的节点数
//g[x]表示不选x,....
struct edge{
int y, next;
}e[MAXM];
int head[MAX], cnt;
void add_edge(int x, int y) {
e[++cnt].y = y;
e[cnt].next = head[x];
head[x] = cnt;
}
void dfs(int x) {
g[x] = 0;
f[x] = 1;//初始化
for(int i = head[x]; i; i = e[i].next) {
dfs(e[i].y);
g[x] += f[e[i].y];
f[x] += min(f[e[i].y], g[e[i].y]);
}
}
int main() {
int k, x, y, root;
while(~scanf("%d",&n)) {
memset(f, 0, sizeof(f));
memset(g, 0, sizeof(g));
memset(head, 0, sizeof(head));
for(int i = 1; i <= n; i++) {
scanf("%d:(%d)",&x, &k);
if(i == 1) root = x;
for(int j = 1; j <= k; j++) {
scanf("%d",&y);
add_edge(x, y);
}
}
dfs(root);
printf("%d
",min(f[root], g[root])) ;
}
}
POJ 1155
一个树形网络,编号为 1 的是广播站,叶子节点为广播接收者,费用是从根节点到叶子结点的边权和,价值是所有选中的叶子结点价值和。
问在保证广播站收益不亏本的情况下最多能选择多少叶子结点?
数据范围支持 (n^3)
f[i] [j] : i节点下端有j个节点的收益 时的最大价值和
先粘上老师的代码,自己不会...
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#define N 3000+5
#define M 6000+5
const int inf = -100000000;
using namespace std;
int head[N];
int num[N];
struct graph
{
int next,to,val;
graph () {}
graph (int _next,int _to,int _val)
:next(_next),to(_to),val(_val){}
}edge[M];
int cnt;
inline void add(int x,int y,int z)
{
edge[++cnt] = graph(head[x],y,z);
head[x] = cnt;
}
inline int read()
{
int x=0,f=1;char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')f=-1;ch = getchar();}
while(ch >='0' && ch <='9'){x=(x<<1)+(x<<3)+ch-'0';ch = getchar();}
return x*f;
}
int f[N][N];
void init()
{
memset(head,0,sizeof head);
memset(num,0,sizeof num);
cnt = 0;
}
void DFS(int x)
{
for(int i=head[x];i;i=edge[i].next)
{
DFS(edge[i].to);
// for(int j=num[x];j>=0;--j)
// for(int k=1;k<=num[edge[i].to];++k)
// f[x][j+k] = max(f[x][j+k],f[x][j]+f[edge[i].to][k]-edge[i].val);
// num[x] += num[edge[i].to];
num[x] += num[edge[i].to];
for(int j=num[x];j>=0;--j)//一定要是j>="0",应该是因为它可以都不选,它代价-价值为负
for(int k=1;k<=num[edge[i].to];++k)
f[x][j] = max(f[x][j],f[x][j-k]+f[edge[i].to][k]-edge[i].val);
}
}
int main()
{
int n , m;
//freopen("13.in","r",stdin);
while(cin >> n >> m)
{
init();
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
f[i][j] = inf;
for(int i=1;i<=n-m;++i)
{
int k = read();
num[i] = 0;
for(int j=1;j<=k;++j)
{
int y = read() , z = read();
add(i,y,z);
}
}
for(int i=n-m+1;i<=n;++i)
num[i] = 1, f[i][1] = read();//num[]一定要在这初始化,不知道为啥..求解!
DFS(1);
for(int i=m;i>=0;--i)if(f[1][i]>=0){printf("%d
",i);break;}
}
}
poj 1947(luogu P1272
树形分组背包(我并不清楚....
注意初始化与定义相匹配
注意两个代码实现的细节
参考博客
#include<cstdio>
#include<algorithm>
#include<string.h>
using namespace std;
const int N = 150+9;
const int INF = 2147000047;
inline int read() {
char ch = getchar(); int f = 1, x = 0;
while(ch<'0' || ch>'9') {if(ch == '-') f = -1; ch = getchar();}
while(ch>='0' && ch<='9') {x = (x<<3)+(x<<1)+ch-'0'; ch = getchar();}
return x*f;
}
int n, p, ans;
int head[N], cnt;
struct edge{
int y, nxt;
}e[N<<1];
void add_edge(int x, int y) {
e[++cnt].y = y;
e[cnt].nxt = head[x];
head[x] = cnt;
}
int f[N][N], size[N];
//f[i][j]:保留i,使树i只含有j个节点的最小剪枝数
//不选i的情况另外考虑
void dfs(int x) {
size[x] = 1;
int tmp;
for(int i = head[x]; i; i = e[i].nxt) {
dfs(e[i].y);
for(int j = size[x]; j >= 1; --j) {
for(int k = 1; k <= size[e[i].y]; ++k) //考虑选择子树
f[x][j+k] = min(f[x][j+k], f[x][j]+f[e[i].y][k]-1);
//最开始初始化f时,将每个节点与它的直系儿子断开,使得x->e[i].y这条边被删去,现在选择了e[i].y,需要补上
}
size[x] += size[e[i].y];
}
}
int tmp_son[N], is[N], root;
int main() {
n = read(), p = read();
int x, y;
for(int i = 1; i < n; ++i) {
x = read(), y = read();
is[y] = 1, tmp_son[x]++;//统计每个节点的直系儿子数
add_edge(x, y);
}
memset(f, 0x3f3f3f3f, sizeof(f));
for(int i = 1; i <= n; ++i) {
if(!is[i]) root = i;//找到根(题目只有一颗树,那就是有根的
f[i][1] = tmp_son[i];//顺带初始化:分开i与直系儿子即可
}
dfs(root);
int ans = f[root][p];
for(int i = 1; i <= n; i++) //按题目要求,子树里也行
if(f[i][p] < ans) ans = f[i][p]+1;//除根节点外,其它点需要切除与父亲的联系
printf("%d
", ans);
return 0;
}
poj 1935
给你一个树,一些点和一个根,从根出发遍历给定的点(任意顺序),最后不必回根的最小路径. n <= 2*10^5
过给定点的距离和
分析来自150137
如果你精通数据结构的话,这题会有一些高端做法,支持数据范围扩大若干倍。这里只考虑这个问题。
首先我们一定是停在一个距离根最远的点,答案就是往返-最远点到跟的距离。求每个点到跟的距离很简单,考虑如何求出过给定点的距离和。
做法也很简单,如果一个点被标记,那就沿着这棵树向上爬,爬到最后一个没被标记的点,并更新答案,将沿途标记。
每个点最多被标记一次,所以复杂度 O(n).
总结
通过上面的题,我们发现,这些题还是比较简单的,状态表示常常与他给的限制和一些必要的元素组成,往往还会与背包相联系。
然而这些都是之前非常熟练的东西,我们只需要简单的处理一些小的细节。
——150137