提米树/【BZOJ2031】剪枝-DP

【问题描述】

Temmie 村里有很多提米树,上面长满了 Temmie 薄片。一天,Flowey 来到
Temmie 村做客,Temmie 们想要送给它一棵提米树。
对于这棵提米树,满足这样的性质:
·1.这棵树可以看做一棵 n 个点的无向无环图,每个点上长了一定数量的
Temmie 薄片,薄片数量记为这个点的权值,这些点被标记为 1 到 n 的整数,其
中 1 号点是树的根,没有孩子的点是树上的叶子。
·2.对于这棵树进行深度优先搜索,每个点从左到右按顺序遍历每个孩子,得
到这棵树的 DFS 序。
·3.定义(a,b)是一对相邻的叶子,当且仅当没有其它的叶子节点在 DFS 序上在
a,b 之间。每对相邻的叶子都会产生一个代价,代价为 a 到 b 路径上(不包含 a,b)
的点中,最大点权值。
·4.提米树可以提供决心,一棵提米树能提供的决心的数量是树上所有叶子上
长的 Temmie 薄片数量和,减去所有相邻叶子的代价。
Flowey 虽然不是以 Temmie 薄片为食的生物,但是它需要决心。这棵提米树
能提供的决心太少了,Temmie 们决定对这棵树进行若干次剪枝(可以不剪枝),使
得这棵树能提供的决心最多。
一次剪枝定义为:如果一个点的孩子都是叶子,就可以把它所有的孩子剪掉。

【输入格式】

从 temmie.in 中读入数据。
输入数据的第一行为一个正整数 n,代表提米树上点的数量。
接下来 n 行,对于每行:
第一个数为一个正整数,代表这个点上 Temmie 薄片的数量
第二个数为一个整数 num,代表这个点孩子的数量
接下来 num 个数,依次表示这个点从左到右的孩子。

【输出格式】

输出到 temmie.out 中
输出包含一个数,为这棵树剪枝后最多能提供的决心的数量。

【样例输入 1】

3
1 2 2 3
2 0
2 0

【样例输出 1】

3

【样例输入 2】

5
1 2 2 3
2 1 4
3 1 5
4 0
5 0

【样例输出 2】

6


思路

  • 关于这道题的两个关键性质/?:
  • 1.注意到这里求的都是每两个相邻叶子节点路径上的极值,用倍增求复杂度是nlogn,但用暴力dfs求刚好遍历整棵树复杂度O(n);
  • 2.无论如何剪枝,两个剪枝前不相邻的叶子都不会在剪枝后相邻【关键!!!】
  • dp[i]表示到i节点的答案,初步写出dp方程:dp[x]=dp[y]-max(h[x],H[y])+num[x],其中h[x]为x至lca的最大值,H[y]同理
  • 发现此dp方程枚举每一对h[x],H[y],时间复杂度为n^2(不是/然而并不会算...)超时100%,想到优化时间
  • 想到前缀优化,与前缀和类似,前缀最大值(然而并不能想到)
  • 又应为前缀最大值后h数组单调递增,想到双指针[套路]将复杂度降到O(n)
  • 思路看起来很复杂,但实现稍简单,具体注释见代码

代码

#include <iostream>
#include <cstdio>
#define inf 0x7f7f7f7f
#define maxn 100005
using namespace std;
int n,fa[maxn],head[maxn],cnt,keep[maxn],deep[maxn],w[maxn],e[maxn],h[maxn],H,num[maxn],dp[maxn],g[maxn],tmax;
struct fdfdfd{int next,to;}a[maxn];
void addedge(int x,int y){a[++cnt].to=y; a[cnt].next=head[x]; head[x]=cnt;}
int max(int a,int b){return a>b?a:b;}
void dfs(int x)
{
	deep[x]=deep[fa[x]]+1;
	if(!head[x]) keep[++cnt]=x;
	else for(int i=head[x];i;i=a[i].next) dfs(a[i].to);
}
int main()
{
	scanf("%d",&n);
	for(int i=1,tnum;i<=n;++i)
	{
		scanf("%d%d",&num[i],&tnum);
		for(int j=1,ch;j<=tnum;++j) scanf("%d",&ch),addedge(i,ch),fa[ch]=i;
	}
	cnt=0; dfs(1);//一遍dfs存储树的所有叶子节点
	int x=keep[1],y;
	while(x) dp[x]=num[x],x=fa[x];//初始化第一条链的值,dp[x]=num[x];
	for(int i=2;i<=cnt;++i)
	{
		w[0]=e[0]=0; x=keep[i]; y=keep[i-1];//w,e暴力爬树分别记录想x,y到lca路径上的点
		while(deep[x]>deep[y]) w[++w[0]]=x,x=fa[x];
		while(deep[y]>deep[x]) e[++e[0]]=y,y=fa[y];
		while(x!=y)
		{
			w[++w[0]]=x,x=fa[x];
			e[++e[0]]=y,y=fa[y];
		}//注意从w[0]~1记录从lca到叶节点,e同理
		g[0]=h[e[0]+1]=H=tmax=-inf; int r=e[0];
		for(int j=e[0];j;--j) h[j]=max(h[j+1],num[fa[e[j]]]);//前缀最大值[补充1]
		for(int j=1;j<=e[0];++j) g[j]=max(g[j-1],dp[e[j]]-h[j]);//此处g为for中dp转移的一部分,不懂先跳过
		for(int j=w[0];j;--j)
		{
			H=max(H,num[fa[w[j]]]);//省去H数组,每次直接求
			while(r&&H>h[r]) tmax=max(tmax,dp[e[r--]]);//找到e上最后一个小于H的节点
			dp[w[j]]=max(tmax-H,g[r])+num[w[j]];//[补充2]
		}
	}
	x=keep[cnt]; int ans=-inf;
        while(x) ans=max(dp[x],ans),x=fa[x];
        printf("%d
",ans);
	return 0;
}
//[补充1]:在阅读某些题解代码时不理解为什么时fa[e[j]],读题后发现“代价为 a 到 b 路径上(不包含 a,b)”。。。
//[补充2]:max中,tmax-H 是h数组<H时的 dp[y]-max(h[x],H[y]) 最大值,g[r]则是h数组>H时的最大值,
//也就同时解释为什么要提前求出g数组,而不能直接用dp[e[r]]-h[r]

  • 大概这道题主要的关键点在于性质2,虽然就算知道这条性质,代码的细节仍然多得令人发指,考试多半只能拿暴力
原文地址:https://www.cnblogs.com/wuwendongxi/p/13618972.html