【jzoj 5336】Timi / 提米树(DP)

Timi / 提米树

题目链接:jzoj 5336

题目大意

给你一棵树,树上点有权值。
然后树的分数是它们叶子节点权值和减去所有相邻叶子的代价。
相邻叶子是两个叶子节点,它们在 dfs 序之间没有别的叶子节点。代价则是它们之间的路径上(不包含这两个叶子节点)最大的点权值。
然后你可以对树进行若干次剪枝,剪枝是如果一个点的所有儿子都是叶子节点,你就可以把它的所有儿子删去。
要你给出能有的最大分数。

思路

首先我们来看一些性质:
它可以无限次操作剪枝,所以你可以相当于对一个点剪枝就是把它的儿子部分全部减掉。

然后不难发现你要让两个点成为相邻的叶子,一定要它们在原来树的某两个相邻叶子的路径上。

然后我们不难想出 O ( n 2 ) O(n^2) O(n2) 的暴力。
f i f_i fi 为 dfs 序为 1 ∼ i 1\sim i 1i 的处理好的最优分数。
然后不难想到枚举叶子,然后枚举叶子之间的路径,然后路径两边各自枚举,然后转移。
然后考虑优化,我们把转移的式子列出来:
f i = max ⁡ ( f j − max ⁡ ( g i , g j ) ) + a i f_i=\max(f_j-\max(g_i,g_j))+a_i fi=max(fjmax(gi,gj))+ai
g i g_i gi i i i 到 LCA 的路径中权值最大的点权, a i a_i ai 则是点权)
不难想到可以预处理出 g g g,以及 f − g f-g fg
显然 g g g 有单调性,可以把 g g g 分成 g i > g j g_i>g_j gi>gj g i ⩽ g j g_i\leqslant g_j gigj 两种。( i , j i,j i,j 分别代表左右两边的点)
那就枚举右边,找左边的分界点就搞个指针就可以了。

代码

#include<cstdio>
#include<vector>
#define INF 0x3f3f3f3f3f3f3f3f

using namespace std;

int n, fa[100001], deg[100001];
int a[100001], x, y, dfn[100001];
int Leaf[100001], tmp, f[100001];
int X[100001], Y[100001], s[100002];
int g[100002], left, right, num, ans;
vector <int> son[100001];

void dfs(int now) {
	dfn[now] = ++tmp;
	if (!son[now].size()) Leaf[++Leaf[0]] = now;
	for (int i = 0; i < son[now].size(); i++) {
		deg[son[now][i]] = deg[now] + 1;
		dfs(son[now][i]);
	}
}

void get_first() {
	x = Leaf[1];
	while (x != 1) {
		f[x] = a[x];
		x = fa[x];
	}
	a[x] = a[x];
}

void get_road() {
	X[0] = Y[0] = 0;
	int xx = x, yy = y;
	while (deg[xx] > deg[yy]) {
		X[++X[0]] = xx;
		xx = fa[xx];
	}
	while (deg[yy] > deg[xx]) {
		Y[++Y[0]] = yy;
		yy = fa[yy];
	}
	while (xx != yy) {
		X[++X[0]] = xx;
		Y[++Y[0]] = yy;
		xx = fa[xx]; yy = fa[yy];
	}
}

void get_ans() {
	x = Leaf[Leaf[0]];
	while (x != 1) {
		ans = max(ans, f[x]);
		x = fa[x];
	}
	ans = max(ans, f[x]);
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		scanf("%d", &x);
		for (int j = 1; j <= x; j++) {
			scanf("%d", &y);
			son[i].push_back(y);
			fa[y] = i;
		}
	}
	
	deg[1] = 1;
	dfs(1);
	
	get_first();
	for (int i = 2; i <= Leaf[0]; i++) {
		x = Leaf[i - 1]; y = Leaf[i];
		get_road();
		
		s[X[0] + 1] = 0;
		for (int i = X[0]; i >= 0; i--)//先跑出后缀
			s[i] = max(s[i + 1], a[fa[X[i]]]);
		g[0] = -INF;
		for (int i = 1; i <= X[0]; i++)//跑出原来路径只有左边的部分
			g[i] = max(g[i - 1], f[X[i]] - s[i]);
		left = right = -INF; num = X[0];
		for (int i = Y[0]; i >= 1; i--) {
			right = max(right, a[fa[Y[i]]]);
			while (num && right > s[num]) {//找到分界点
				left = max(left, f[X[num]]);
				num--;
			}
			f[Y[i]] = max(left - right, g[num]) + a[Y[i]];//左边和右边选优的那个
		}
	}
	
	get_ans();
	printf("%d", ans);
	
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值