声明

https://blog.csdn.net/no1_terminator/article/details/77824790

参考课件和讲授来自Accelerator

找树的直径

树的直径定义为一个树中最长的一条链。

  1. 一种做法比较显然,我们可以大力 DP,维护出一个节点向下的最长链F(x)和次长链G(x),保证F,G不出自同一个儿子,然后用二者加和来更新答案,同时更新父亲节点的最长链和次长链
  2. 另外一种做法,则可以这样:随便选择一个点,然后找到距离这个点最远的点 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 来比有难度

  1. 在树上进行,相较于序列,更新的方式更多,对思维难度和代码实现难度要求都更高。
  2. 对 DP 优化的考察更为明显,如何通过更优秀的状态表示将一个复杂度更高的动态规划降维
  3. 背包问题的拓展以及树上背包

    接下来我们将通过习题来解决这些问题。

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\n",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\n",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\n", ans);
return 0;
}

poj 1935

给你一个树,一些点和一个根,从根出发遍历给定的点(任意顺序),最后不必回根的最小路径. n <= 2*10^5

过给定点的距离和

分析来自150137

如果你精通数据结构的话,这题会有一些高端做法,支持数据范围扩大若干倍。这里只考虑这个问题。

首先我们一定是停在一个距离根最远的点,答案就是往返-最远点到跟的距离。求每个点到跟的距离很简单,考虑如何求出过给定点的距离和。

做法也很简单,如果一个点被标记,那就沿着这棵树向上爬,爬到最后一个没被标记的点,并更新答案,将沿途标记。

每个点最多被标记一次,所以复杂度 O(n).

总结

通过上面的题,我们发现,这些题还是比较简单的,状态表示常常与他给的限制和一些必要的元素组成,往往还会与背包相联系。

然而这些都是之前非常熟练的东西,我们只需要简单的处理一些小的细节。

——150137

树型DP(2)的更多相关文章

  1. POJ3659 Cell Phone Network(树上最小支配集:树型DP)

    题目求一棵树的最小支配数. 支配集,即把图的点分成两个集合,所有非支配集内的点都和支配集内的某一点相邻. 听说即使是二分图,最小支配集的求解也是还没多项式算法的.而树上求最小支配集树型DP就OK了. ...

  2. POJ 3342 - Party at Hali-Bula 树型DP+最优解唯一性判断

    好久没写树型dp了...以前都是先找到叶子节点.用队列维护来做的...这次学着vector动态数组+DFS回朔的方法..感觉思路更加的清晰... 关于题目的第一问...能邀请到的最多人数..so ea ...

  3. 【XSY1905】【XSY2761】新访问计划 二分 树型DP

    题目描述 给你一棵树,你要从\(1\)号点出发,经过这棵树的每条边至少一次,最后回到\(1\)号点,经过一条边要花费\(w_i\)的时间. 你还可以乘车,从一个点取另一个点,需要花费\(c\)的时间. ...

  4. 洛谷P3354 Riv河流 [IOI2005] 树型dp

    正解:树型dp 解题报告: 传送门! 简要题意:有棵树,每个节点有个权值w,要求选k个节点,最大化∑dis*w,其中如果某个节点到根的路径上选了别的节点,dis指的是到达那个节点的距离 首先这个一看就 ...

  5. 【POJ 3140】 Contestants Division(树型dp)

    id=3140">[POJ 3140] Contestants Division(树型dp) Time Limit: 2000MS   Memory Limit: 65536K Tot ...

  6. Codeforces 581F Zublicanes and Mumocrates(树型DP)

    题目链接  Round 322 Problem F 题意  给定一棵树,保证叶子结点个数为$2$(也就是度数为$1$的结点),现在要把所有的点染色(黑或白) 要求一半叶子结点的颜色为白,一半叶子结点的 ...

  7. ZOJ 3949 (17th 浙大校赛 B题,树型DP)

    题目链接  The 17th Zhejiang University Programming Contest Problem B 题意  给定一棵树,现在要加一条连接$1$(根结点)和$x$的边,求加 ...

  8. BZOJ 1564 :[NOI2009]二叉查找树(树型DP)

    二叉查找树 [题目描述] 已知一棵特殊的二叉查找树.根据定义,该二叉查找树中每个结点的数据值都比它左儿子结点的数据值大,而比它右儿子结点的数据值小. 另一方面,这棵查找树中每个结点都有一个权值,每个结 ...

  9. Codeforces 149D Coloring Brackets(树型DP)

    题目链接 Coloring Brackets 考虑树型DP.(我参考了Q巨的代码还是略不理解……) 首先在序列的最外面加一对括号.预处理出DFS树. 每个点有9中状态.假设0位不涂色,1为涂红色,2为 ...

  10. HDU 5905 Black White Tree(树型DP)

    题目链接  Black White Tree 树型DP,设$f[i][j]$为以$i$为根的子树中大小为$j$的连通块中可以包含的最小黑点数目. $g[i][j]$为以$i$为根的子树中大小为$j$的 ...

随机推荐

  1. ArrayList的输出以及一些问题

    //首先需要创建一个ArrayList ArrayList arr=new ArrayList(); //然后往ArrayList里面插入一些值 arr.add("a"); arr ...

  2. 2019-2020-1 20199305《Linux内核原理与分析》第六周作业

    系统调用的三层机制(下) (一)给MenuOS增加命令 (1)打开虚拟机,首先用rm -rf menu指令删除当前的menu目录,然后用git clone重新克隆一个新版本的menu,进入menu,运 ...

  3. python之爬取练习

    练习要求爬取http://yuedu.anyv.net/网址的最大页码数和文章标题和链接 网址页面截图: 代码截图: 完整代码: 根据网页显示页码的方式,爬取的所有页码中倒数第二个页码是最大页码. i ...

  4. 【洛谷5335】[THUSC2016] 补退选(指针实现Trie)

    点此看题面 大致题意: 三种操作:加入一个字符串,删除一个字符串,求最早什么时候以某个字符串为前缀的字符串个数超过给定值. \(Trie\) 这道题显然是\(Trie\)的暴力裸题. 考虑我们对于\( ...

  5. Codeforces Round #597 (Div. 2) E. Hyakugoku and Ladders 概率dp

    E. Hyakugoku and Ladders Hyakugoku has just retired from being the resident deity of the South Black ...

  6. IT兄弟连 Java语法教程 赋值运算符

    从本书之前的内容中就一直在使用赋值运算符.现在是正式介绍赋值运算符的时候了.赋值运算符是单个等号”=“.在Java中,赋值运算符的工作方式与所有其它计算机语言相同.它的一般形式如下: var = ex ...

  7. 【shell脚本】优化内核参数===

    一.Linux内核参数优化 Sysctl命令用来配置与显示在/proc/sys目录中的内核参数.如果想使参数长期保存,可以通过编辑/etc/sysctl.conf文件来实现.  命令格式: sysct ...

  8. Chapter 2 :重构的原则

    1,什么是重构? 在不改变软件可观察行为的前提下,使用一些重构的手法,提高代码可读性. 换句话说,在保持软件可用的前提下,修改代码使得更加容易被理解. 2,为什么重构? 为了后续的代码维护和修改,易读 ...

  9. 安全性测试:OWASP ZAP 2.8 使用指南(一):安全测试基础及ZAP下载、安装

    概览 本文意在对于OWASP's Zed Attack Proxy(ZAP)软件做一个基本使用指南介绍. ZAP是一个用于实施安全性测试的工具,即使没有很强的安全测试背景也可以很好的使用. 为了达到这 ...

  10. 关于python的十一道练习

    关于python的十一道练习 1.编写程序,输入一个自然数字符串,然后输出各位数字之和.例如,输入字符串1234,输出10. def sums1(): #第一题 strs=input('请输入一个自然 ...