题目:https://blog.csdn.net/weixin_44584560/article/details/86599565

https://blog.csdn.net/dcx2001/article/details/78269908@##@@@

1.子树和计数。

这类问题主要是统计子树和,通过加减一些子树满足题目中要求的某些性质。(具有能够用DP解决的性质)

例如:

1.codeforces 767C Garland

 这道题是让你把树分成3部分,使每部分点权和相等,这就是通过算子树的size[]实现的。

2.洛谷1122最大子树和

这道题让你剪去一些子树,让剩下的子树点权和最大。这题就要维护子树的点权和,f[i]表示i这颗子树的点权和最大值。

1、codeforce garland

分割一棵树
分成三部分,每部分的和一样

//codeforce 的题,超时了???? 
//分割一棵树
//分成三部分,每部分的和一样
int summ=0,tot=0;
int w[maxn];  //暖度 
int to[maxn*2],fi[maxn],ne[maxn*2];
void link(int x,int y){
	to[++tot]=y;
	ne[tot]=fi[x];
	fi[x]=tot;
}
int n,root,sz[maxn],ans[5],cnt;
void dfs(int x,int fa){ //开始分割,要有fa是因为这是一棵无根树 
	sz[x]=w[x];
	for(int i=fi[x];i;i=ne[i]){
		int v=to[i];
		if(v!=fa){
			dfs(v,x);
			sz[x]+=sz[v]; //加上子树的值 
		}
	}
	if(sz[x]==summ) {   //在这里判断
		ans[++cnt]=x;
		sz[x]=0;   //清零
	}
}


int main(){
	int x,y;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d %d",&x,&y);
		if(x!=0){
			link(x,i);
			link(i,x);
		}
		else root=i;
		w[i]=y;
		summ+=y;
	}
	if(summ%3!=0){
		printf("-1\n");
		return 0;
	}
	summ/=3;
	dfs(root,0);
	if(cnt<=2) {
		printf("-1\n");
	}
	else printf("%d %d\n",ans[2],ans[1]);
return 0;
}

2、P1122 洛谷 最大子树和

是一个很标准的树形DP,上面的第二类:求在树上选一些点满足价值最大的问题

这题是一个比较常见的树型dp的模型(稍有变形):子树型,给一棵 n 个点的树,以 1 号点为根,求以每个点为根的子树大小

先设立状态: f[u] 表示以u为根且包含u的最大权联通块

状态转移方程:f[u]+=max(0,f[v]); (v为u的孩子) 有些儿子比较丑,美丽指数小于0,可能不选,所以与0作个比较

状态转移比较容易,主要基于dfs实现,还是要多做题才能熟练

int fi[maxn],to[maxn*2],nex[maxn*2];
int n,tot;
void link(int x,int y){
	to[++tot]=y;
	nex[tot]=fi[x];
	fi[x]=tot;
}
int dp[maxn],w[maxn];
int cut[maxn]; //记录要不要剪掉,如果这支树为负的话,就剪掉 
int maxx=-1;
void dfs(int x,int fa){
	dp[x]=w[x]; //先赋值 
	for(int i=fi[x];i;i=nex[i]){
		int t=to[i];
		if(t!=fa) dfs(t,x);
	}
	for(int i=fi[x];i;i=nex[i]){
		int t=to[i];
		if(t!=fa&&!cut[t]) dp[x]+=dp[t]; //因为后面递归的时候会自己判断 
	}
	if(dp[x]<0) cut[x]=1;
	maxx=max(maxx,dp[x]); //哪一根花卉最漂亮
}


int main(){
	int x,y;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	for(int i=1;i<=n-1;i++){
		scanf("%d %d",&x,&y);
		link(x,y);
		link(y,x);
	}
	dfs(1,1);
	printf("%d\n",maxx);
return 0;
}

 

2.树上背包问题

这类问题就是让你求在树上选一些点满足价值最大的问题,一般都可以设f[i][j]表示i这颗子树选j个点的最优解

1.洛谷1272重建道路

这道题是让你剪去最少的边实现最后剩P个点。所以P个点就是背包,剪去的边数就是价值。这题需要转化一下把剪边变成加边。

2.洛谷1273有线电视网

这道题是让你在保证不亏损的情况下满足更多的客户看到转播。此时用户的个数就是容量,f[i][j]表示i这颗子树选j个用户能赚的最多的钱。

1、P1272 洛谷 道路重建

剪去最少的边实现最后剩P个点。所以P个点就是背包,剪去的边数就是价值。这题需要转化一下把剪边变成加边。

dp[i][j]表示以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数

那么我们的初始化就变成了dp[i][1]=deg[i],也就是把与i相连的所有边全部拆掉。

 讲解:https://www.luogu.com.cn/problemnew/solution/P1272

int n,num,p;
/*
每个状态代表一棵子树,这个子树与父节点相连。 那么初始化的话dp[i][1]=son[i],一开始都是不连儿子只连父亲

那么转移的那个就变成了dp[u][j]=max(dp[u][j-k]+dp[v][k]-1)

为什么减1呢,因为注意到我们一开始点都是不与儿子相连的,我们通过dp的过程一步一步把他们连起来。

那么对于u->v这个边,一开始在初始化u的时候已经被减掉了,因为他是连接儿子的边,而v->u这个边并没有,因为这个连向父亲的边,
我们要通过v转移,就得用u->v这个边,所以就得补回来。

而且最终算的时候,除了根节点无父亲外,其他的都要和父节点断开,也就是边数+1

第二种就是dp[i][j]表示以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数。

那么我们的初始化就变成了dp[i][1]=deg[i]

也就是把与i相连的所有边全部拆掉。
*/
struct node{
	int to;
	int next;
}edge[maxn*2];
int head[maxn];
void link(int x,int y){
	edge[++num].next=head[x];
	edge[num].to=y;
	head[x]=num;
}
int dp[maxn][maxn]; //这个的含义是以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数。
int in[maxn]; //给个结点的度
void dfs(int x,int fa){
	dp[x][1]=in[x]; //初始化
	for(int i=head[x];i;i=edge[i].next){
		if(edge[i].to!=fa){
			dfs(edge[i].to,x);
		for(int j=p;j>0;j--){
			for(int k=1;k<j;k++)
			dp[x][j]=min(dp[x][j],dp[x][k]+dp[edge[i].to][j-k]-2); //为甚么减2
			//因为再两个in[]里面都加了 
		}
		}
	}
} 
 
int main(){
	scanf("%d %d",&n,&p);
	int x,y;
	for(int i=1;i<n;i++){
		scanf("%d %d",&x,&y);
		in[x]++;
		in[y]++;
		link(x,y);
		link(y,x);
	}
	memset(dp,0x3f,sizeof(dp));
	dfs(1,0);
	int ans=INF;
	for(int i=1;i<=n;i++) ans=min(ans,dp[i][p]);
	printf("%d\n",ans);
return 0;
}

第二种做法

#include <bits/stdc++.h>
#define ll long long 
using namespace std;
const int maxn=310;
const int INF=0x3fffffff;

inline int read(){
	int f=1,x=0;
	char ch;
	do{
		ch=getchar();
		if(ch=='-') f=-1;
	}while(ch<'0'||ch>'9');
	do{
		x=x*10+ch-'0';ch=getchar();
	}while(ch>='0'&&ch<='9');
	return f*x;
}
struct node{
	int u,v,next;
}edge[maxn];
int n,p;
int cnt,head[maxn];
int f[maxn][maxn];
int a[maxn];
bool b[maxn];
int root,ans=INF;
inline void add(int u,int v){
	edge[++cnt].v=v;
	edge[cnt].next=head[u];
	head[u]=cnt;
}
inline int dfs(int now){
	int temp,sum=1;
	for(int i=head[now];i;i=edge[i].next){
		int v=edge[i].v;
		temp=dfs(v);
		sum+=temp;
		for(int j=sum;j>=1;j--){
			for(int k=1;k<j;k++) f[now][j]=min(f[now][j],f[now][j-k]+f[v][k]-1);
		} 
	}
	return sum;
} 
int main(){
	n=read();p=read();
	int x,y;
	for(int i=1;i<=n-1;i++){
		x=read();y=read();
		a[x]++;
		b[y]=1;
		add(x,y);
	}
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++){
		if(!b[i]) root=i;
		f[i][1]=a[i]; //孩子数量 
	}
	dfs(root);
	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;
}

  

2、P1273 有线电视网

1.明确dp[i][j]含义,表示i节点,选j个用户,能得到的钱的最大值,然后对每个节点做分组背包。

2.怎么看出是分组背包?首先,背包的总容量相当于该点为根节点的子树中所有的用户数量(dp[i][j]的 j 不可能超过它连接的所有用户数)。然后,把该节点的每个儿子看成一组,每组中的元素为选一个,选两个...选n个用户。

3.转移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-这条边的花费) i,j不解释了,v表示枚举到这一组(即i的儿子),k表示枚举到这组中的元素:选k个用户。

4.最后输出dp[1][i]>=0的i的最大值,所以反向枚举。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m;//n为整个有线电视网的结点总数,m为用户终端的数量
//第一个转播站即树的根结点编号为1,其他的转播站编号为2到n-m,用户终端编号为n-m+1到n
int head[3010],k;
int val[3010];//用户为观看比赛而准备支付的钱数
int dp[3010][3010];//dp[i][j]表示i节点,选j个用户,能得到的钱的最大值 
struct node
{
    int to,next,w;
}edge[10000010];
void adde(int u,int v,int w)
{
    edge[++k].to=v; edge[k].next=head[u];
    edge[k].w=w; head[u]=k;
}
int dfs(int u)
{
    if(u>n-m)//u为用户终端
    {
        dp[u][1]=val[u];
        return 1;  //数量
    } 
    int sum=0,t;
    for(int k=head[u];k;k=edge[k].next)//该点连接了几个节点意为有几组,遍历每一组 
    {
        int v=edge[k].to;
        t=dfs(v); sum+=t; //t为该组的元素个数,或是说这个儿子为根的子树大小(这里的大小指子树中用户的个数),sum为该节点最多可以选多少个用户,即背包容量 
        for(int j=sum;j>0;j--)  //注意这两个界限
        {
            for(int i=1;i<=t;i++)//遍历组中的元素,选多少个用户相当于一个元素 
            {
                if(j-i>=0) dp[u][j]=max(dp[u][j],dp[u][j-i]+dp[v][i]-edge[k].w);
            }
        }
    }
    return sum;
}
int main()
{
    memset(dp,~0x3f,sizeof(dp));//初始化一个极大负值,因为dp可能为负
    scanf("%d%d",&n,&m);
    for(int u=1;u<=n-m;u++)
    {
        int size,v,w;
        scanf("%d",&size);
        for(int j=1;j<=size;j++)
        {
            scanf("%d%d",&v,&w);
            adde(u,v,w);
        }
    }
    for(int i=n-m+1;i<=n;i++) scanf("%d",&val[i]);
    for (int i=1;i<=n;i++) dp[i][0]=0;//选0个用户的花费肯定是0啦
    dfs(1);
    for (int i=m;i>=1;i--)
        if (dp[1][i]>=0)
        {
            printf("%d",i);
            break;
        }
    return 0;
}

3.花费最少的费用覆盖所有点

这类问题是父亲与孩子有联系的题。基本有两种出法:1.选父亲必须不能选孩子(强制)2.选父亲可以不用选孩子(不强制)。

1.洛谷2458 [SDOI2006]保安站岗
这题就属于类型2.这就需要预估,要不要选这个节点的父亲。f[i][0]表示选自己,f[i][1]表示选孩子,f[i][2]表示选父亲。
2.UVA 1220 Party at Hali-Bula(这是最大独立集问题,用做和上面题区分)  
这题就是强制要求父亲和孩子不能同时选,但这题没有要求必须把所有点完全覆盖,只是让选尽可能多的点,所以这样就可以用f[i][0]表示选自己,f[i][1]表示选孩子,省去f[i][2]表示选父亲了,因为没有都覆盖的强制要求,他的父亲选不选可以到他父亲再判断。

1、P2458 [SDOI2006]保安站岗

这题是父亲与孩子有限制的树形dp。

由于这个点放不放保安与儿子放不放保安和父亲放不放保安都有关系,所以假设f[x][0] 自己选,f[x][1] 自己不选,儿子选,f[x][2] 自己不选,父亲选。dp不是没有后效性吗?
为什么这个节点可以看他的父亲呢?其实是可以的,这个叫做未来计算,可以事先把还没发生但是肯定会发生的费用累加到答案中。那么这题就非常简单了。
dp方程:f[x][0]+=min(f[vv][1],min(f[vv][2],f[vv][0]));f[x][2]+=min(f[vv][0],f[vv][1]);dp[x][1]有一些特殊的地方因为自己不选儿子并不一定所有的儿子都选,
所以我们要选一个最优的儿子选。可以记一个f[vv][0]-f[vv][1]的最小值,最后把这个加进去就行了。
原文链接:https://blog.csdn.net/dcx2001/article/details/78269908

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=2010;
const int INF=0x3fffffff;
typedef long long LL;
//这个也是不用于最大独立集的那种题,每个点选择的代价是不一样的
//所以要开三维
/*
这题是父亲与孩子有限制的树形dp。

由于这个点放不放保安与儿子放不放保安和父亲放不放保安都有关系,所以假设f[x][0] 自己选,f[x][1] 自己不选,儿子选,f[x][2] 自己不选,父亲选。dp不是没有后效性吗?
为什么这个节点可以看他的父亲呢?其实是可以的,这个叫做未来计算,可以事先把还没发生但是肯定会发生的费用累加到答案中。那么这题就非常简单了。
dp方程:f[x][0]+=min(f[vv][1],min(f[vv][2],f[vv][0]));f[x][2]+=min(f[vv][0],f[vv][1]);dp[x][1]有一些特殊的地方因为自己不选儿子并不一定所有的儿子都选,
所以我们要选一个最优的儿子选。可以记一个f[vv][0]-f[vv][1]的最小值,最后把这个加进去就行了。
原文链接:https://blog.csdn.net/dcx2001/article/details/78269908
*/ 
int f[maxn][4],tot,n;
//f[x][0] 自己选,f[x][1] 自己不选,儿子选,f[x][2] 自己不选,父亲选
int in[maxn],v[maxn];
int to[maxn*2],head[maxn],next[2*maxn];
void add(int x,int y){
	to[++tot]=y;
	next[tot]=head[x];
	head[x]=tot;
}
void dfs(int x,int fa){
	f[x][0]=v[x]; //  自己的代价
	for(int i=head[x];i;i=next[i]){
		int vv=to[i];
		if(vv!=fa){
			dfs(vv,x);
		}
	} 
	int minn=INF;
	bool yes=0,have=0;
	for(int i=head[x];i;i=next[i]){
		int vv=to[i];
		have=true;
		f[x][0]+=min(f[vv][0],min(f[vv][1],f[vv][2]));
		f[x][2]+=min(f[vv][0],f[vv][1]);
		if(f[vv][0]<=f[vv][1]){
			f[x][1]+=f[vv][0];  //儿子选,要挑选 
			yes=1;
		}
		else{
			f[x][1]+=f[vv][1];
			minn=min(minn,f[vv][0]-f[vv][1]);
		}
	}
	if(!yes) f[x][1]+=minn;
	if(!have) f[x][1]=INF; 
}
int main(){
	scanf("%d",&n);
	int x,y,z,x1;
	for(int i=1;i<=n;i++){
		scanf("%d %d %d",&x,&y,&z);
		v[x]=y;
		while(z--){
			scanf("%d",&x1);
			add(x,x1);add(x1,x);
		}
	}
	memset(f,0,sizeof(f));
	dfs(1,0);
	printf("%d\n",min(f[1][1],f[1][0]));
return 0;
}

2、hdu Anniversary party 1520

选父不选子 不选父则都可以

using namespace std;
const int maxn=1010;
//没过 
const int INF=0x3fffffff;
//树形dp
//选父不选子   不选父则都可以
int dp[6010][2]; //0位不选,为选
vector<int> tree[6010];
int w[6010]; 
int n;
int fa[6010];
void dfs(int t){
	dp[t][0]=0;
	dp[t][1]=w[t]; //先初始化
	for(int i=0;i<tree[t].size();i++){
		int son=tree[t][i];
		dfs(son); //先递归 
		dp[t][0]+=max(dp[son][0],dp[son][1]);
		dp[t][1]+=dp[son][0];
	} 
}
int main(){
	scanf("%d",&n);
		for(int i=1;i<=n;i++){
			scanf("%d",&w[i]);
			fa[i]=-1;
			tree[i].clear();
		}
		int x,y;
	//	memset(dp,0,sizeof(dp));
		while(scanf("%d %d",&x,&y)){
			if(x==0&&y==0) break;
			fa[x]=y;
			tree[y].push_back(x);
		}
		int root=1;
		while(fa[root]!=-1){
			root=fa[root];
		}
		dfs(root);
		printf("%d\n",max(dp[root][0],dp[root][1]));
return 0;
}

poj 1463 Strategic game  和上面是一样的

在一个节点放士兵,就可以监视与这个点相连的所有点,求监视所有的路,至少需要多少士兵

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1510;
const int INF=0x3fffffff;
typedef long long LL;
//树形dp
//在一个节点放士兵,就可以监视与这个点相连的所有点,求监视所有的路,至少需要多少士兵
int n;
int pre[maxn],dp[maxn][2],child[maxn],root;
void DP(int root){
	int dp0=0,dp1=0;
	if(child[root]==0){  //是叶子节点 
		dp[root][0]=0;
		dp[root][1]=1;
		return;
	}
	for(int i=0;i<n;i++){
		if(pre[i]==root) {
			DP(i); //对孩子进行递归,最终到达叶子节点 
			dp1+=min(dp[i][0],dp[i][1]);
			dp0+=dp[i][1]; 
		}
	}
	dp[root][1]=dp1+1;
	dp[root][0]=dp0;
} 
 
int main(){
	while(~scanf("%d",&n)){
		memset(dp,0,sizeof(dp));
		memset(pre,-1,sizeof(pre));
		root=-1;
		int fa,m,x;
		memset(child,-1,sizeof(child));
		for(int i=0;i<n;i++){
			scanf("%d:(%d)",&fa,&m);
			child[fa]=m;
			if(root==-1) root=fa;
			while(m--){
				scanf("%d",&x);
				pre[x]=fa; 
				if(x==root) root=fa;  //找到根节点 
			}
		}
		DP(root);
		printf("%d\n",min(dp[root][1],dp[root][0])); 
	}
return 0;
}

 

 

 

 

 

4.树上统计方案数问题

这类问题就是给你一个条件,问你有多少个点的集合满足这样的条件。这类题主要运用乘法原理,控制一个点不动,看他能做多少贡献

1. 51nod1588幸运树。 问有多少个三元组满足幸运数字, 可以控制一个点不动,分成子树内,子树外两个部分,分别相乘就行了。

与多种算法结合&&大模拟

这种题只能根据题目分析,然后随机应变了。
1.洛谷3621 [APIO2007]风铃

把题目中的要求变成公式就行了。

一个树形dp的变形。

首先发现这个树一定是二叉树,根据邻接表的性质所以可以把左边的点最后加,这样边就是先左后右了。
设mindep[i]表示i这个点的子树中铃的最小深度,maxdep[i]表示最大深度。dp[i]表示使i这颗子树变成满足条件的需要几步,转移就是两个子树相加就行了,
需要注意的是不满足的情况,除了深度差大于1,还有这个条件不要丢掉。maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]]。

首先对于所有玩具如果有深度差超过1的就是无解(显然),所以dfs一遍记录最小最大的深度即可。然后如果有一个点的两颗子树中都含有最小、最大深度,那么这种情况也是无解,因为无论你怎么交换都不能使深度小的全部到右边去。

然后考虑最少交换次数:其实对于每一个节点的左右子树,三种情况需要交换,

  1. 左边全是小深度的,右边全是大深度的

  2. 左边全是小深度的,右边大小深度都有

  3. 左边大小深度都有,右边全是大深度的

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e5+10;
const int INF=0x3fffffff;
typedef long long LL;
int n,cnt,tot;
int dep[maxn],mindep[maxn],maxdep[maxn];
//深度 , 最小、最大深度 
int head[maxn*2],next[2*maxn],to[maxn],dp[maxn],w[maxn];
void add(int x,int y){
	to[++cnt]=y;
	next[cnt]=head[x];
	head[x]=cnt;
}
/*
一个树形dp的变形。

首先发现这个树一定是二叉树,根据邻接表的性质所以可以把左边的点最后加,这样边就是先左后右了。
设mindep[i]表示i这个点的子树中铃的最小深度,maxdep[i]表示最大深度。dp[i]表示使i这颗子树变成满足条件的需要几步,转移就是两个子树相加就行了,
需要注意的是不满足的情况,除了深度差大于1,还有这个条件不要丢掉。maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]]。

原文链接:https://blog.csdn.net/dcx2001/article/details/78269908
*/
int c[10];
void dfs(int x,int fa){
	for(int i=head[x];i;i=next[i]){
		int v=to[i];
		if(v==fa) continue;
		dep[v]=dep[x]+1;
		dfs(v,x);
		//先递归在更新
		mindep[x]=min(mindep[x],mindep[v]);
		maxdep[x]=max(maxdep[x],maxdep[v]); 
	}
	if(w[x]==-1)
	 mindep[x]=maxdep[x]=dep[x];  //这个就只有一种情况
	else{
		int cnt1=0;
		for(int i=head[x];i;i=next[i]){
	 	int v=to[i];
	 	if(v==fa) continue;
	 	c[++cnt1]=v;
	 } 
	  if((abs(maxdep[c[1]]-mindep[c[2]])>1)||(abs(mindep[c[1]]-maxdep[c[2]])>1) || (maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]])) {
	 	printf("-1");
	 	exit(0);
	 }
	 dp[x]=dp[c[1]]+dp[c[2]]+((mindep[c[1]]<maxdep[c[2]])?1:0);
	}
	 //三种情况是不可能有结果的
	 //第一种是两边差距>1或者两边都有最大最小 
	 
}
int main(){
	scanf("%d ",&n);
	int x,y;
	tot=n;
	for(int i=1;i<=n;i++){
		scanf("%d %d",&x,&y);
		if(y==-1) {
			y=++tot;w[tot]=-1;  //先弄右子树,这样左子树就是在前面,这样保证上面的c[1]是左子树
 		}
 		if(x==-1){
 			x=++tot;w[tot]=-1;
		 }
		 add(i,y);add(y,i);
		 add(i,x);add(x,i);
	}
	memset(dp,0x3f,sizeof(dp));
	dfs(1,0);
	printf("%d\n",dp[1]);
return 0;
}

//错了错了,landegaile
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
//#include<cmath>
using namespace std;
const int N=300010;
int pre[N*2],last[N],other[N*2],num,w[N];
int dep[N],mindep[N],maxdep[N],c[10],dp[N];
inline void add(int x,int y){
    num++;
    pre[num]=last[x];
    last[x]=num;
    other[num]=y;
}
void dfs(int x,int fa){
    for(int i=last[x];i;i=pre[i]){
        int v=other[i];
        if(v!=fa){
            dep[v]=dep[x]+1;
            dfs(v,x);
            mindep[x]=min(mindep[x],mindep[v]);
            maxdep[x]=max(maxdep[x],maxdep[v]);
        }
    }
    if(w[x]==-1){
        mindep[x]=maxdep[x]=dep[x];
    }
    else{
        int cnt1=0;
        for(int i=last[x];i;i=pre[i]){
            int v=other[i];
            if(v!=fa)
            c[++cnt1]=v;
        }
        if((abs(maxdep[c[1]]-mindep[c[2]])>1)||(abs(mindep[c[1]]-maxdep[c[2]])>1) || (maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]])){
            puts("-1");exit(0);
        }
        dp[x]=dp[c[1]]+dp[c[2]]+((mindep[c[1]]<maxdep[c[2]])?1:0);
    }
}
void dfs1(int x,int fa){
    printf("%d ",x);
    for(int i=last[x];i;i=pre[i]){
        int v=other[i];
        if(v!=fa)
        dfs1(v,x);
    }
}
int main(){
    int n;
    int x,y;
    scanf("%d",&n);
    int cnt=n;
    for(int i=1;i<=n;i++){
        scanf("%d%d",&x,&y);
        if(y==-1) y=++cnt,w[cnt]=-1;
        add(i,y);add(y,i);
        if(x==-1) x=++cnt,w[cnt]=-1;
        add(i,x);add(x,i);    
    }
    //cout<<other[last[2]]<<endl;
    memset(mindep,0x3f,sizeof(mindep));
    dfs(1,0);
    //if(maxdep[1]-mindep[1]>1) puts("-1"); 
    printf("%d\n",dp[1]);
    return 0;
} 

  

2. 51nod1673树有几多愁

这道题是非常强的综合题,用到了虚树+状压dp+树形dp。

3.51nod1531树上的博弈

非常强的一道博弈题。需要分情况的讨论

 

 

 一本通

2、hdu Computer 2196

对每个点,求距离它最远的点

	/* 
	while(scanf("%d",&n)){
		inti();
		dfs1(1);
		dp[1][2]=0; //根到上面的距离是0
		dfs2(1);
		for(int i=1;i<=n;i++) printf("%d\n",max(dp[i][0],dp[i][2])); //两个方向,要么是 
	}
	
	*/ 
	//下面的会超时。。。虽然好理解 
/*
struct node{
	int id;
	int cost; //到爸爸的距离 
};
vector<node> tree[maxn];
int dp[maxn][3];
//0代表到子树的最长距离,1代表到子树的次长距离,2代表从i往上走的最短距离
int n;
void inti(){
	for(int i=1;i<=n;i++) tree[i].clear();
	int x,co;
	memset(dp,0,sizeof(dp));
	for(int i=2;i<=n;i++){
		scanf("%d %d",&x,&co);
		node temp;
		temp.id=i;
		temp.cost=co;
		tree[x].push_back(temp);
	}
}
void dfs1(int fa){
	//最长距离,次长距离
	int one=0,two=0;
	for(int i=0;i<tree[fa].size();i++){
		node child=tree[fa][i];
		dfs1(child.id); //先递归
		int cost=child.cost+dp[child.id][0];
		if(cost>=one){
			two=one;
			one=+cost;
		} 
		else if(cost>two) two=cost;
	} 
	dp[fa][0]=one;
	dp[fa][1]=two;
}

void dfs2(int fa){  //先处理父节点,在处理子节点,所以处理顺序是从上到下,所以最后递归
	for(int i=0;i<tree[fa].size();i++){
		node temp=tree[fa][i];
		if(dp[temp.id][0]+temp.cost==dp[fa][0]){
			dp[temp.id][2]=max(dp[fa][2],dp[fa][1])+temp.cost; //如果再最长的路上,那么就加上次长的距离 
		}
		else 
		dp[temp.id][2]=max(dp[fa][2],dp[fa][0])+temp.cost;
		dfs2(temp.id);
	} 
	
}
*/



#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=10000+200;
struct edge
{
    int to;//终端点
    int next;//下一条同样起点的边号
    int w;//权值
} edges[MAXN*2];
int tot;//总边数
int head[MAXN];//head[u]=i表示以u为起点的所有边中的第一条边是 i号边
void add_edge(int u,int v,int w)//添加从u->v,权值为w的边
{
    edges[tot].to=v;
    edges[tot].w=w;
    edges[tot].next = head[u];
    head[u] = tot++;
}
int dist[MAXN][3];//dist[i][0,1,2]分别为正向最大距离,正向次大距离,反向最大距离
int longest[MAXN];
int dfs1(int u,int fa)//返回u的正向最大距离
{
    if(dist[u][0]>=0)return dist[u][0];  //记忆化
    dist[u][0]=dist[u][1]=dist[u][2]=longest[u]=0;  //递归边界
 
    for(int e=head[u]; e!=-1; e=edges[e].next)
    {
        int v= edges[e].to;
        if(v==fa)continue;
 
        if(dist[u][0]<dfs1(v,u)+edges[e].w) //更新最长
        {
            longest[u]=v;
            dist[u][1] = max(dist[u][1] , dist[u][0]);
            dist[u][0]=dfs1(v,u)+edges[e].w;
        }
//更新次长 else if( dist[u][1]< dfs1(v,u)+edges[e].w )//这里一定要记得,要不然无情WA dist[u][1] = max(dist[u][1] , dfs1(v,u)+edges[e].w); } return dist[u][0]; } void dfs2(int u,int fa) { for(int e=head[u];e!=-1;e=edges[e].next) { int v = edges[e].to; if(v==fa)continue; if(v==longest[u]) dist[v][2] = max(dist[u][2],dist[u][1])+edges[e].w; //如果在最长的一条上 else dist[v][2] = max(dist[u][2],dist[u][0])+edges[e].w; //不在最长的 dfs2(v,u); //后递归 } } int main() { int n; while(scanf("%d",&n)==1&&n) { tot=0; memset(dist,-1,sizeof(dist)); memset(head,-1,sizeof(head)); memset(longest,-1,sizeof(longest)); for(int i=2; i<=n; i++) { int v,w; scanf("%d%d",&v,&w); add_edge(i,v,w);//加边 add_edge(v,i,w);//加边 } dfs1(1,-1); dfs2(1,-1); for(int i=1;i<=n;i++) printf("%d\n",max(dist[i][0],dist[i][2])); } return 0; }

1575:【例 1】二叉苹果树   ---->第二类树上背包

dp[u][i]为u节点上保留i根树枝的至多保留的苹果数,那么转态转移方程便是dp[u][i]=max(dp[u][i],dp[u][i-k-1]+g[u][i].w+dp[v][k])

dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[t][k]+adj[u][i].wei);
//t是u的儿子,其实是在所有的儿子里面挑选,挑选出保留m条边能够获得的最大值。最后printf("%d\n",dp[1][m]);  //以1为根,保留m 

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
//设dp[u][i]为u节点上保留i根树枝的至多保留的苹果数,那么转态转移方程便是dp[u][i]=max(dp[u][i],dp[u][i-k-1]+g[u][i].w+dp[v][k])
struct node{
	int to,wei;
};
int n,m;
int dp[maxn][maxn];
vector<node> adj[maxn];
void dfs(int u,int root){
	for(int i=0;i<adj[u].size();i++){
		int t=adj[u][i].to;
		if(t==root) continue;  //不能往回搜 
		dfs(t,u);
		for(int j=m;j>=1;j--){  //遍历所有要保留的边的数量 
			for(int k=j-1;k>=0;k--){  //分离的树枝 
				dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[t][k]+adj[u][i].wei);
				//t是u的儿子,其实是在所有的儿子里面挑选,挑选出保留m条边能够获得的最大值 
			}
		}
	}
	
}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n-1;i++){
		int u,v,w;
		scanf("%d %d %d",&u,&v,&w);
		adj[u].push_back(node{v,w});
		adj[v].push_back(node{u,w});
	} 
	dfs(1,0);
	printf("%d\n",dp[1][m]);  //以1为根,保留m 
return 0;
}

  

1576:【例 2】选课  --->第二类树上背包

设f[now][j][k]表示以now为根节点的子树考虑前j个节点选k门课的方案数((这道题没有用

和上一题很像

dp[i][j]表示以i为根节点选j个的最优值    dp[now][j]=max(dp[now][j],dp[now][j-k]+dp[t][k]);

这道题还建立了虚拟源点,一般在森林里面,可以建立一个这样的超级源点。

这样只用在最后输出时调用dp[0][m+1]即可

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
//树形背包、DAG 
//设f[now][j][k]表示以now为根节点的子树考虑前j个节点选k门课的方案数
//因为1号节点是根节点,显然递推起点f[now][1][1]=val[now]
//然后又发现每门课有最多有一个先修课
//所以这一定是一个森林
//为了方便处理也是为了迎合输入数据 
//就把0号节点看做根节点即可    j建立超级源点 
int n,m;
struct node{
	int to,next;
}ed[maxn];
int dp[maxn][maxn];
int head[maxn],cnt; 
void add(int x,int y){
	ed[++cnt].to=y;
	ed[cnt].next=head[x];
	head[x]=cnt;
}
void DP(int now){
	for(int i=head[now];i;i=ed[i].next){
		int t=ed[i].to;
		DP(t);  //需要不断递归到底层 
		//可选的课程,跟上一题要保留的树枝一样 
		for(int j=m+1;j>=1;j--){
			for(int k=0;k<j;k++){  //注意这个递归顺序 
				dp[now][j]=max(dp[now][j],dp[now][j-k]+dp[t][k]);
			}
		}
	}
}

int main(){
	scanf("%d %d",&n,&m);
	int u,wei;
	for(int i=1;i<=n;i++){
		scanf("%d %d",&u,&wei);
		add(u,i);
		dp[i][1]=wei; //初始化在这里 选1个 
	}
	DP(0); //建立的虚拟源点0 !!! 
	printf("%d\n",dp[0][m+1]);
return 0;
}  

1577:【例 3】数字转换

其实这道题要仔细分析,分析以后就是知道怎样连边?只要比它小,就可以连边

连边之后,知道要求什么,求最长变换,就是求最长链,其实就是求树的直径

于每一个数,把它和它能够转移到的数之间连一条边。
由于不存在多元环,这个图本质上是一棵树。
然后在树上求最长链的长度就可以了。
具体实现就是dfs遍历整棵树,对于以每个点i为根的子树上的最长链长
形成一个无向图,然后求最长链就可以了
!!!!问题其实就是求树的直径,做两次dfs,随便选个点x做一次dfs找到直径的一个节点ans,然后ans做一个dfs得到直径

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=5e4+10;
const int INF=0x3fffffff;
typedef long long LL;
/*
对于每一个数,把它和它能够转移到的数之间连一条边。 
由于不存在多元环,这个图本质上是一棵树。 
然后在树上求最长链的长度就可以了。 
具体实现就是dfs遍历整棵树,对于以每个点i为根的子树上的最长链长
 形成一个无向图,然后求最长链就可以了 
 //!!!!问题其实就是求树的直径, 
*/
int n,ans,mm;
int in[maxn],summ[maxn];
bool vis[maxn];
int cnt,head[maxn];
struct node{
	int to;
	int next;  //建图 
}ed[maxn<<1];
void adde(int u,int v){
	ed[++cnt].to=v;
	ed[cnt].next=head[u];
	head[u]=cnt;
}
void dfs(int u,int step){
	vis[u]=1;
	if(step>mm){
		mm=step;
		ans=u;  //最长链 
	} 
	for(int i=head[u];i;i=ed[i].next){
		if(!vis[ed[i].to]) dfs(ed[i].to,step+1);
	}
}


int main(){
	summ[1]=0;
	summ[2]=1;
	summ[3]=1;
	for(int i=4;i<=50000;i++){
		for(int j=2;j*j<=i;j++){
			if(i%j==0){
				summ[i]+=j;
				if(j*j!=i) summ[i]+=i/j; //加上另一个家伙 
			}
		}
		summ[i]++; //加上1 
	}
	scanf("%d",&n);
	if(n==1) {
		printf("0\n");return 0;
	} 
	for(int i=2;i<=n;i++){
		if(summ[i]<i){  //从小向大连边 
			adde(summ[i],i);
			adde(i,summ[i]);
		}
	}
	dfs(1,1);
	memset(vis,0,sizeof(vis));
	mm=0;
	dfs(ans,1);
	printf("%d\n",mm-1);
	
return 0;
}

更简单的一个做法,都不用建图...

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
using namespace std;
typedef long long LL;
const int maxn=5e4+10;
const int INF=0x3f3f3f3f;
int summ[maxn],d1[maxn],d2[maxn],n;
void rea(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		for(int j=2;j<=n/i;j++){
			summ[i*j]+=i;   //预处理可以转移的数据 
		}
	}
}
void dp(){
	for(int i=n;i>=1;i--){ //从大到小建 
  		if(summ[i]<i){
  			if(d1[i]+1>d1[summ[i]]){
  				d2[summ[i]]=d1[summ[i]];
  				d1[summ[i]]=d1[i]+1;
			  }
			else if(d1[i]+1>d2[summ[i]]) d2[summ[i]]=d1[i]+1;
		  }
	}
}
int main(){
	int ans=0;
	rea();dp();
	for(int i=1;i<=n;i++) ans=max(ans,d1[i]+d2[i]);
	printf("%d\n",ans);
	return 0;
}

  

1578:【例 4】战略游戏  --->第三类儿子爸爸有限制   树的最大独立集

和上面的一样的,dp[maxn][2],开二维就好了,0表示不放,1表示放

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1510;
const int INF=0x3fffffff;
typedef long long LL;
int n;
int dp[maxn][2];
int pre[maxn];
int child[maxn];  //孩子数量 
int root=-1;
void dfs(int root){
	int dp0=0,dp1=0;
	if(child[root]==0){
		//直接赋值
		dp[root][0]=0;
		dp[root][1]=1;
		return; 
	}
	for(int i=0;i<n;i++){
		if(pre[i]==root){
			dfs(i);
			dp0+=dp[i][1];
			dp1+=min(dp[i][0],dp[i][1]);
		}
	}
	dp[root][0]=dp0;
	dp[root][1]=dp1+1;  //在这里加1 
}
int main(){
	scanf("%d",&n);
	int fa,x,num;
	memset(child,-1,sizeof(child));
	memset(pre,-1,sizeof(pre));   //从0开始,所以必须赋值为-1 
	for(int i=0;i<n;i++){
		scanf("%d %d",&fa,&num);
		child[fa]=num;
		if(root==-1) root=fa;
		while(num--){
			scanf("%d",&x);
			pre[x]=fa;
			if(x==root) root=fa;
		}
	}
	//cout<<root<<endl;
	dfs(root);
	printf("%d",min(dp[root][0],dp[root][1])); 
return 0;
}

  

1579: 【例 5】皇宫看守    --->第三类儿子爸爸有限制

这道题和上面不一样,要开三维,和Party那道题也不一样,

本题与战略游戏最大的区别,战略游戏是求树的最大独立集
最大独立集,选取每一个节点的代价是一样的,选取的点不相接得到的一定是最优的解
而这题的选取节点的代价是不相同的,故选取的点相接是有可能获得最优解的
f[u][0]表示i的父亲节点放了一个守卫,以i ii为根的子树最小需要的代价
f[u][1]表示i的子节点放一个守卫,以…(同上)
f[u][2]表示i自身放一个守卫,以…(同上)

在递归中不断更新,这个更新比较复杂,记住吧

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e6+10;  //!!!嘿嘿嘿嘿 
const int INF=0x3fffffff;
typedef long long LL;
//这道题与战略游戏不一样
/*
本题与战略游戏最大的区别,战略游戏是求树的最大独立集
最大独立集,选取每一个节点的代价是一样的,选取的点不相接得到的一定是最优的解
而这题的选取节点的代价是不相同的,故选取的点相接是有可能获得最优解的
f[u][0]表示i ii的父亲节点放了一个守卫,以i ii为根的子树最小需要的代价
f[u][1] f[u][1]f[u][1]表示i ii的子节点放一个守卫,以…(同上)
f[u][2] f[u][2]f[u][2]表示i ii自身放一个守卫,以…(同上)
*/ 
int n,root,cnt,m;
int head[maxn],wei[maxn],vis[maxn];
int dp[maxn][3]; //改为3维 
struct node{
	int to,next;
}ed[maxn];
void adde(int u,int v){
	ed[++cnt].to=v;
	ed[cnt].next=head[u];
	head[u]=cnt;
}
void readd(){
	scanf("%d",&n);
	int fa,num,x;
	root=-1;
	for(int i=1;i<=n;i++){
		scanf("%d",&fa);
		scanf("%d %d",&wei[fa],&num);
		while(num--){
			scanf("%d",&x);
			adde(fa,x);  //建立爸爸指向儿子的边 
			vis[x]++;  //入度++,入度为0的是root 
		}
	}
	for(int i=1;i<=n;i++){
		if(vis[i]==0) root=i;
	} 
}
/*
long long dp[N][2][2];
    //第一维[0,1]表示这个点是否有士兵
    //第二维[0,1]表示这个点是否安全 
    inline void dfs(int x,int fa)
    {
        int i;
        long long Min=inf;
        bool bo=0;
        for(i=head[x];i;i=Next[i]) if(to[i]!=fa)
        {
            dfs(to[i],x);
            dp[x][0][0]+=dp[to[i]][0][1];
            if(dp[to[i]][0][1]>dp[to[i]][1][1]) bo=1;
            else Min=min(Min,dp[to[i]][1][1]-dp[to[i]][0][1]);
            dp[x][0][1]+=min(dp[to[i]][0][1],dp[to[i]][1][1]);
            dp[x][1][1]+=min(min(dp[to[i]][0][0],dp[to[i]][0][1]),dp[to[i]][1][1]);
        }
        if(!bo)dp[x][0][1]+=Min;
        dp[x][1][1]+=Cost[x];
        return;
    }
*/ 
void dfs(int u,int fa){
	int d=INF;
	for(int i=head[u];i;i=ed[i].next){
		int v=ed[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][0]+=min(dp[v][2],dp[v][1]);  //爸爸没放,那么儿子要么自己放,要么儿子的儿子放
		dp[u][1]+=min(dp[v][2],dp[v][1]); //爸爸的子节点放,那么相当于爸爸还是没有放
		d=min(d,dp[v][2]-min(dp[v][2],dp[v][1]));
		//儿子放最便宜的 
		dp[u][2]+=min(dp[v][2],min(dp[v][1],dp[v][0])); 
		//如果爸爸放,那么儿子啥情况都可以,所有取所有的情况的最小值 
	}
	dp[u][1]+=d;  //加上儿子放的最小的代价
	dp[u][2]+=wei[u]; //加上自己的价钱 
}
int main(){
	readd();
	memset(dp,0,sizeof(dp));
	dfs(root,0);
	printf("%d",min(dp[root][2],dp[root][1])); 
return 0;
}

1580:加分二叉树   区间dp(不是树形PD TAT

很模板的NOIP原题,先区间dp搞一搞,顺便记下路径,递归输出方案就over了
要知道在中序序列里任选一个点作为根节点,其左边的点就是左子树的中序遍历序列,右边的点就是右子树的中序遍历序列,问题具有递归性质。枚举根节点按照题意更新即可。
用个数组记录下区间的根节点,递归打印前序序列。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
//很模板的NOIP原题,先区间dp搞一搞,顺便记下路径,递归输出方案就over了
//要知道在中序序列里任选一个点作为根节点,其左边的点就是左子树的中序遍历序列,右边的点就是右子树的中序遍历序列,问题具有递归性质。枚举根节点按照题意更新即可。
//用个数组记录下区间的根节点,递归打印前序序列。
//区间dp(不是树形PD TAT
int a[40],summ[40],dp[40][40],rel[40][40];
int n;
bool flag;
void print(int s,int e){
	if(flag) printf(" ");
	flag=true;
	if(s==e) printf("%d",s);
	else if(s+1==e) printf("%d %d",s,e);
	else{
		printf("%d",rel[s][e]);
		print(s,rel[s][e]-1);
		print(rel[s][e]+1,e);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		summ[i]=summ[i-1]+a[i];
	}
	for(int i=1;i<=n;i++){
		dp[i][i]=a[i];
		dp[i][i+1]=a[i]+a[i+1];
	}
	for(int len=2;len<n;len++){  //区间DP 
		for(int i=1,j=len+1;j<=n;j++,i++){
			dp[i][j]=0; //求最大值 
			for(int k=i+1;k<j;k++){
				int t=dp[i][k-1]*dp[k+1][j]+a[k];  //以k为根节点 
				if(t>dp[i][j]){
					dp[i][j]=t;
					rel[i][j]=k;  //记录根节点 
				} 
			}
			if(summ[j]-summ[i-1]>dp[i][j]){  //另一种选择 
				dp[i][j]=summ[j]-summ[i-1];
				rel[i][j]=j;
			}
		}
	}
	printf("%d\n",dp[1][n]);
	flag=false;
	print(1,n);
return 0;
}

我觉得这个程序更好理解

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=35;
const int INF=0x3fffffff;
typedef long long LL;
//这个写法我更好理解

int a[maxn];
int dp[maxn][maxn];
int pre[maxn][maxn];
int n;
void dfs(int l,int r){
	if(l>r) return;
	int x=pre[l][r];
	cout<<x<<" ";
	dfs(l,x-1);
	dfs(x+1,r);
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int len=1;len<=n;len++){
		for(int l=1;l<=n-len+1;l++){
			int r=l+len-1;
			if(l==r) dp[l][r]=a[l],pre[l][r]=l;//叶节点
			else{
				for(int k=l;k<r;k++){
					int left=1,right=1;
					if(k!=l) left=dp[l][k-1];///存在左子树
					if(k!=r) right=dp[k+1][r];  //存在右子树
					if(dp[l][r]<left*right+a[k]){
						dp[l][r]=left*right+a[k];
						pre[l][r]=k;
					} 
				}
			}
		}
	}
	cout<<dp[1][n]<<endl;
	dfs(1,n);	

return 0;
}

1581:旅游规划

这道题的本质是求出直径,然后求出在直径上的点  和上面的数字转换很像,而且都是求直径,可以看一下两种不同的求直径的方法
// dp一遍,求出一个点到除他所在的子树的最大距离。
//然后枚举所有点看看是不是字树内的最大距离加上子树外的最大距离等于直径长度。
//怎么求子树外的最大距离呢?
//其实只要从上到下,从根到叶子结点转移。
// u ,他的儿子 v ,f[v] 表示 v 子树外的最远距离。
//那么如果这个点吧 d1[u] 更新了,那么他就不能从 d1[u] 哪里得到更新
//不然就可以。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=2e5+10;
const int INF=0x3fffffff;
typedef long long LL;
//https://www.cnblogs.com/JCNL666/p/10728048.html
//这道题的本质是求出直径,然后求出在直径上的点 
// dp一遍,求出一个点到除他所在的子树的最大距离。
//然后枚举所有点看看是不是字树内的最大距离加上子树外的最大距离等于直径长度。
//怎么求子树外的最大距离呢?
//其实只要从上到下,从根到叶子结点转移。
// u ,他的儿子 v ,f[v] 表示 v 子树外的最远距离。
//那么如果这个点吧 d1[u] 更新了,那么他就不能从 d1[u] 哪里得到更新
//不然就可以。
int d1[maxn],d2[maxn],f[maxn];
//分别是最长路,次长路, 这个点向上能走的最长路
int n,max_dis=-1;
vector<int> e[maxn];
void dfs1(int u,int fa){  //第一个DFS:求这棵树的直径 
	for(int i=0;i<e[u].size();i++){
		int v=e[u][i];
		if(v==fa) continue;
		dfs1(v,u);
		if(d1[v]+1>d1[u]){   //最长 
			d2[u]=d1[u];
			d1[u]=d1[v]+1;
		}
		else if(d1[v]+1>d2[u]){  //次长 
			d2[u]=d1[v]+1;
		}
	}
	max_dis=max(max_dis,d1[u]+d2[u]);   //直径 
}
void dfs2(int u,int fa){
	int ans=0;
	for(int i=0;i<e[u].size();i++){
		int v=e[u][i];
		if(v==fa) continue;
		if(d1[v]+1==d1[u]) ans++;   //个数 
	}
	for(int i=0;i<e[u].size();i++){
		int v=e[u][i];
		if(v==fa) continue;
		if(d1[u]!=d1[v]+1||(ans>1&&d1[u]==d1[v]+1)) f[v]=max(f[u],d1[u])+1;
		//如果v不在u的最长子树那条路里面,或者在但是这并不是唯一的一条最长路
		//那么v到外面的最长路,除了可以考虑u到外面的最长路+1,或者也可以考虑u的像下面的最长路+1(到自己的兄弟 
		else f[v]=max(f[u],d2[u])+1;
		//不是的话,就可以考虑次长的兄弟,不能考虑自己呀,因为要么自己就是最长的,要么就是唯一的 
		dfs2(v,u);
	}
}
int main(){
	scanf("%d",&n);
	int u,v;
	for(int i=1;i<n;i++){
		scanf("%d %d",&u,&v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs1(0,0);
	dfs2(0,0);
	for(int i=0;i<n;i++){
		if(d1[i]+max(d2[i],f[i])==max_dis) printf("%d\n",i);
		//到子树下面的最长路或者到外面的最长路加起来等于直径 
	}
return 0;
}

1582:周年纪念晚会  --->第三类爸爸儿子有限制

和前面写过的party favor很像

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
const int MIN=-INF;
int n;
struct node{
	int x,isroot;
	vector<int> child;
}ed[1000001];
int dp[1000001][2];
void dfs(int x){
	dp[x][0]=0;
	dp[x][1]=ed[x].x;
	for(int i=0;i<ed[x].child.size();i++){
		dfs(ed[x].child[i]);
		dp[x][0]+=max(dp[ed[x].child[i]][1],dp[ed[x].child[i]][0]);
		dp[x][1]+=(dp[ed[x].child[i]][0]);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&ed[i].x);
		ed[i].isroot=1;
	}
	for(int i=1;i<=n-1;i++){
		int x,y;
		scanf("%d %d",&x,&y);
		ed[y].child.push_back(x);
		ed[x].isroot=0;
	}
	for(int i=1;i<=n;i++){
		if(ed[i].isroot==1){
			dfs(i);
			printf("%d",max(dp[i][1],dp[i][0]));
			return 0;
		}
	}
}

1583:叶子的染色

对于某一个节点,其被染成 x 色的代价,
//(1) 可以直接继承 其父亲被染成 x 色的代价
//(2) 可以保持父亲为 非 x 色,并将此节点单独染成 x 色

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e4+10;
const int INF=0x3fffffff;
typedef long long LL;
//还是不太理解
//https://www.cnblogs.com/luckyblock/p/11456303.html
//则,对于某一个节点,其被染成 x 色的代价,
   //(1) 可以直接继承 其父亲被染成 x 色的代价
   //(2) 可以保持父亲为 非 x 色,并将此节点单独染成 x 色
struct node{
	int from,to,next;
}ed[2*maxn];
int head[maxn],c[maxn];
int cnt,root,n,m;
int dp[maxn][2]; // f[i][j]表示,第i个点,将其染成j颜色,所需要的代价
inline int rread(){
	int f1=1,w=0;
	char ch=getchar();
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') f1=-1;
	while(isdigit(ch)){w=w*10+ch-'0';ch=getchar();
	}
	return f1*w;
} 
void adde(int u,int v){
	ed[++cnt].from=u;
	ed[cnt].to=v;
	ed[cnt].next=head[u];
	head[u]=cnt;
}
void dfs(int u,int fa){
	if(u<=m) return; //根节点,就直接返回 (叶节点的代价不需要被更新)
	for(int i=head[u];i;i=ed[i].next){ //枚举所有非父节点 
		int v=ed[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][1]+=min(dp[v][1]-1,dp[v][0]);  //如果和儿子和爸爸一样,就没有必要去花这个钱 
		dp[u][0]+=min(dp[v][0]-1,dp[v][1]);//用子节点,更新当前点的各值 
	} 
}
int main(){
	n=rread();m=rread();
	root=m+1;   //随便一个 
	for(int i=1;i<=m;i++) c[i]=rread();  //1~m表示叶子节点 
	for(int i=1;i<=n-1;i++){
		int u=rread(),v=rread();
		adde(u,v);
		adde(v,u);
	}
	for(int i=1;i<=n;i++){
		dp[i][0]=dp[i][1]=1;   //初始的代价都为1 
		if(i<=m) dp[i][c[i]^1]=INF;  //初始化为无穷大,表示不能该百年 
	}
	dfs(root,root);
	printf("%d",min(dp[root][1],dp[root][0])); //root要么染色要么布染色 
return 0;
}

1584:骑士

涉及基环树什么的概念,我现在理解不了
https://www.cnblogs.com/rvalue/p/7684069.html
https://www.cnblogs.com/LiGuanlin1124/p/10801620.html
https://www.cnblogs.com/Pedesis/p/10919226.html
由于每位骑士只有一个厌恶的骑士,所以我们考虑连一条被厌恶骑士指向该骑士的有向边。这样就会形成若干个连通块,每个连通块均为基环树,且不存在指向环的边。
然后对于每个连通块,先找到环的位置,然后删去环的一条边(形成树形结构),删去的边上的两点记作a和b,
以不选用a骑士和不选用b骑士分别做一次树形dp,得到最大战斗力。将每个连通块的最大战斗力相加,得到答案。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e6+10;
const int INF=0x3fffffff;
typedef long long LL;
//涉及基环树什么的概念,我现在理解不了
//https://www.cnblogs.com/rvalue/p/7684069.html
//https://www.cnblogs.com/LiGuanlin1124/p/10801620.html
///https://www.cnblogs.com/Pedesis/p/10919226.html 
/*
由于每位骑士只有一个厌恶的骑士,所以我们考虑连一条被厌恶骑士指向该骑士的有向边。这样就会形成若干个连通块,每个连通块均为基环树,且不存在指向环的边。
然后对于每个连通块,先找到环的位置,然后删去环的一条边(形成树形结构),删去的边上的两点记作a和b,
以不选用a骑士和不选用b骑士分别做一次树形dp,得到最大战斗力。将每个连通块的最大战斗力相加,得到答案。
*/ 
LL n,a[maxn];//仇视的对象 
LL to[maxn],next[maxn],head[maxn],dp[maxn][2],summ,cnt,root;
bool b[maxn];
bool vis[maxn];
bool book[maxn];
LL w,yx,temp;
void adde(int u,int v){
	to[++cnt]=v;
	next[cnt]=head[u];
	head[u]=cnt;
}
void findd(LL x,LL fa){
	book[x]=1;
	for(LL i=head[x];i;i=next[i]){
		if(to[i]!=fa){
			if(!book[to[i]]) findd(to[i],x);
			else{
				w=to[i];  //找到了环 
				yx=x;
				return;	
			}
		}
	}
}
void tree(LL x){
	vis[x]=1;
	b[x]=1;
	dp[x][0]=0;
	dp[x][1]=a[x]; //取得情况 
	for(LL i=head[x];i;i=next[i]){
		if(!vis[to[i]]){
			tree(to[i]);   //这个就和上司舞会一样了 
			dp[x][1]+=dp[to[i]][0];
			dp[x][0]+=min(dp[to[i]][1],dp[to[i]][0]);
		}
	}
}

void get_c(LL x){
	findd(x,-1);
	root=w;   //进行两次上司的舞会 
	tree(yx);
	temp=dp[w][0];  //不选 
	memset(vis,0,sizeof(vis));
	root=yx;  //第二次 
	tree(w);
	summ+=max(temp,dp[yx][0]);  //最两种情况里面最大的
	return; 
}

int main(){
	scanf("%lld",&n);
	LL x;
	for(int i=1;i<=n;i++){
		scanf("%lld %lld",&a[i],&x);
		adde(x,i);
		adde(i,x);
	}
	for(int i=1;i<=n;i++){
		if(!b[i]) get_c(i);
	}
	printf("%lld\n",summ);
return 0;
}

  

 

 posted on 2020-04-01 16:49  shirlybabyyy  阅读(230)  评论(0编辑  收藏  举报