1. 程式人生 > 程式設計 >PowerShell基本使用教程

PowerShell基本使用教程

\(LCA\)問題(倍增法)

前言

其實本身並沒有寫這篇部落格的打算,主要原因是看了很多的部落格,然後感覺寫那篇部落格的大佬寫的實在是太好了,自愧不如。

但,問題在於,我雖然已經完全理解了\(LCA\)倍增的真諦,但是在程式碼實現方面我還是沒有能夠達到自己寫的地步。

所以,個人感覺還是有必要寫一篇部落格的。

在此奉上大佬的部落格

版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。

\(LCA\)前置

鏈式前向星

對於幾乎所有的圖論題目而言,存圖幾乎是必備的一項操作,而鏈式前向星則是存圖的一種方式,由於其優秀的時空複雜度,使得鏈式前向星成為了對於圖論題目最常用的一種存圖方式。

在使用鏈式前向星的前提之下,我們通常使用\(DFS\),\(BFS\)來進行圖的遍歷。

因此,我們同樣可以藉助\(DFS\)來理解鏈式前向星。

\(DFS\)演算法的實現過程可以這樣理解:

1.以當前點作為起點,在所有與該起點連線的邊中隨便找一條,然後跳到這條邊的終點上。

2.再將當前跳到的點當作起點,重複1。

3.若跳到某一點後,沒有以這個點為起點的邊了,就原路返回到之前的起點上,找一條與這條不同的邊,再跳到它的終點上。

顯然,\(DFS\)標記的是著一條邊所指向的終點,以及一個點的出度。

好巧不巧,

鏈式前向星的結構中真他更好包括了這亮點,鏈式前向星的結構定義如下:

struct node{
	int to;
	int next;
}edge[maxn];

鏈式前向星是以邊為單位進行儲存。其中,成員to表示這條邊的終點,而next就比較重要了,表示本條邊的起點相同的前一條邊,在edge陣列中的下標,如果這條邊的起點是第一次出現的,則置為0。也就是說,鏈式前向星的next屬性,像連結串列一樣,將途中起點相同的邊連在了一起。就像下面這個圖。

那麼我們就可以得到一個edge陣列。

當我們想要得到一條邊的終點時,就呼叫edge[i].to,當我們想要知道這個起點連線的其他邊時,就可以呼叫edge[i].next。那麼現在的問題就是如何快速地求得next的屬性。

解決方法:

再定義一個數組head,head[i]表示最近一次輸入的以i為起點的邊在edge陣列中的下標。

我們來看程式碼:


#include<iostream>
using namespace std;
const int maxn=1000;
struct node
{
	int to;
	int next;
}edge[maxn];
int head[maxn];
int cnt=1;
void add(int from,int t)
{
	edge[cnt].to=t;
	edge[cnt].next=head[from];
	head[from]=cnt++;
}
bool s[maxn];
void dfs(int x)
{
    s[x]=true;
    printf("%d ",x);
    for(int i=head[x];i!=0;i=edge[i].next)
    {
    	if(!s[edge[i].to])
        	dfs(edge[i].to);
    }
}
 
int main()
{
	int u,v,w;
	int n;
	cin>>n;
	while(n--)
	{
		cin>>u>>v;
		add(u,v);
	}
	dfs(1);
	return 0;
}

\(ST\)演算法

\(ST\)演算法在更多的情況下其實應該應用與\(RMQ\)問題(區間最值問題)之中。

但是\(LCA\)倍增演算法同樣需要用到與\(ST\)演算法相似甚至幾乎相同的程式碼思路和程式碼構造,所以可以前置學習一下。

\(RMQ\)問題中,\(ST\)演算法就是倍增的產物。

給定一個長度為\(N\)的數列\(A\)

\(ST\)演算法能夠在\(O(nlogn)\)的時間複雜度下預處理,之後以\(O(1)\)的時間複雜度線上回答數列\(A\)中下標在\(l~r\)之間的最大值是多少。

\(F[i,j]\)表示數列\(A\)中下標在子區間\([i,i+2^j-1]\)裡的數的最大值,也就是從\(i\)開始的\(2^j\)個數的最大值。

遞推邊界是\(F[i,0]=A[i]\)

有公式:

\[F[i,j]=max(F[i,j-1],F[i+2^j,j-1]) \]

// 區間最值
void ST_prework() { // st演算法預處理
	for(int i = 1 ;i<=n ; i++ ) {
		f[i][0] = a[i] ; // 處理邊界 [i,i] 的最大值就是 a[i]
	}
	int t = log(n)/log(2) + 1 ; // 這裡是列舉右端點
	for(int j =1 ; j<t ; j++){
		for(int i = 1 ;i<=n-(1<<j)+1 ;i++) {
			f[i][j] = min(f[i][j-1] ,f[i+(1<<(j-1))][j-1]) ;
		}
	}
	
}

當詢問任意區間\([l,r]\)的最值時,我們先計算一個\(k\)使\(k\)滿足\(2^k<r-l+1\leq2^{k+1}\),也就是使2的\(k\)次冪小於區間長度的前提下的最大的\(k\).

那麼,從\(l\)開始的\(2^k\)個數和以\(r\)結尾的\(2^k\)個數這兩段一定覆蓋了整個區間\([l,r]\)的最大值。

這兩段的最大值分別是\(F[i,k]\)\(F[r-2^k+1,k]\),二者中較大的那個就是整個區間的最大值。

int ST_query(int l ,int r){ // 查詢 區間 [l,r] 之間的最值
	int k = log(r-l+1)/log(2);
	return max(f[l][k],f[r-(1<<k)+1][k]) ;
}

\(LCA\)本體

兩個關鍵理論

相信大家都做過這樣一道題,大概意思表達的是任何一個正整數都可以表示成兩個不同的2的次冪的加和。

如果\(c\)\(a\)\(b\)\(LCA\),那麼\(c\)的所有祖先同樣是\(a\)\(b\)的公共祖先,但不是最近的。

\(LCA\)中的\(ST\)(預處理)

\(ST\)演算法中,

我們維護了一個數組\(dp[i][j]\),表示的是以下標\(i\)為起點的長度為\(2^j\)的序列的資訊。

然後用動態規劃的思想求出了整個陣列。

而通過倍增求\(LCA\)要跳2的冪次方層。

這就與\(dp\)陣列的\(j\)下標的定義不謀而合。

所以我們定義倍增法中的\(dp[i][j]\)為:結點\(i\)的向上\(2^j\)層的祖先。


//fa表示每個點的父節點 
int fa[100],DP[100][20];
void init()
{
	//n為結點數,先初始化DP陣列 
	for(int i=1;i<=n;i++)
		dp[i][0]=fa[i];
	//動態規劃求出整個DP陣列 
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i<=n;i++)
			DP[i][j]=DP[DP[i][j-1]][j-1];
}

上述程式碼完成了整個函式的預處理部分,下面則是查詢函式。

查詢函式

這個函式的引數就是要查詢的兩個結點\(a\)\(b\)

在函式中我們應指定\(a\)是深度較大的那一個(\(b\)也可以),這樣方便操作。

然後讓\(b\)不斷向上回溯,知道跟\(a\)處於同一深度。

然後讓\(a\)\(b\)同時向上回溯,直到二者相遇。

這個過程不難理解:

對於第一次回溯,我們要做的是儘可能大得跳,以便於使兩個點到達相同的深度。

因為我們已經知道了兩個點的深度差。

而對於第二次回溯,我們就是隨便亂跳,如果大了,就一個一個得往回跳,知道找到\(LCA\)


//查詢函式
int LCA(int a,int b)
{
    //確保a的深度大於b,便於後面操作。
	if(dep[a]<dep[b])
		swap(a,b);
    //讓a不斷往上跳,直到與b處於同一深度
    //若不能確保a的深度大於b,則在這一步中就無法確定往上跳的是a還是b
	for(int i=19;i>=0;i--)
	{
        //往上跳就是深度減少的過程
		if(dep[a]-(1<<i)>=dep[b])
			a=dp[a][i];
	}
    //若二者處於同一深度後,正好相遇,則這個點就是LCA
	if(a==b)
		return a;
    //a和b同時往上跳,從大到小遍歷步長,遇到合適的就跳上去,不合適就減少步長
	for(int i=19;i>=0;i--)
	{
        //若二者沒相遇則跳上去
		if(dp[a][i]!=dp[b][i])
		{
			a=dp[a][i];
			b=dp[b][i];
		}
	}
    //最後a和b跳到了LCA的下一層,LCA就是a和b的父節點
	return dp[a][0];
}

以上就是倍增的主要思路。

\(LCA\)程式碼

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct zzz {
    int t, nex;
}e[500010 << 1]; int head[500010], tot;
void add(int x, int y) {
	e[++tot].t = y;
	e[tot].nex = head[x];
	head[x] = tot;
}
int depth[500001], fa[500001][22], lg[500001];
void dfs(int now, int fath) {
	fa[now][0] = fath; depth[now] = depth[fath] + 1;
	for(int i = 1; i <= lg[depth[now]]; ++i)
		fa[now][i] = fa[fa[now][i-1]][i-1];
	for(int i = head[now]; i; i = e[i].nex)
		if(e[i].t != fath) dfs(e[i].t, now);
}
int LCA(int x, int y) {
	if(depth[x] < depth[y]) swap(x, y);
	while(depth[x] > depth[y])
		x = fa[x][lg[depth[x]-depth[y]] - 1];
	if(x == y) return x;
	for(int k = lg[depth[x]] - 1; k >= 0; --k)
		if(fa[x][k] != fa[y][k])
			x = fa[x][k], y = fa[y][k];
	return fa[x][0];
}
int main() {
	int n, m, s; scanf("%d%d%d", &n, &m, &s);
	for(int i = 1; i <= n-1; ++i) {
		int x, y; scanf("%d%d", &x, &y);
		add(x, y); add(y, x);
	}
	for(int i = 1; i <= n; ++i)
		lg[i] = lg[i-1] + (1 << lg[i-1] == i);
	dfs(s, 0);
	for(int i = 1; i <= m; ++i) {
		int x, y; scanf("%d%d",&x, &y);
		printf("%d\n", LCA(x, y));
	}
	return 0;
}