记树形DP

树形DP

树形DP和线性DP的区别
在思维上,仅仅是迭代关系由线性转变成了非线性
比如说:
最大子段和
这题用f[i]表示以a[i]结尾的最大子串长度
最后只需要从1到N全部扫一遍求max即可
最大子树和
线性DP是线性的扫过一遍,最大子树和则是从叶子节点开始扫。
如果有儿子的话就在左和根,右和根,根,都有之间选一个,但是不能只有左右而没有根因为这样就不满足子树,对应到上面就是必须f[i-1]+a[i],a[i]
再考虑上面一层的时候就下面已经保证是选出来的最佳方案,满足无后效性所以是对的;
至于怎么迭代就是肾搜,先递归再DP
例题:
最大子树和

应用
树的直径

例题:
树的直径
树的直径的性质:(转自例题题解)

树的直径满足如下性质:

若有多条直径,则所有的直径之间皆有公共点。

证明:若果存在两条直径没有公共点,我们一定可以用这两条直径的四个端点中的某两个构造出一条更长的直径。

直径的两端一定是叶子。

证明:如果存在一个端点不是叶子,我们可以取那个端点的子节点,代替那个端点做直径,则构造出一条更长的直径。

树中距离某一直径端点最远的点,至少有一个是该直径的另一个端点。

证明:如果不是,那么我们一定可以用与之距离最远的点更新直径。

对于树上任意一个点,与之距离最远的一个点,至少有一个直径的端点。

证明:证明
这个证明的第二部分有疏漏。重新写一下
反证法:当点X不在直径上时,设有一个点是与其距离最远的点H,但不是直径AB上
那么在它通往H时,必然存在一个通向B与通向H的分界点,
此处再分类:
分界点要么在直径上要么不在直径上;显然对于两种情况来说分界点;到X的距离肯定要大于分界点到B的距离,如果分界点在直径上A有更远点,如果分界点不在原来直径上A还要走一段路才能到分界点那就肯定有更远点了,所以说假设不成立,命题得证;
利用如上性质搜两次出答案
但是!以下代码只适用于数据不卡你的时候,有可能存在一种情况使得从直径出发的时候会有两个点是距离最长的这时候最好去都搜一遍
发帖问人发现这是假的,我们只需要找出一条直倞即可;
以上情况说明可能会存在多种直径
证明一下:
距离根节点最长的这几个节点,必然会存在一或多个点,从这个点开始分支,这个点是有可能是根节点的;显然这个点越靠近根节点那么这两个点之间的距离就会越长,那么从任意一个所谓最远点开始搜,搜到的点必然是经过那个最靠近根节点的分界点的,而这条就一定是直径

代码广搜因为快

#include<cstdio>
#include<queue>
#include<cstring>
using namespace std;
const int MAXN=100010;
int n,d[MAXN];
bool vis[MAXN];
vector<int>G[MAXN];
int bfs(int s){	
	for(int i=1;i<=n;i++) d[i]=1e9;
	queue<int>q;
	d[s]=0;
	q.push(s);
	while(!q.empty()){
		int v=q.front();
		q.pop();
		for(int i=0;i<G[v].size();i++){
			int e=G[v][i];
			if(d[e]>d[v]+1&&e){
				d[e]=d[v]+1;
				q.push(e);
			}
		}
	}
	int maxn=0;
	int V=0;
	for(int i=1;i<=n;i++) if(maxn<d[i]&&d[i]!=1e9) maxn=d[i],V=i;
	return V; 
}
int main()
{
	scanf("%d",&n);
	for(register int i=1;i<n;i++)
	{
		int u,v; 
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
		G[v].push_back(u);
	}
	int r=bfs(1);
	r=bfs(r);
	printf("%d\n",d[r]);
	return 0;
}
树的重心

换根法:
虽然当前节点不一定是根,但是你可以先去假设他是根然后再去统计信息。。
树的重心很好求。每个点记忆化搜索一遍,记录下每个点的子树的节点的数量,然后再将这些与节点数除以2进行比较,选差距最小的那个
代码就不写了因为简单。

最大子树

DP一遍,见上

对于每个点求离每个点最远的点的距离

周二讲了很多没怎么听懂,所以这里有一种巨简单O(N)做法
就是根据树的直径的性质

对于树上任意一个点,与之距离最远的一个点,至少有一个直径的端点。

我们先找根,从根来bfs,然后找到了一个直径的端点此时再bfs找到了另外一个直径的端点
再bfs一遍,把两次bfs的结果都记录下来,对于每一个节点比较到两个直径端点的距离,长的那个就是最远距离点;

不能取两个相伶的节点,求取到的最大数

经典例题
没有上司的舞会
直接上代码和注释,比较久以前手打的,很拉很多余;

#include<iostream>
#include<cstdio>
using namespace std;
struct tree{
	int father;
	int happy;
	int son[2005];
   	int numson;
}a[6005];
int n,f[6005][2];
void dp(int x){
	f[x][0]=0;
	f[x][1]=a[x].happy;
	if(a[x].numson!=0)
	for(int i=1;i<=a[x].numson;i++){
		int y=a[x].son[i];
		dp(y);
		f[x][0]+=max(f[y][0],f[y][1]);//要么选要么不选。
		f[x][1]+=f[y][0];//只能不选。
	}
	return;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i].happy);
	}
	for(int i=1;i<n;i++){
		int l,k;
		scanf("%d%d",&l,&k);
		a[k].numson++;
		a[k].son[a[k].numson]=l;
		a[l].father=k;
	}
	int root;
	for(int i=1;i<=n;i++){
		if(a[i].numson==0){
			f[i][0]=0;
			f[i][1]=a[i].happy ;
		}
		else if(a[i].father==0){
			root=i;
		}
	}
	dp(root);
	printf("%d",max(f[root][0],f[root][1]));
	return 0;
}

显然

二叉/多叉苹果树

二叉苹果树
选课
这两题代码很像很像但又不完全一样
同:
这两题都是树上背包。都是有一定约束条件的,就是要求先选父节点再选子节点;
异:
二叉树和多叉树,一个选点一个选边,一个链表存一个邻接表存;

最草的就是转移方程怎么想;

其实对于每个节点而言都是部分背包,而对于整个树就是分组背包;
二叉:

void dfs(int x){
	for(int i=head[x];i;i=nexxt[i]){
		int y=to[i];
		dfs(y);
		zs[x]+=zs[y]+1;
		for(int j=min(q,zs[x]);j>=1;j--){
			for(int k=min(zs[y],j-1);k>=0;k--)
				f[x][j]=max(f[x][j],f[x][j-k-1]+f[y][k]+w[i]);
		}
	}
}

来解释一下为什么二叉的时候会这么列方程,
x表示的是一个点,对于这个下面的子树,一条一条去遍历,显然对于第一个被便利的子树
f[x][j]都是0,对于f[x][j-k-1]其实是可有可无的;
那么第二次去遍历的时候就能使用上f[x][j-k-1]了;f[y][k]表示的是当前正在被考虑的这个子树的贡献值,子树和x点之间需要有一条边来连接这就是w[i];w[i]也是Q中的一部分所以要[j-k-1];
对于每个子树来说,i不同,所以从一个子树到下一个子树的时候不需要担心会不会多选但是下面一个就不一样了;而且这个转移方程,如果说我把树改成多叉树,依然选边,这个方程还是能做的;

多叉:

void dfs(int x){
	for(int i=0;i<a[x].son.size();i++){
		int son=a[x].son[i];
		dfs(son);
		for(int j=m;j>=1;j--){
			for(int k=j-1;k>=0;k--)
			f[x][j]=max(f[x][j],f[son][k]+f[x][j-k]);
		}
	}
}

这个跟上面的不同就是这个题目选点,多叉二叉其实也不是很重要了;
因为这是选点,考虑的x点不能放在转移方程内部,不然会多算的,刚开始做的时候就是把上面复制一遍随便改改然后wa了,然后就是[j-k]而不是[j-k-1]因为没有把那个点给提出来算;
X点就把其权值付给f[x][1]就行了;
这个题原来是森林;
森林不森林也不重要,反正都接到一个点上就行了,m在处理过程中就要加一给0留下空间;
为什么说这样子写一定能保证把X点给选进去呢;
在遍历第一柯子树的时候,显然f[x][j]一定都是0;在状态转移方程中,f[x][j],f[son][k]+f[x][j-k]这两项取Max,在第一颗子树时,前一项显然没有意义,那么只能由后一项来更新,因为k=j-1时,j-k=1,所以对于每个被更新的j,f[x][j]都包含了X节点;

代码:
二叉:

#include<iostream>
#include<cstdio>
using namespace std;
const int N=1000010;
int n,m,a,b,cnt,c,q;
int to[N],head[N],nexxt[N],w[N],f[10000][1000],zs[N];
int dfs(int x){
	for(int i=head[x];i;i=nexxt[i]){
		int y=to[i];
		dfs(y);
		zs[x]+=zs[y]+1;
		for(int j=min(q,zs[x]);j>=1;j--){
			for(int k=min(zs[y],j-1);k>=0;k--)
				f[x][j]=max(f[x][j],f[x][j-k-1]+f[y][k]+w[i]);
		}
	}
}
int main()
{
	scanf("%d%d",&n,&q);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&a,&b,&c);
		nexxt[++cnt]=head[a];
		to[cnt]=b;
		head[a]=cnt;
		w[cnt]=c;
	}
	dfs(1);
	printf("%d",f[1][q]);
	return 0;
}

多叉:

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define int long long 
int n,m;
struct Node{
	int father;//里面装的是i
	int val;
	vector<int>son;
}a[350];
int f[310][310];//f[i][j]表示在i个节点选j个东西的Max值
void dfs(int x){
	for(int i=0;i<a[x].son.size();i++){
		int son=a[x].son[i];
		dfs(son);
		for(int j=m;j>=1;j--){
			for(int k=j-1;k>=0;k--)
			f[x][j]=max(f[x][j],f[son][k]+f[x][j-k]);
		}
	}
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	m++;
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&a[i].father,&f[i][1]);
		a[a[i].father].son.push_back(i);
	}
	dfs(0);
	printf("%lld\n",f[0][m]);
	return 0;
}
例题
积蓄程度(最大流)

积蓄程度
这题可以说是挺难的,至少我做了很久,发现了很多以前做题时都没有考虑到的纰漏,还是非常具有启发性的一道题;

先讲一下我的错误思路:
首先换根法没有问题,扫两遍也没有问题,问题就出在两次扫过去的时候我所储存的信息,还有对这个图的理解;
上面我已经说了,我写不太来邻接表,于是开始的时候去用链表来写这题;
题目中所说的:给出点X,Y,以及中间的容量;
我开始用用树来写,也就是X,Y之间随便认个爹,然后儿子节点的value赋为Z,对于叶子节点,d[i]=w[i],然后对于非叶子的节点,在它通往父亲的边的d就是min(d[l]+d[r],w[i]),第一遍DFS的时候非常方便,以至于我都不觉得这是错误的方法,第二遍搜索的时候出问题了,f[1]已经更新出来了,但是f[2]怎么更新有点难想,再往下我就彻底想不出来了,这么做如果能做的话可能还不如O(n2)划算,剩下的时间多做点别的题;
其实这么做的话,不足之处就在于我把每个点和其向上的边取min之后已经彻底完全的捆绑在一起了,一个无向图变成了有向图,那么在改方向的时候自然会很难改,总之这种阴间方法我是写不出代码,不知道有没有人能写出来;

然后我只能用邻接表去写了,我写的码不小心删了,这个是y总的:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010, M = N * 2, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int d[N], f[N], deg[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 fa)
{
    if (deg[u] == 1)
    {
        d[u] = INF;
        return d[u];
    }
    d[u] = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        d[u] += min(w[i], dfs_d(j, u));
    }
    return d[u];
}
void dfs_f(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        if (deg[j] == 1) f[j] = min(w[i], f[u] - w[i]);
        else
        {
            f[j] = d[j] + min(f[u] - min(d[j], w[i]), w[i]);
            dfs_f(j, u);
        }
    }
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d", &n);
        memset(h, -1, sizeof h);
        idx = 0;
        memset(deg, 0, sizeof deg);
        for (int i = 0; i < n - 1; i ++ )
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
            deg[a] ++ , deg[b] ++ ;
        }
        int root = 1;
        while (root <= n && deg[root] == 1) root ++ ;
        if (root > n)
        {
            cout << w[0] << endl;
            continue;
        }
        dfs_d(root, -1);
        f[root] = d[root];
        dfs_f(root, -1);
        int res = 0;
        for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);
        printf("%d\n", res);
    }
    return 0;
}

在这份代码中d数组的含义跟我的错误代码有所不同
d[i]表示能从下往上流到 i 节点的流量大小,没有跟w[i]取min,因为对于大部分的节点,肯定有不止一条的边和他相连,要用的时候再去和对应的边来取min;这样子显然更符合无向图的特点 ;
而且对于这个代码边和点是分开来存的,这样方便了很多而且清晰了很多;
还有一种想法,对于我这种用不熟邻接表的人,可以链表加上邻接矩阵,邻接矩阵可以用(vector | | map)&&pair来实现,yjc同学看过了,应该可以做这就更加适合菜鸡了;

各种题

吐槽下这些OJ连题解都没

1:给出一棵树 每个节点有权值 要求父节点和子节点不能同时取 求能够取得的最大值 (hdu1520)

没有上司的舞会

2:给出一棵树,求离每个节点最远的点的距离 (hdu2196)

就是上面的倞典应用

4:1>一棵树,定义每个节点的balance值:去掉这点节点后的森林里所有树的最大节点数。求出最小的balance值和其所对应的节点编号(poj1655)

树的重心

3:1>在一个地图上,有N座城堡,每座城堡都有一定的宝物,在每次游戏中允许攻克M个城堡并获得里面的宝物。但由于地理位置原因,有些城堡不能直接攻克,要攻克这些城堡必须先攻克其他某一个特定的城堡。求获得尽量多的宝物应该攻克哪M个城堡。 (hdu1561)

选课

33:给你一棵无向树 T,要求依次去除树中的某个结点,求去掉该结点后变成的森林 T’ 中的最大分支。并要求该分支为去除的结点尽可能少。答案可能有多个,需要按照节点编号从小到大输出 (poj3107)

树的重心,从小到大输出也不难,第一遍找出min,第二遍只要是符合min 的都push_back进去

5:给一棵树, n结点<=1000, 和K <=200, 在这棵树上找大小为k的子树, 使其点权和值最大 (zoj3201)

这题属于多差苹果树的类型,但是有点和之前不同之处在于他说的是求子树
我们之前在做多差苹果树的时候呢,将f[x][1]都初始化为1,以此来保证选子节点的时候他的父节点能被先选去,最后输出f[1][m],这题说的子树不一定包含根节点,所以最后再来一次遍历,输出
max(f[x][m])(1<=x<=n)

6:给一个树状图,有n个点。求出,去掉哪个点,使得剩下的每个连通子图中点的数量不超过n/2。如果有很多这样的点,就按升序输出。n<=10000 (poj2378)

7:一棵n个结点的带权无根树,从中删去一条边,使得剩下来的两棵子树的节点权值之和的绝对值最小,并求出得到的最小绝对值 (poj3140)

这题和之前树的重心有点不同的是他转化成了边;
但是丝毫不影响解决这道题
可以用邻接表来存
也可以用链表来存:每条边的权值存在他所到达的那条边上;随便选个根节点,根节点所存的权值就是0;因为这题没有涉及到有向图中方向的改变,且pushup的过程中符合结合律;
因为邻接表不是很熟练所以敲了个
代码

#include "FUCK YJC"

8:给出一些点,有值,给出一些边,然后求去掉一条边后将分成连通的两部分,且两部分的差值最小 (hdu2242)

树的重心,点权;

9:有n个点组成一个树,问至少要删除多少条边才能获得一棵有p个结点的子树 (poj1947)

10:一棵树n<=1000(节点的分支<=8),Snail在根处,它要找到在某个叶子处的house而其中一些节点上有worm,worm会告诉它的house是否在这个子树上求Snail最快寻找到house走过路径的期望值 (poj2057)

11:给你一颗苹果树,有N个节点每个节点上都有一个苹果也就是有一个权值,当你经过这个节点是你将得到这个权值,重复走节点是只能算一次,给你N-1条边。问你只能走K步能得到的最大权值和 (poj2486)

做不来暂时先空着

12:一颗二叉苹果树树上结苹果,要求剪掉几棵枝,然后求保留Q根树枝能保留的最多到苹果数 (ural1018)

二叉苹果树

13:给定一棵树,求最少连多少条边,使得每个点在且仅在某一个环内。 (poj1848)

14:在一棵树形的城市中建立一些消防站,但每个城市有一个最大距离限制,求需要的最小花费 (poj2152)

感谢 徐 倞 舟 老师的辛勤教诲

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值