轉載自:http://hi.baidu.com/diannaochina/blog/item/48dfff33c3c36840ad4b5fb5.html
一、最近公用祖先(Least Common Ancestors)
對於有根樹T的兩個結點u、v,最近公用祖先LCA(T,u,v)表示一個結點x,滿足x是u、v的祖先且x的深度儘可能大。另一種理解方式是把T理解為一個無向無環圖,而LCA(T,u,v)即u到v的最短路上深度最小的點。
這裡給出一個LCA的例子:
例一
對於T=<V,E>
V={1,2,3,4,5}
E={(1,2),(1,3),(3,4),(3,5)}
則有:
LCA(T,5,2)=1
LCA(T,3,4)=3
LCA(T,4,5)=3
二、RMQ問題(Range Minimum Query)
RMQ問題是指:對於長度為n的數列A,回答若干詢問RMQ(A,i,j)(i,j<=n),返回數列A中
下標在[i,j]裡的最小值下標。這時一個RMQ問題的例子:
例二
對數列:5,8,1,3,6,4,9,5,7 有:
RMQ(2,4)=3
RMQ(6,9)=6
RMQ問題與LCA問題的關係緊密,可以相互轉換,相應的求解演算法也有異曲同工之妙。
下面給出LCA問題向RMQ問題的轉化方法。
對樹進行深度優先遍曆,每當“進入”或回溯到某個結點時,將這個結點的深度存入數組
E最後一位。同時記錄結點i在數組中第一次出現的位置(事實上就是進入結點i時記錄的
位置),記做R[i]。如果結點E[i]的深度記做D[i],易見,這時求LCA(T,u,v),就等價於求
E[RMQ(D,R[u],R[v])],(R[u]<R[v])。例如,對於第一節的例一,求解步驟如下:
數列E[i]為:1,2,1,3,4,3,5,3,1
R[i]為:1,2,4,5,7
D[i]為:0,1,0,1,2,1,2,1,0
於是有:
LCA(T,5,2) = E[RMQ(D,R[2],R[5])] = E[RMQ(D,2,7)] = E[3] = 1
LCA(T,3,4) = E[RMQ(D,R[3],R[4])] = E[RMQ(D,4,5)] = E[4] = 3
LCA(T,4,5) = E[RMQ(D,R[4],R[5])] = E[RMQ(D,5,7)] = E[6] = 3
易知,轉化後得到的數列長度為樹的結點數的兩倍加一,所以轉化後的RMQ問題與LCA
問題的規模同次。
LCA轉化為RMQ問題的實現:
int D[N], E[N], R[N];class RMQ {public: int Min[N][20]; void build(int n) { int i, j, m; for(i = 1; i <= n; ++i) Min[i][0] = i; m = int(log(double(n))/log(2.0)); FOR(j, 1, m) { FOR(i, 1, n - (1<<j) + 1) { if(D[Min[i][j-1]] < D[Min[i+(1<<(j-1))][j-1]]) { Min[i][j] = Min[i][j-1]; } else { Min[i][j] = Min[i+(1<<(j-1))][j-1]; } } } } int query(int s, int e) { int k = log(double(e - s + 1))/log(2.0); if(D[Min[s][k]] < D[Min[e-(1<<k) + 1][k]]) return Min[s][k]; else return Min[e-(1<<k) + 1][k]; }} Q;vector<int> g[N];int in[N], cnt;bool vis[N];void init(int n) { CL(in, 0); CL(D, 0); CL(E, 0); CL(R, 0); CL(vis, 0); cnt = 1; for(int i = 1; i <= n; ++i) { g[i].clear(); }}void dfs(int t, int dep) { if(!vis[t]) { R[t] = cnt; vis[t] = true; } D[cnt] = dep; E[cnt++] = t; for(int i = 0; i < int(g[t].size()); ++i) { dfs(g[t][i], dep + 1); D[cnt] = dep; E[cnt++] = t; }}int main() { //freopen("data.in", "r", stdin); int t, i, n, x, y; scanf("%d", &t); while(t--) { scanf("%d", &n); init(n); REP(i, n - 1) { scanf("%d%d", &x, &y); g[x].push_back(y); in[y]++; } FOR(i, 1, n) { if(in[i] == 0) { dfs(i, 0); break; } } cnt--; Q.build(cnt); scanf("%d%d", &x, &y); x = R[x], y = R[y]; if(x > y) swap(x, y); printf("%d\n", E[Q.query(x, y)]); } return 0;}
RMQ問題以及ST演算法
RMQ(Range Minimum/Maximum Query)問題是求區間最值問題。你當然可以寫個O(n)的(怎麼寫都可以吧=_=),但是萬一要詢問最值1000000遍,估計你就要掛了。這時候你可以放心地寫一個線段樹(前提是不寫錯)O(logn)的複雜度應該不會掛。但是,這裡有更牛的演算法,就是ST演算法,它可以做到O(nlogn)的預先處理,O(1)!!!地回答每個詢問。
來看一下ST演算法是怎麼實現的(以最大值為例):
首先是預先處理,用一個DP解決。設a[i]是要求區間最值的數列,f[i,j]表示從第i個數起連續2^j個數中的最大值。例如數列3 2 4 5 6 8 1 2 9 7 ,f[1,0]表示第1個數起,長度為2^0=1的最大值,其實就是3這個數。f[1,2]=5,f[1,3]=8,f[2,0]=2,f[2,1]=4……從這裡可以看出f[i,0]其實就等於a[i]。這樣,Dp的狀態、初值都已經有了,剩下的就是狀態轉移方程。我們把f[i,j]平均分成兩段(因為f[i,j]一定是偶數個數字),從i到i+2^(j-1)-1為一段,i+2^(j-1)到i+2^j-1為一段(長度都為2^(j-1))。用上例說明,當i=1,j=3時就是3,2,4,5 和 6,8,1,2這兩段。f[i,j]就是這兩段的最大值中的最大值。於是我們得到了動規方程F[i,j]=max(F[i,j-1],F[i+2^(j-i),j-1]).
接下來是得出最值,也許你想不到計算出f[i,j]有什麼用處,一般毛想想計算max還是要O(logn),甚至O(n)。但有一個很好的辦法,做到了O(1)。還是分開來。如在上例中我們要求區間[2,8]的最大值,就要把它分成[2,5]和[5,8]兩個區間,因為這兩個區間的最大值我們可以直接由f[2,2]和f[5,2]得到。擴充到一般情況,就是把區間[l,r]分成兩個長度為2^n的區間(保證有f[i,j]對應)。直接給出運算式:
k:=trunc(l(r-l+1)/ln(2));
ans:=max(F[l,k],F[r-2^k+1,k]);
我的RMQ模板 POJ 3264:
#include <iostream>#include <cstdio>#include <cstring>#include <cstdlib>#include <stack>#include <cmath>#include <algorithm>#define CL(arr, val) memset(arr, val, sizeof(arr))#define REP(i, n) for(i = 0; i < n; ++i)#define FOR(i, l, h) for(i = l; i <= h; ++i)#define FORD(i, h, l) for(i = h; i >= l; --i)#define L(x) x << 1#define R(x) x << 1 | 1#define MID(l, r) (l + r) >> 1typedef long long LL;using namespace std;const int N = 50010;int Max[N][20];int Min[N][20];int num[N], n;void init() { int i, j, m; FOR(i, 1, n) { Max[i][0] = num[i]; Min[i][0] = num[i]; } m = int(log(double(n))/log(2.0)); FOR(j, 1, m){ FOR(i, 1, n - (1<<j) + 1) { Max[i][j] = max(Max[i][j-1], Max[i+ (1<<(j-1))][j-1]); Min[i][j] = min(Min[i][j-1], Min[i+ (1<<(j-1))][j-1]); } }}int solve(int s, int e) { int k = log(double(e - s + 1))/log(2.0); int x = max(Max[s][k], Max[e - (1<<k) + 1][k]); int y = min(Min[s][k], Min[e - (1<<k) + 1][k]); return x - y;}int main() { //freopen("data.in", "r", stdin); int q, i, a, b; CL(Max, 0); CL(Min, 0); scanf("%d%d", &n, &q); FOR(i, 1, n) scanf("%d", num + i); init(); while(q--) { scanf("%d%d", &a, &b); printf("%d\n", solve(a, b)); } return 0;}