天天看點

樹上倍增詳解 How far away ?HDU -2586

看了這麼久終于看懂學長給的樹上倍增代碼了 下面就來盤點一下那些難以了解的地方 先把學長的部落格也放出來https://blog.csdn.net/qq_41635132

前導知識是RMQ沒有學過的同學先去學一下哦

先說一下樹上倍增的基本原理 友善下面的閱讀

樹上倍增詳解 How far away ?HDU -2586

我們先想一下 如果找 節點 8 和節點 5 的最小公共祖先 是不是等于找 4 5的公共祖先 這裡4是8的祖先 且4 和 5在同一層

那麼為什麼放到同一層才行呢 看看我們的題目 樹上倍增 我們待會要開始倍增了 那麼怎麼個倍增方法呢 兩個同一層的節點 如果同時上跳2^n步是不是還在同一層呢? 答案是肯定的 那麼這有什麼用處呢

樹上倍增詳解 How far away ?HDU -2586

我們假設這是 4 和 5的情況 從圖中我們可以看到它們的lca 是 節點A

那隻要我們上跳的時候上跳到a 4 和 5就相遇了對吧。

那麼怎麼上跳呢下面就是倍增

我們每次上跳2^(i–)步 如果上跳之後兩個相遇 就證明我們跳過了對吧

那麼就排除掉 接着下一次的跳躍 如果上跳之後沒有相遇我們就移動 4 和5的位置 從圖中可以看到 最近一次跳躍應該是 2^1的時候 這時候我們再走一步就到了公共祖先 這時候就可以停止了(注意不是上跳到AA點 因為我們不能讓他們再同一點相遇 我們隻需讓他們保持他們兩個是A的兩個子節點就行 最後我們可以傳回 4上面一個節點(也就是A))

那麼為什麼我們不跳到A呢? 因為如過到兩點相遇就傳回的話我們的判斷語句應該是這樣的

if(4上跳x步數之後==5上跳x步數之後)
	return 4 和 5 相遇的地方
           

看出來了嗎 寫成代碼傳回條件就是4和5相遇就傳回了 那麼我們就不能确定到底傳回的節點是不是最小的公共祖先節點了

樹上倍增詳解 How far away ?HDU -2586

下面我們配着題來了解

下面這份代碼是A - How far away ? HDU - 2586的答案

Problem Description
There are n houses in the village and some bidirectional roads connecting
 them. Every day peole always like to ask like this "How far is it if I 
 want to go from house A to house B"? Usually it hard to answer.
 But luckily int this village the answer is always unique, since the roads
 are built in the way that there is a unique simple path("simple" means you
 can't visit a place twice) between every two houses. Yout task is to 
 answer all these curious people.
 

Input
First line is a single integer T(T<=10), indicating the number of test 
cases.
 For each test case,in the first line there are two numbers
 n(2<=n<=40000) and m (1<=m<=200),the number of houses and the number of 
 queries. The following n-1 lines each consisting three numbers i,j,k,
separated bu a single space, meaning that there is a road connecting house
 i and house j,with length k(0<k<=40000).The houses are labeled from 1 to n.
 Next m lines each has distinct integers i and j, you areato answer the
  distancebetween house i and house j.
 

Output
For each test case,output m lines. Each line represents the answer of the 
query. 
Output a bland line after each test case.
 

Sample Input
2
3 2
1 2 10
3 1 15
1 2
2 3

2 2
1 2 100
1 2
2 1
 

Sample Output
10
25
100
100
           

完整代碼 完整代碼下面會分解出來分析

#include<cstring>
#include<cstdio>
#include<iostream>
#pragma warning (disable:4996)
using namespace std;
const int maxn = 40010;
struct edge//學長本人喜歡把模闆寫到結構體裡面 就按着這個解釋了
{
	int to[maxn << 1], value[maxn << 1];
	int head[maxn], Next[maxn << 1];
	int cnt;
	void init() { memset(head, -1, sizeof head), cnt = 0;}
	void add(int f, int t, int v)
	{
		to[++cnt] = t;
		value[cnt] = v;
		Next[cnt] = head[f];
		head[f] = cnt;
	}
}e;
int n, m;
int dep[maxn], dis[maxn], dp[maxn][22];
void dfs(int rt) {
	dep[rt] = dep[dp[rt][0]] + 1;
	for (int i = 0; dp[rt][i]; i++)
		dp[rt][i + 1] = dp[dp[rt][i]][i];
	for (int i = e.head[rt]; ~i; i = e.Next[i]) {
		if (dep[e.to[i]])continue;
		dis[e.to[i]] = dis[rt] + e.value[i];
		dp[e.to[i]][0] = rt;
		dfs(e.to[i]);
	}
}
int lca(int a, int b) {
	if (dep[a] < dep[b]) swap(a, b);
	for (int i = 20; i >= 0; i--)
		if (dep[dp[a][i]] >= dep[b])
			a = dp[a][i];
	if (a == b) return a;
	for (int i = 20; i >= 0; i--)
		if (dp[a][i] != dp[b][i])
			a = dp[a][i], b = dp[b][i];
	return dp[a][0];
}
int main()
{
	int t;
	scanf("%d", &t);
	while (t--) {
		scanf("%d%d", &n, &m);
		e.init();
		for (int i = 0; i <=n; i++)
			dep[i] = 0;
		int a, b, v;
		for (int i = 1; i < n; i++) {
			scanf("%d%d%d", &a, &b, &v);
			e.add(a, b, v), e.add(b, a, v);
			//注意哦 因為是無向圖 是以我們得加兩次
		}
		dis[1] = 0, dfs(1);//為什麼是1 其他數其實也可以 
		//但是無論n是多少 1<=n 恒成立 是以選擇1當作根節點 
		//為什麼1就是根節點了呢 解釋再下面哦
		while (m--) {
			scanf("%d%d", &a, &b);
			printf("%d\n", dis[a] + dis[b] - 2 * dis[lca(a, b)]);
			//不再詳細解釋這裡了
		}
	}
	return 0;
}
           

分解時間

結構體解釋

//下面我們把 to[],value[],head[],Next[]這四個數組放到一起看 
to[i],value[i],head[i],Next[i]一起記錄了一組輸入給我們帶來的資訊
struct edge
{
	int to[maxn << 1], value[maxn << 1];
	int head[maxn], Next[maxn << 1];
	int cnt;
	void init() { memset(head, -1, sizeof head), cnt = 0;}
	void add(int f, int t, int v)
	{
		to[++cnt] = t;  //to數組就是記錄了f 能到達的一個地方
		value[cnt] = v;//value記錄了 f 和 t兩個節點之間的距離
		Next[cnt] = head[f];  
		
//先看head head[]剛開始都是0 
//然後 head[i]記錄的是上一組以和f有關的資料的出現的地方
//比如我第三組資料輸入的是 1 2 3 add(1,2,3)之後Next[3]=0,head[1]=3
//(其實不是三昂是2*(3-1)+1, 因為後面會再add(2,1,3),這是一個無向圖)
//代表上一次 出現過1這個節點的地方在第三組資料 這樣通過這種關系我們
//可以快速找到 1這個節點能到達的所有節點 dfs的時候會仔細說明

		head[f] = cnt;//更新一下head[f]的值 最近一次出現f的地方是
		      		  //第cnt組資料;
	}
           

小插曲

int dep[maxn], dis[maxn], dp[maxn][22];
    //dep[]用來記錄該節點的深度
   // dp[i][j]用來記錄 i節點往上走2^j步之後是哪個節點()
           

dfs環節到

void dfs(int rt) {
	dep[rt] = dep[dp[rt][0]] + 1;//注意以上越是上層的節點depth越小
	//rt的深度就等于rt一層節點的深度加一

			//注意判斷條件如果已經到了0這個
			//節點的話 就不用再往下比對了因為0已經是最原始的節點了(
			//實際上沒有把這個節點放到樹裡面這麼說知識為了友善了解)
	for (int i = 0; dp[rt][i]; i++)
		dp[rt][i + 1] = dp[dp[rt][i]][i];//這裡就是更新一下rt的2^j層之上是哪一個節點

//以rt為爸爸 把它所能到達的所有節點看錯它的子節點
	for (int i = e.head[rt]; ~i; i = e.Next[i]) {
		if (dep[e.to[i]])continue;
		//如果e.to[i]這個節點節點已經通路過了 那就繼續往下查找
		dis[e.to[i]] = dis[rt] + e.value[i];//更新一下值
		dp[e.to[i]][0] = rt;
		//rt是e.to[i]節點的爸爸 e.to[i]往上走2^0步就是它的爸爸 
		dfs(e.to[i]);//然後對e.to[i]找兒子
	}
}
           

可能存在的疑問 為什麼dfs()之後就能把所有節點涵蓋進去

我們想一下一棵樹

0
      1 2
    3 4 5 6
    是不是轉換成這樣
          1
        3 4 0
             2
             5  6 
  雖然我不想見到這麼醜的樹 但是我們不能剝奪它的樹籍
  根據這兩個圖 我們可以得出 dfs(rt) 傳入的這個rt 就被我們人為
  改變成根節點了 那麼往下也就能解釋為什麼會找到每一個節點了
 
           

靈魂所在 倍增之路開始了

//找a和b的公共爸爸
int lca(int a, int b) {
	if (dep[a] < dep[b]) swap(a, b);
	//這裡預設把a看作靠下面一層的節點 僅僅是為了友善 把b看成靠下的也行
	
	//這個for循環時找到a的祖先中與b的深度相同的一個
	for (int i = 20; i >= 0; i--)
		if (dep[dp[a][i]] >= dep[b])//注意判斷條件 >=
			a = dp[a][i];		//for搞完之後就是 a就是和b同一級了
//注意我們最終的傳回值僅僅是a(原本) 和 b的公共祖先 這裡a(現)
//已經是a的祖先了(也有可能是本身) 是以不用管a的變化會導緻邏輯錯誤
//為啥一定能傳回的是a的祖先呢 下面再仔細解釋 看懂的同學可以忽略了昂

	if (a == b) return a;//不解釋這裡了

//這個for循環就是找公共祖先的時候了
//先看函數傳回值是 dp[a][0] 是以最後dp[a][0]代表的就是他們公共祖先
//又因為是dp[a][0]是以 這時候 a 和 b都是dp[a][0]這個節點的"親兒子"	
//見下圖 原諒我是用ppt畫的圖。。。
	for (int i = 20; i >= 0; i--)
		if (dp[a][i] != dp[b][i])
			a = dp[a][i], b = dp[b][i];
	return dp[a][0];
}
           
關于為什麼返最終的a是a(原)的父親 且與b深度一樣的
我們先假設 a與b的深度差是9 
那麼我們可以跳 2^3 步 再跳2^0 次方步就可以了對吧
那如何證明别的深度差也可以跳到呢?
答案就是二進制
任何一個深度差都是一個整數 我們把它轉化成一個二進制之後 
先瞎寫一個數哈 
11010 是不是可以先跳2^4 步 然後2 ^3次方步 然後 2^1 步就行了
           
樹上倍增詳解 How far away ?HDU -2586

a往上走一步就是dp[a][0];

main函數就不再細說了 相信大家也都能看懂了