1. 程式人生 > 實用技巧 >題解 [NOI2014]購票

題解 [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\) 的答案,可以得到轉移式:

\[f_u=\min_{v} \{f_v+(\text{deep}_u-\text{deep}_v )\times p_u+q_u\},s.t. \ \ v\in fa_u\wedge \text{deep}_u-\text{deep}_v\le l_u \]

\[\Rightarrow f_u=q_u+\text{deep}_u\times p_u+\min_{v}\{f_v-\text{deep}_v\times p_u\} \]

然後我們就發現這個式子可以斜率優化了。假設對於點 \(u\) 存在點 \(j\) 比點 \(k\)

更優,可以得到:

\[f_j-\text{deep}_j\times p_i\le f_k-\text{deep}_k\times p_i \]

\[\Rightarrow p_i\ge \frac{f_j-f_k}{\text{deep}_j-\text{deep}_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;
}