题意简述
给定一颗树 ( (N) 个结点) , 树中每个结点有一个邮递员, 每个邮递员要沿着唯一的路径走向 (Capital) ( (1) 号结点 ) , 每到一个城市他可以有两种选择 :
- 继续走到下个城市.
- 让这个城市的邮递员替他出发.每个邮递员出发需要一个准备时间 (S[i]) , 他们的速度是 (V_i) ,表示走一公里需要多少分钟.
现在要你求出每个城市的邮递员到 (Capital) 的最少时间 ( 不一定是他自己到 (Capital) , 可以是别人帮他 )
数据范围: (N leq 10^5)
题目分析
把 (1) 号结点看作根, 题目相当于求每个结点到根的最短用时.
考虑 动态规划.
-
状态设计: 设 (F[i]) 表示从 (i) 号结点出发到根的最短用时.
-
状态转移方程:
(F[i] = min (F[j] + S[i] + (Dis[i] - Dis[j]) * V[i]))
解释: (Dis[i]) 表示 (i) 到根结点的距离, (j) 为 (i) 的祖先结点.
这个暴力转移的时间复杂度是 (O (n^2)) 的, 直接爆炸. 于是考虑优化.
开始转化方程:
首先把 (min) 忽略掉 :
(F[i] = F[j] + S[i] + (Dis[i] - Dis[j]) * V[i])
光翼展开 :
(F[i] = F[j] + S[i] + Dis[i] * V[i] - Dis[j] * V[i])
斗转星移项 :
(F[j] = V[i] * Dis[j] + F[i] - Dis[i] * V[i] - S[i])
斜率优化 的形式 !
把上式看作一条直线的解析式 :
(F[j]) 与 (Dis[j]) 一起看作二维平面中的点 ((Dis[j], F[j])) ;
(V[i]) 看作斜率 ;
(F[i] - Dis[i] * V[i] - S[i]) 看作纵截距 (即直线与 (y) 轴交点的纵坐标) ;
转移就等价于最小化纵截距! 这个可以通过维护下凸壳 + 二分解决.
"能单调队列吗?"
"不能!"
因为 (V[i]) 不具有单调性, 本题中最优决策点 不具有单调性.
这直接使得单调队列不能随意弹出队头决策点. 不能弹出队头自然就想到了 单调栈.
具体做法如下 :
- (DFS)
- 遇到一个结点
- 更新其答案
- 将其加入单调栈
- 回溯
发现算法实现的问题在于单调栈在 (DFS) 过程中的维护.
如果每次暴力的弹栈, 暴力的回溯恢复栈, 时间会直接爆炸.
不妨这样做 :
- 二分找到 决策点 (i) 的插入位置 (k + 1).
- 记录当前栈大小 (Top), 及 (Sta[k + 1]) 的值.
- 将 (Top) 赋值为 (k + 1).
- 回溯时只需恢复 (Top) 及 (Sta[k + 1]) 的值.
这相当于 假装 进行了弹栈操作, 从而保证每次只会修改一个位置的值, 便于快速还原.
再来考虑如何二分找到最优决策点 :
int Zoe (int p) {
int L, R, Mid;
L = 1, R = Top;
while (L <= R) {
Mid = (L + R) >> 1;
if (Slope (Sta[Mid - 1], Sta[Mid]) > K (p)) R = Mid - 1;
else if (Slope (Sta[Mid], Sta[Mid + 1]) < K (p)) L = Mid + 1;
else return Mid;
} return Mid;
}
(Sta[]) 为单调栈, (Slope) 求两点斜率, (K (p)) 即为 点 (p) 斜率 (V[p])
如图所示, 此时最优决策点应在 ([L, Mid - 1]) 中.
如图所示, 此时最优决策点应在 ([Mid + 1, R]) 中.
如图所示, 此时最优决策点就为 (Sta[Mid]).
再考虑如何二分找到当前点的插入位置.
由于本题中一条路径上的 (Dis) 是单调不降的, 在图像上的体现为决策点集 (x) 坐标的不降.
int Nico (int p) {
int L, R, Mid;
L = 1, R = Top;
while (L <= R) {
Mid = (L + R) >> 1;
if (Slope (Sta[Mid], Sta[Mid + 1]) < Slope (Sta[Mid], p)) L = Mid + 1;
else if (Slope (Sta[Mid - 1], Sta[Mid]) > Slope (Sta[Mid - 1], p)) R = Mid - 1;
else return Mid;
} return Mid;
}
如图所示, 此时 (p) 点弹不掉 (Sta[Mid + 1]) , 插入位置应更加靠后, 即 ([Mid + 1, R])
如图所示, 此时 (p) 点能弹掉 (Sta[Mid]) , 插入位置应更加靠前, 即 ([L, Mid - 1])
如图所示, 此时 (p) 点能弹掉 (Sta[Mid + 1]), 但弹不掉 (Sta[Mid]), 于是位置即为 (Mid + 1)
剩下的就没有问题了吧...
没有了吧...
有了吧...
了吧...
吧...
代码实现
#include <cstdio>
typedef long long LL;
const int N = 1e5;
int n;
LL S[N + 5], V[N + 5];
int Head[N + 5], Cnt;
struct Edge { int Nxt, To; LL Val; } E[N * 2 + 5];
LL F[N + 5], Dis[N + 5];
int Sta[N + 5], Top;
LL X (int);
LL Y (int);
LL K (int);
double Slope (int, int);
void Add (int, int, LL);
void DFS (int, int);
int Zoe (int);
int Nico (int);
int main() {
scanf ("%d", &n);
for (int i = 1; i <= n - 1; ++i) {
int u, v, w;
scanf ("%d%d%d", &u, &v, &w);
Add (u, v, 1ll * w), Add (v, u, 1ll * w);
} for (int i = 2; i <= n; ++i) scanf ("%lld%lld", &S[i], &V[i]);
DFS (1, 0);
for (int i = 2; i <= n; ++i) printf ("%lld ", F[i]);
return 0;
} LL X (int p) { return Dis[p];
} LL Y (int p) { return F[p];
} LL K (int p) { return V[p];
} double Slope (int u, int v) {
if (X (u) == X (v)) return 0;
return (double) (Y (u) - Y (v)) / (X (u) - X (v));
} void Add (int u, int v, LL w) {
E[++Cnt] = (Edge) { Head[u], v, w }, Head[u] = Cnt;
} int Zoe (int p) {
int L, R, Mid;
L = 1, R = Top;
while (L <= R) {
Mid = (L + R) >> 1;
if (Slope (Sta[Mid - 1], Sta[Mid]) > K (p)) R = Mid - 1;
else if (Slope (Sta[Mid], Sta[Mid + 1]) < K (p)) L = Mid + 1;
else return Mid;
} return Mid;
} int Nico (int p) {
int L, R, Mid;
L = 1, R = Top;
while (L <= R) {
Mid = (L + R) >> 1;
if (Slope (Sta[Mid], Sta[Mid + 1]) < Slope (Sta[Mid], p)) L = Mid + 1;
else if (Slope (Sta[Mid - 1], Sta[Mid]) > Slope (Sta[Mid - 1], p)) R = Mid - 1;
else return Mid;
} return Mid;
} void DFS (int p, int Fp) {
int tap = Zoe (p);
F[p] = F[Sta[tap]] + S[p] + (Dis[p] - Dis[Sta[tap]]) * V[p];
int tbp, ReTop, ReVal;
tbp = Nico (p);
ReTop = Top, ReVal = Sta[tbp + 1];
Sta[Top = tbp + 1] = p;
for (int i = Head[p]; i; i = E[i].Nxt) {
int v = E[i].To;
if (v == Fp) continue;
Dis[v] = Dis[p] + E[i].Val, DFS (v, p);
} Top = ReTop, Sta[tbp + 1] = ReVal;
}