1. 程式人生 > 實用技巧 >JZOJ 4289.Mancity

JZOJ 4289.Mancity

\(Mancity\)

\(Description\)

\(Input\)

\(Output\)

\(Sample Input\)

8 3 6
1 2
1 1
3 2
4 2
5 1
6 1
6 2
4 1
3 3
2 4
4 2
2 5
8 2

\(Sample Output\)

1
0
2
2
3
4

\(Hint\)

\(Data Constraint\)

大致理一會題意,發現就是兩個點 \(u,v\) 每步向上跳 \(d\) 距離,每步不能落在邊上,在他們的 \(lca\) 處轉身,經過 \(lca\) 直到相遇,求最少步數
而有了這句話,我們就可以直接模擬 \(u,v\) 同時一步一步往上跳,跳到離 \(lca\)

最近的點(不超過 \(lca\)),再看這兩個點的距離,計算貢獻

然而太慢,我們發現一步一步向上跳太 \(naive\)
又想到倍增就是改一步一步向上跳為一次向上跳 \(2^i\)步,預處理向上跳 \(2^i\) 步是誰
那麼我們似乎可以同理維護一步跳距離為 \(d\),每步不能落在邊上,一次跳 \(2^i\) 步是誰
記為 \(top[u][i]\),表示 \(u\) 向上跳 \(2^i\)步落在哪個點,只用算 \(top[u][0]\) 就可以得到其它的 \(top\)了。
這樣忒慢的一步一步跳變成了 \(O(logn)\) 級別的

那麼怎麼算 \(top[u][0]\)
我們如果從 \(u\)

處向上一條邊一條邊地算,那麼時間是不可接受的
注意到,\(u\)\(top[u][0]\) 必然是他的祖先,並且 \(top[u][0]\) 必然是 \(top[fa[u]][0]\) 或往下
那麼我們就可以從 \(top[fa[u][0]]\) 處掃,不合法就往下
你向上跳的路徑是固定的(你只有一個father),可往下就不一定了
所以我們開一個棧 \(stack\)\(dfs\) 到當前點 \(u\),將它入棧,那麼你求 \(top[fa[u]][0]\) 往下跳時,必然是 \(top[fa[u]][0]\) 在棧中的編號加一(根據 \(dfs\) 遍歷的順序可得)
為了方便處理,我們讓 \(top[u][0]\)
先表示 \(top[u][0]\) 在棧中的編號
然後回溯時讓 \(top[u][0] = stack[top[u][0]]\),即變為具體的點
詳細細節見 \(Code1\)

\(Code1\)

倍增,\(nlogn\)\(80\)

#include<cstdio>
#include<iostream>
using namespace std;

const int N = 5e5 + 5;
int n , d , q , fa[N] , anc[N][25] , f[N][25];
int tot , dep[N] , h[N] , dis[N] , st[N] , top;

struct edge{
	int nxt , to;
}e[2 * N];

inline void add(int x , int y)
{
	e[++tot].to = y;
	e[tot].nxt = h[x];
	h[x] = tot;
}

inline void dfs(int x)
{
	st[++top] = x;
	anc[x][0] = max(1 , anc[fa[x]][0]); //最近跳到根1,anc即上文提及的top,先存棧中的編號
	while (dis[x] - dis[st[anc[x][0]]] > d) anc[x][0]++; //跳不了那麼遠,換棧中下一個
	
	for(register int i = 1; i <= 21; i++)
	if (f[x][i - 1]) f[x][i] = f[f[x][i - 1]][i - 1];
	else break;
	
	for(register int i = h[x]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		dep[v] = dep[x] + 1 , f[v][0] = x;
		dfs(v);
	}
	
	anc[x][0] = st[anc[x][0]]; //回溯變具體的點
	--top;
}

inline int LCA(int x , int y)
{
	if (dep[x] < dep[y]) swap(x , y);
	int deep = dep[x] - dep[y];
	for(register int i = 0; i <= 21; i++)
	if (deep & (1 << i)) x = f[x][i];
	if (x == y) return x;
	for(register int i = 21; i >= 0; i--)
		if (f[x][i] != f[y][i]) x = f[x][i] , y = f[y][i];
	return f[x][0];
}

inline int getans(int x , int y)
{
        int lca = LCA(x , y) , res = 0;
        //根據求出來的anc,讓u,v同時往上跳
	for(register int i = 21; i >= 0; i--) 
		if (dis[anc[x][i]] > dis[lca]) x = anc[x][i] , res = res + (1 << i);
	for(register int i = 21; i >= 0; i--)
		if (dis[anc[y][i]] > dis[lca]) y = anc[y][i] , res = res + (1 << i);
	if (x == y) return res;
        //處理轉彎處需要的步數
	if (dis[x] + dis[y] - 2 * dis[lca] <= d) return res + 1;
	else return res + 2;
}

int main()
{
	freopen("mancity.in" , "r" , stdin);
	freopen("mancity.out" , "w" , stdout);
	scanf("%d%d%d" , &n , &d , &q);
	for(register int i = 2; i <= n; i++)
	{
		scanf("%d%d" , &fa[i] , &dis[i]);
		dis[i] += dis[fa[i]];
		add(fa[i] , i);
	}
	dfs(1);
	
	for(register int i = 1; i <= n; i++) 
		for(register int j = 1; j <= 21; j++)
			if (anc[i][j - 1]) anc[i][j] = anc[anc[i][j - 1]][j - 1];
			else break;
	int x , y;
	for(register int i = 1; i <= q; i++)
	{
		scanf("%d%d" , &x , &y);
		printf("%d\n" , getans(x , y));
	}
}

正如你所見 \(O(nlogn)\) 竟然過不了
出題人毒瘤卡 \(O(nlogn)\) !
那我還要更快?!
可求 \(lca\),樹剖和倍增都是 \(nlogn\) 的呀???
咦,題目似乎可以離線!
嗯,那就上 \(Tarjan\)\(lca\) 吧!
\(Tarjan\)\(lca\) 大致思想是將詢問掛在節點上,遍歷到它的時候就去處理詢問
按照 \(dfs\) 遍歷的順序,我們在遍歷完一個節點將它合併的它的祖先節點
如果當前點為 \(u\) ,它的詢問物件 \(v\) 已經被訪問過,那麼他們的最近公共祖先就是 \(find(v)\)
當然,不明白的話就去查查百度

注意,如果 \(u,v\) 是祖孫關係,那麼處理詢問時它們兩個都會算
所以儲存時要開兩倍陣列

不過,上面的 \(Tarjan\)\(lca\) 快很多了,可計算答案怎麼搞?
重新記 \(top[u]\) 表示原先的 \(top[u][0]\)
其實,我們注意到 \(u\) 所對應的 \(top[u]\) 是唯一的,它們不會形成環
於是所有的 \(top\) 關係又組成了一棵樹
那麼求 \(u,v\) 最接近公共祖先的兩個節點時也可以同 \(Tarjan\) 一樣使用並查集
我們在遍歷完一個節點將它合併的 \(top\) 節點
將詢問掛在 \(lca\)
因為正在處理 \(lca\) ,所以查詢 \(u,v\) 兩點各自的 \(top\) 樹的祖先絕對不會超過 \(lca\)\(lca\) 還沒合併到它的 \(top\)
所以我們直接 \(find(u),find(v)\),就得到了離 \(lca\) 最近的點。當然,求步數時給並查集加個權值就好了
然後轉彎時上文同理
詳見 \(Code2\)

\(Code2\)

\(Tarjan\) , 並查集, \(O(n \alpha(n))\)\(100\)

#include<cstdio>
#include<iostream>
using namespace std;

const int N = 5e5 + 5;
int n , d , q , top[N] , stop , stack[N];
int tot1 , tot2 , tot3 , tot4 , h1[N] , h2[N] , h3[N] , h4[N];
int dis[N] , fa[N] , vis[N] , ask[N][3] , ans[N] , dist[N];

struct edge{
	int nxt , to;
}e1[N] , e2[N] , e4[2 * N];

struct edgeask{
	int nxt , to , id;
}e3[2 * N];

inline void add1(int x , int y) //原來的樹
{
	e1[++tot1].to = y;
	e1[tot1].nxt = h1[x];
	h1[x] = tot1;
}

inline void add2(int x , int y) //Top關係樹
{
	e2[++tot2].to = y;
	e2[tot2].nxt = h2[x];
	h2[x] = tot2;
}

inline void add3(int x , int y , int z) //ask,Tarjan時用來求一個點和其詢問物件的 $lca$
{
	e3[++tot3].to = y;
	e3[tot3].id = z;
	e3[tot3].nxt = h3[x];
	h3[x] = tot3;
}

inline void add4(int x , int y) //lca處處理詢問,即在詢問掛在lca處,遍歷到時再處理
{
	e4[++tot4].to = y;
	e4[tot4].nxt = h4[x];
	h4[x] = tot4;
}

inline int find1(int x) //tarjan時所用的並查集的find
{
	if (fa[x] == x) return x;
	fa[x] = find1(fa[x]);
	return fa[x];
}

inline int find2(int x) //top關係樹所用的並查集的find
{
	if (top[x] == x) return x;
	int t = top[x];
	top[x] = find2(top[x]);
	dist[x] += dist[t]; //路壓合併時,加上它father的權值
	return top[x];
}

inline void dfs1(int u)
{
        //同理計算top
	stack[++stop] = u;
	top[u] = max(1 , top[fa[u]]);
	while (dis[u] - dis[stack[top[u]]] > d) top[u]++;
	
	for(register int i = h1[u]; i; i = e1[i].nxt)
		dfs1(e1[i].to);
		
	top[u] = stack[top[u]]; //更換為具體點
	--stop; 
	add2(top[u] , u);
}

inline void dfs2(int u)
{
	top[u] = u , fa[u] = u , vis[u] = 1;
	for(register int i = h1[u]; i; i = e1[i].nxt)
	{
		dfs2(e1[i].to);
		fa[e1[i].to] = u;
	}
	
	for(register int i = h3[u]; i; i = e3[i].nxt) //求lca
	{
		int v = e3[i].to;
		if (!vis[v]) continue;
		add4(find1(v) , e3[i].id);
	}
	
	for(register int i = h4[u]; i; i = e4[i].nxt) //lca處處理詢問
	{
		int id = e4[i].to , x = ask[id][0] , y = ask[id][1];
		int xx = x , yy = y;
		x = find2(x) , y = find2(y); //求兩個淺點
		int m = dis[x] + dis[y] - dis[u] * 2;
		ans[id] = dist[xx] + dist[yy] + (m > 0) + (m > d);
	}
	
	for(register int i = h2[u]; i; i = e2[i].nxt) top[e2[i].to] = u , dist[e2[i].to] = 1; //維護top
}

int main()
{
	freopen("mancity.in" , "r" , stdin);
	freopen("mancity.out" , "w" , stdout);
	scanf("%d%d%d" , &n , &d , &q);
	for(register int i = 2; i <= n; i++) 
	{
		scanf("%d%d" , &fa[i] , &dis[i]);
		dis[i] += dis[fa[i]];
		add1(fa[i] , i);
	}
	dfs1(1);
	
	for(register int i = 1; i <= q; i++) 
	{
		scanf("%d%d" , &ask[i][0] , &ask[i][1]);
		add3(ask[i][0] , ask[i][1] , i) , add3(ask[i][1] , ask[i][0] , i); //將詢問掛上去
	}
	dfs2(1);
	for(register int i = 1; i <= q; i++) printf("%d\n" , ans[i]);
}