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;
}