The Elder(hdu 5956 树上斜率dp + 队列还原)

本文介绍了一种解决特定树形结构问题的算法——树形DP结合斜率优化技术,通过实例详细解析了如何利用单调队列进行状态转移优化,以达到O(n)的时间复杂度。

题目链接:

The Elder

 

题意:

给定一棵树,根节点为1,每条边都有一个权值。节点 i 到节点 j 的花费为这两点间路径权值和的平方。现在从每个点到根节点,可以在路径的任何点停留,每次停留cost+=这两点间路径权值和的平方+P(最后一步跳到根节点不需要+P),使该cost最小化。最后求所有点的cost的最大值。

 

思路:

每个点到根节点都是一条唯一路径:

设dp[i]:表示节点 i 到根节点的花费的最小值。

设sum[i]:表示路径上权值的前缀和。

dp[i]=dp[j]+(sum[i]-sum[j])^{2}+P  (0<=j<i)

复杂度O(n^2),对该路径上的权值进行斜率dp优化O(n):

设 j>k,且 j 优于 k :dp[j]+(sum[i]-sum[j])^{2}+P<dp[k]+(sum[i]-sum[k])^{2}+P

\frac{(dp[j]+sum[j]^{2})-(dp[k]+sum[k]^{2})}{sum[j]-sum[k]}<2sum[i]

设x=sum[i],y=dp[i]+sum[i]^2,上述不等式左边即为斜率。

可知:

当 \frac{(dp[j]+sum[j]^{2})-(dp[k]+sum[k]^{2})}{sum[j]-sum[k]}<2sum[i]<\frac{(dp[i]+sum[i]^{2})-(dp[j]+sum[j]^{2})}{sum[i]-sum[j]} 时,

j 优于 k 且 j 优于 i 。

因此维护一个下凸

因为sum[i]单调递增,所以维护单调队列,每次找到第一个 j 和 j+1 的斜率>2sum[i],j 即为最佳跳转点。

本题最关键的点在于单调队列的还原。如果树的每条链都单独计算,会有很多重复计算的边,复杂度会远超O(n)。因此需要回溯到一个点的时候把单调队列还原。

做法是dfs树的时候给每个点一个dfs序。维护一个栈,当单调队列里有点被弹出了,就把该点在单调队列的位置,值和被弹出的时间戳压入栈。当回溯到该点时,把栈中dfs序在该点之后的点还原,再把单调队列此时的 left 和 right 还原。

因为最后一步跳到根节点不需要+P,所以按上述式子多算了一次P,答案剪掉即可。

 

Code:

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
const int MAX = 2e5 + 50;

typedef struct {
	int to, val;
}Point;

typedef struct {
	int pos;
	int val;
	int time;
}Node;

int n, p;
vector<Point>mp[MAX];
ll dp[MAX], sum[MAX];
int Left, Right;
int q[MAX];
int dfstime;
stack<Node>s;

ll up(int j, int k) {
	return (dp[j] + sum[j] * sum[j]) - (dp[k] + sum[k] * sum[k]);
}

ll down(int j, int k) {
	return (sum[j] - sum[k]);
}

void dfs(int root, int fa)
{
	int nowtime = ++dfstime;
	while (Left < Right&&up(q[Left + 1], q[Left]) <= 2 * sum[root] * down(q[Left + 1], q[Left]))
		Left++;
	int front = q[Left];
	dp[root] = dp[front] + (sum[root] - sum[front])*(sum[root] - sum[front]) + p;
	while (Left < Right&&up(q[Right], q[Right - 1])*down(root, q[Right]) >= up(root, q[Right])*down(q[Right], q[Right - 1])) {
		s.push(Node{ Right,q[Right],nowtime });
		Right--;
	}
	q[++Right] = root;
	int le = Left, re = Right;
	for (int i = 0; i < mp[root].size(); i++) {
		if (mp[root][i].to == fa)    continue;
		sum[mp[root][i].to] = sum[root] + mp[root][i].val;
		Left = le, Right = re;
		while (!s.empty()) {
			Node tmp = s.top();
			if (tmp.time <= nowtime)    break;
			q[tmp.pos] = tmp.val;
			s.pop();
		}
		dfs(mp[root][i].to, root);
	}
	return;
}

int main()
{
	int T;
	scanf("%d", &T);
	while (T--)
	{
		while (!s.empty())
			s.pop();
		scanf("%d%d", &n, &p);
		for (int i = 0; i <= n; i++)
			mp[i].clear();
		for (int i = 1; i < n; i++) {
			int u, v, w;
			scanf("%d%d%d", &u, &v, &w);
			mp[u].push_back(Point{ v, w });
			mp[v].push_back(Point{ u, w });
		}
		dfstime = 0;
		dp[1] = 0;
		sum[1] = 0;
		Left = 1, Right = 1;
		q[1] = 1;
		int le = Left, re = Right;
		for (int i = 0; i < mp[1].size(); i++) {
			sum[mp[1][i].to] = sum[1] + mp[1][i].val;
			Left = le, Right = re;
			while (!s.empty()) {
				s.pop();
			}
			dfs(mp[1][i].to, 1);
		}
		ll ans = 0;
		for (int i = 1; i <= n; i++) {
			ans = max(ans, dp[i] - p);
		}
		printf("%lld\n", ans);
	}
	return 0;
}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值