题解 [NOI2014]购票
题目大意
有一个 \(n\) 个点的树,每个点有三个值 \(p_u,q_u,l_u\) ,现在可以从 \(u\) 走到点 \(v\) 当且仅当 \(v\) 是 \(u\) 的祖先并且 \(\text{dis}(u,v)\le l_u\) ,这样的花费为 \(\text{dis}(u,v)\times p_u+q_u\) 。问每个点到 \(1\) 所需的最小总花费。
\(n\le 2\times 10^5\) ,保证答案在 \(\text{long long}\) 范围内。
思路
还说还是看到 \(\text{Qiuly}\) 做这道题才做的,想要练习一下自己本来就菜的一批的斜率优化,结果发现自己除了斜率优化啥也不会了。。。
我们假设 \(f_u\) 为点 \(u\) 的答案,可以得到转移式:
然后我们就发现这个式子可以斜率优化了。假设对于点 \(u\) 存在点 \(j\) 比点 \(k\) 更优,可以得到:
然后我们发现这个东西我们可以维护一个下凸壳,但是因为 \(p_i\) 并不单调,所以我们直接在凸壳上面二分找到第一个斜率不大于 \(p_i\) 的点就好了。
但是我们发现我们这个东西其实是一棵树,我们显然没办法直接套这个做法。我们先考虑在区间上的做法,再考虑拓展到树上。
我们发现其实我们可以 \(\text{cdq}\) 分治解决这个问题,即每次先递归解决左区间,然后在左区间的凸壳上考虑对于右区间的贡献,然后继续递归解决右区间。可以发现这样做的时间复杂度为 \(\Theta(n\log^2 n)\) 的。
考虑拓展到树上。我们发现其实我们可以用淀粉质解决这个问题,每次我们找到当前子树的重心,假设设为 \(x\) ,我们先递归解决该子树除了 \(x\) 的子树的部分(下面设为 \(S_1\)),那么我们可以考虑 \(S_1\) 对 \(x\) 的子树(下面设为 \(S_2\))产生的贡献,同上文,然后继续递归解决 \(S_2\)。
考虑分析时间复杂度,可以想到每个点的均摊时间复杂度就是点分树上的深度乘上对于一个点更新操作的时间,即为 \(\Theta(\log^2n)\) ,所以总时间复杂度即为 \(\Theta(n\log^2 n)\) 。
有几个细节需要提醒一下,就是说找重心的时候要找最接近于当前子树的根的点,因为这样才能保证不会陷入死循环,具体为什么自己实现一下就可以明白了。另外一个就是这道题目要开 \(\text{long long}\),而且极大值不能赋小了。
\(\texttt{Code}\)
#include <bits/stdc++.h>
using namespace std;
#define INF 0x7f7f7f7f7f7f7f
#define Int register int
#define int long long
#define MAXN 200005
template <typename T> inline void read (T &t){t = 0;char c = getchar();int f = 1;while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}while (c >= '0' && c <= '9'){t = (t << 3) + (t << 1) + c - '0';c = getchar();} t *= f;}
template <typename T,typename ... Args> inline void read (T &t,Args&... args){read (t);read (args...);}
template <typename T> inline void write (T x){if (x < 0){x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}
int n,t,toop = 1,f[MAXN],p[MAXN],q[MAXN],l[MAXN],fa[MAXN],to[MAXN],wei[MAXN],nxt[MAXN],dis[MAXN],head[MAXN];
void Add_Edge (int u,int v,int w){to[++ toop] = v,wei[toop] = w,nxt[toop] = head[u],head[u] = toop;}
void getdis (int u){for (Int i = head[u];i;i = nxt[i]) dis[to[i]] = dis[u] + wei[i],getdis (to[i]);}
int top,sta[MAXN];double sl[MAXN];//储存每个点到下一个点的斜率
double Slope (int x,int y){return (f[y] - f[x]) * 1.0 / (dis[y] - dis[x]);}
void ins (int x){
while (top > 1 && sl[top - 1] <= Slope (sta[top],x)) -- top;
sta[++ top] = x,sl[top - 1] = Slope (sta[top - 1],x),sl[top] = -INF;
}
int query (double num){
int l = 1,r = top,ans = 0;
while (l <= r){
int mid = (l + r) >> 1;
if (sl[mid] <= num) ans = mid,r = mid - 1;
else l = mid + 1;
}
return sta[ans];
}
int root,mxsiz,siz[MAXN];bool vis[MAXN];//淀粉质需要的东西
void findroot (int u,int SZ){
siz[u] = 1;int mx = 0;
for (Int i = head[u];i;i = nxt[i]) if (!vis[to[i]]) findroot (to[i],SZ),siz[u] += siz[to[i]],mx = max (mx,siz[to[i]]);
mx = max (mx,SZ - siz[u]);
if (mx <= mxsiz) mxsiz = mx,root = u;
}
int sum,pot[MAXN];
void getpoint (int u){
pot[++ sum] = u;
for (Int i = head[u];i;i = nxt[i]) if (!vis[to[i]]) getpoint (to[i]);
}
bool cmp (int x,int y){return dis[x] - l[x] > dis[y] - l[y];}//按照可以到的祖先深度排序
void work (int now,int SZ){
if (SZ == 1) return ;
mxsiz = INF,findroot (now,SZ);int x = root;
for (Int i = head[x];i;i = nxt[i]) vis[to[i]] = 1,SZ -= siz[to[i]];
work (now,SZ),sum = 0;
for (Int i = head[x];i;i = nxt[i]) getpoint (to[i]);
sort (pot + 1,pot + sum + 1,cmp);int a = x;top = 0;
for (Int i = 1;i <= sum;++ i){
int u = pot[i];
while (a != fa[now] && dis[a] >= dis[u] - l[u]) ins (a),a = fa[a];
if (top){
int k = query (p[u]);
f[u] = min (f[u],f[k] + (dis[u] - dis[k]) * p[u] + q[u]);
}
}
for (Int i = head[x];i;i = nxt[i]) work (to[i],siz[to[i]]);
}
signed main(){
read (n,t);
for (Int i = 2,val;i <= n;++ i) read (fa[i],val,p[i],q[i],l[i]),Add_Edge (fa[i],i,val),f[i] = INF;
getdis (1),work (1,n);
for (Int i = 2;i <= n;++ i) write (f[i]),putchar ('\n');
return 0;
}