圖論基礎
圖的種類
圖由頂點和邊組成,分為有向圖和無向圖。
通常點的符號為u和v,邊用符號e表示,連接u和v兩點的邊記為e=(u,v)。點的集合用V表示,邊的集合用E表示,以V和E表示的圖記為G=(V,E)。
無向圖基礎術語
兩點相鄰:兩個頂點之間有邊連接,稱這兩點相鄰。
路徑:相鄰頂點的序列
環(圈):起點與終點重合的路徑
連通圖:任意兩點有路徑連接的圖
度:連接點的邊的數量
樹(Tree):沒有環的連通圖,樹的邊數一定為樹的點數-1,即E=V-1
森林:沒有環的非連通圖
有向圖基礎術語
入度:指向這個點的邊的數量
出度:由這個點指向其他點的邊的數量
DAG:有向無環圖
圖的存儲
鄰接矩陣:用一個二維數組edge[i][j]存圖,初始化為-1,非-1表示從點i到點j有權值為edge[i][j]的邊。
領接表:用vector數組edge[i]存圖,表示的是從i點指向的相鄰的點
前向星:與領接表沒有本質的區別,但是速度更快。
struct Star{
int next, to;
}edge[maxe];//邊集合
int head[maxn];//每個點的邊集合開始的編號
memset(head, -1, sizeof head);//初始化
for(int i = head[u];i != -1;i = edge[i].next);//遍歷容器
int cnt = 1;
void(int u,int v){
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}//加邊
head[i]數組存儲的是由i點指向相鄰點的邊在edge的編號(即edge[head[i]]),next存的是下一條邊,to是連接的點,類比鏈表。
圖的搜索
dfs和bfs,不懂的用百度搜索吧。
並查集
[並查集入門],講的肥腸生動,強烈推薦
[對並查集和帶權並查集的深入理解],講的肥腸詳細,強烈推薦。
感覺帶權並查集就是每個點都帶有屬性,在進行合並操作的時候對屬性進行判斷就可以,和不帶權沒有太大的區別。
例題:POJ-1182 食物鏈
題意:中文,點鏈接看題
思路:重點記錄一下如何用向量思維做這題。
建立一個結構體,包含這個點的父親的信息和它與父親的關系,設0為同類,1為被父親吃,2為吃父親,以此為權建立帶權並查集。那么並查集路徑壓縮和合並時的關系轉換可以用以下公式解決:
路徑壓縮:兒子節點與根節點的關系 = (兒子節點與父親的關系+父親節點與根的關系)%3
集合合並:y根節點與x根節點的關系 = (y根節點與y節點的關系 + y節點與x節點的關系 + x節點與x根節點的關系)%3
關系判斷:x與y節點的關系 = (x節點與xy根節點的關系 + xy根節點與y節點的關系)%3
上面的公式是不是超級像向量相加的轉移公式?什么你說為什么還有父親與兒子的關系,先回答我是不是超級像向量相加的轉移公式?像就好啦加個負號不就換過來了嗎,這就是轉態轉移的向量思維。
#include<iostream>
#include<cstdio>
#include<stack>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<string>
#include<string.h>
#include<queue>
#include<functional>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define sz(a) (int)a.size()
#define de(a) cout<<#a<<" = "<<a<<endl
#define dd(a) cout<<#a<<" = "<<a<<" "
#define be begin
#define en end
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
const int maxn = 50000;
struct TREE{
int father;
int relation;//0->同類,1->被吃,2->吃
}tree[maxn+5];
int find(int rt){
if(rt == tree[rt].father) return rt;
int trt = find(tree[rt].father);
tree[rt].relation = (tree[rt].relation+tree[tree[rt].father].relation)%3;
tree[rt].father = trt;
return trt;
}
void unite(int re,int x,int y,int dx,int dy){
tree[dy].father = dx;
tree[dy].relation = (tree[x].relation + re +(3-tree[y].relation))%3;
}
int main()
{
//std::ios::sync_with_stdio(false);
//std::cin.tie(0);
int n,k;
scanf("%d%d", &n, &k);
for(int i = 1;i <= n;i++){
tree[i].father = i, tree[i].relation = 0;
}
int sum = 0;
while(k--){
int d,x,y;
scanf("%d%d%d", &d, &x, &y);
if(x > n || y > n){
sum++;
continue;
}
if(d == 1){
int dx = find(x),dy = find(y);
if(dx != dy)
unite(d - 1,x,y,dx,dy);
else if(tree[x].relation != tree[y].relation)
sum++;
}
if(d == 2){
if(x == y){
sum++;
continue;
}
int dx = find(x),dy = find(y);
if(dx != dy)
unite(d - 1,x,y,dx,dy);
else if(((3-tree[x].relation + tree[y].relation)%3) != 1)
sum++;
}
}
printf("%d\n", sum);
return 0;
}
最短路算法
[對最短路算法的深入理解] 推薦一下學姐的博客
Bellman-Ford算法
對所有點進行松弛,只要有一次松弛成功那么這個點就有可能松弛別的點,因此就對每個點進行不斷的松弛知道整張圖不再更新為止,算法復雜度為O(nm)(點數與邊數的乘積)。
如果圖中不存在負環,那么Bellman-Ford不會經過一個點兩次,即最多通過m-1條邊,反之則存在負環。因此Bellman-Ford可以用來判斷負環。
SPFA算法(隊列優化的Bellman-Ford算法)
Bellman-Ford算法對於每次更新都要重新遍歷所有的點,但是很顯然的是只要有更新就只有這個點相鄰的點可能會發生變化。所以對於每次更新,只要把成功更新的點放入隊列,進行松弛時取出,直到隊列為空時意味着整張圖不可能繼續松弛。算法復雜度為O(km)(k為一個1-4的無法嚴格證明的數),最壞情況可退化為O(nm)。
for(int i = 1; i <= n; i++) book[i] = 0; //初始時都不在隊列中
queue<int> que;
que.push(1); //將結點1加入隊列
book[1] = 1; //並打標記
while(!que.empty())
{
int cur = que.empty(); //取出隊首
que.pop(); //隊首出隊
for(int i = 1; i <= n; i++)
{
if(e[cur][i] != INF && dis[i] > dis[cur]+e[cur][i]) //若cur到i有邊且能夠松弛
{
dis[i] = dis[cur]+e[cur][i]; //更新dis[i]
if(book[i] == 0) //若i不在隊列中則加入隊列
{
que.push(i);
book[i] = 1;
}
}
}
book[cur] = 0;
}
Dijkstra算法
如果圖中不存在負邊,那么可以對Bellman-Ford算法進行優化:
1)找到一個最短距離已經確定的點,從它出發開始松弛相鄰的點的最短距離
2)之后1)中的最短距離的點就可以忽略了(因為它已經不再可能更新)
怎么得到最短距離確定的點是關鍵,一般的Dijk的做法是從從未使用過的頂點中尋找最短距離d[i]最小的點,由於不存在負邊,那么d[i]在之后的更新中一定不會變小,即這個點一定是最短距離確定的點。以上做法可以利用堆來優化處理,把所有更新過的點存入以最短距離排序的小根堆,那么會有以下性質:
如果有點成功松弛,那么這個點可能是最短距離確定的點,則塞入隊列。
如果這個點在隊列里的最短距離大於當前的最短距離,則丟棄這個點。
由於不存在負邊以及優先隊列的特性,堆頂元素一定是到達這個點的最短距離已經確定的點。
void Dijk(){
d[1] = 0;
priority_queue<pair<int,int>, vector<pair<int,int> >,greater<pair<int,int> > > q;//小根堆,按照greater的定義按第一個鍵值排序
q.push(make_pair(0, 1));
while(q.size()){
pair<int,int> p = q.top();
q.pop();
int v = p.second;
if(d[v] < p.first) continue;
vector<A>::iterator it;//用鄰接表存的圖
for(it = edge[v].begin();it != edge[v].end();it++){
if(l[(*it).to] < k || l[(*it).to] > k+m) continue;
if(d[(*it).to] > d[v]+(*it).cost){
d[(*it).to] = d[v] + (*it).cost;
q.push(make_pair(d[(*it).to], (*it).to));
}
}
}
算法復雜度為O(mlogn)
例題:POJ-1062 昂貴的聘禮
題意:中文題,點鏈接
題解:Dijk找最短路,但是要注意等級限制:設酋長等級為L,等級限制為M,你交易的對象的等級范圍只能在[L-M,L+M]中選擇交易對象。枚舉等級范圍內的等級中心跑Dijk求最短路。(話說集訓的時候居然在數學專題里做到了這題圖論題,差點沒把我嚇死)
#include<iostream>
#include<vector>
#include<cstdio>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<string>
#include<string.h>
#include<queue>
#include<functional>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define sz(a) (int)a.size()
#define de(a) cout<<#a<<" = "<<a<<endl
#define dd(a) cout<<#a<<" = "<<a<<" "
#define be begin
#define en end
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
const int inf = 0x3f3f3f3f;
struct A {
int to, cost;
A(int a, int b) :to(a), cost(b) { }
};
int d[105];
int l[105], v[105];
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
int m, n;
while (cin >> m >> n) {
vector<A> edge[105];
for (int i = 1;i <= n;i++) {
int x;
cin >> v[i] >> l[i] >> x;
rep(j, 0, x) {
int a, b;
cin >> a >> b;
edge[i].pb(A(a, b));
}
}
int mi = inf;
for (int k = l[1] - m;k <= l[1];k++) {
memset(d, inf, sizeof(d));
d[1] = 0;
priority_queue<pii, vector<pii>,greater<pii> > q;
q.push(mp(0, 1));
while(q.size()){
pii p = q.top();
q.pop();
int v = p.se;
if(d[v] < p.fi) continue;
vector<A>::iterator it;
for(it = edge[v].be();it != edge[v].en();it++){
if(l[(*it).to] < k || l[(*it).to] > k+m) continue;
if(d[(*it).to] > d[v]+(*it).cost){
d[(*it).to] = d[v] + (*it).cost;
q.push(mp(d[(*it).to], (*it).to));
}
}
}
for (int i = 1;i <= n;i++) {
mi = min(mi, d[i] + v[i]);
}
}
cout << mi << endl;
}
return 0;
}
Floyd-Warshall算法(兩點最短路)
我們先定義一個數組d[i][j],表示從i到j最短的路徑,那么可以用dp的思路去解決兩點之間的最短路問題:
d[i][j]表示的是從i到j的最短路徑
假設k點是ij某一條可松弛的路徑經過的點
d[i][j] = min(d[i][j], d[i][k]+d[k][j])
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(d[i][j] > d[i][k]+d[k][j])
d[i][j] = d[i][k]+d[k][j];
這就是Floyd-Warshall算法,時間復雜度O(n^3)
二分圖匹配
基本術語
假設有X集合和Y集合,兩個集合的某些點相鄰而集合內的點不相鄰形成的圖稱為二分圖。將兩兩不含公共端點的二分圖邊集合稱為二分圖匹配,元素最多的集合稱為最大匹配。特別的,當每個點都成功匹配,即2*匹配數=點數,則稱為完美匹配
交替路:匹配邊和非匹配邊交替出現的路徑
增廣路:連接兩個未匹配點的交替路
匈牙利算法
匈牙利算法可以看做是一個尋找增廣路的算法,每當找到一條增廣路意味着匹配可以優化,使得匹配數更大。時間復雜度O(n^2)原理比較好懂,參考上面鏈接。
二分圖的一些性質和結論
最小點覆蓋
可以連接所有邊的點集合稱為點覆蓋,用最少的點連接所有的邊稱為最小點覆蓋。
結論:最小點覆蓋 = 最大匹配數
證明:由於匈牙利算法實質上是尋找增廣路,而最大匹配數意味着這張匹配圖里已經不存在增廣路了(否則就不是最大匹配數了啊),也就是說圖中已經不存在兩個未匹配的點連接形成的路徑了,也就說明所有的邊都有點連接,即證明最大匹配數為最小點覆蓋。
例題:POJ-3041 Asteroids
題意:消滅星星,一次可以消滅一行或一列,求最少的子彈數
題解:把行數作為x集合,把列數作為y集合,如果一個點上有星星就把對應行列編號連線,消滅星星的最少次數就是最小點覆蓋。
#include<iostream>
#include<stack>
#include<cstdio>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<string>
#include<string.h>
#include<queue>
#include<functional>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define sz(a) (int)a.size()
#define de(a) cout<<#a<<" = "<<a<<endl
#define dd(a) cout<<#a<<" = "<<a<<" "
#define be begin
#define en end
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
const int maxk = 10000;
const int maxn = 500;
struct Star{
int next, to;
}edge[maxk+5];
int head[maxn+5],flag[maxn+5],metch[maxn+5];
bool find(int u){
for(int i = head[u];i != -1;i = edge[i].next){
int to = edge[i].to;
if(!flag[to]){
flag[to] = true;
if(metch[to] == 0 || find(metch[to])){
metch[to] = u;
return true;
}
}
}
return false;
}
int hungary(int n){
int sum = 0;
for(int i = 1;i <= n;i++){
if(find(i))
sum++;
memset(flag, 0, sizeof flag);
}
return sum;
}
int main()
{
//std::ios::sync_with_stdio(false);
//std::cin.tie(0);
int n,k;
scanf("%d%d", &n, &k);
memset(head, -1, sizeof head);
for(int i = 1;i <= k;i++){
int x,y;
scanf("%d%d", &x, &y);
edge[i].to = y;
edge[i].next = head[x];
head[x] = i;
}
int sum = hungary(n);
printf("%d\n", sum);
return 0;
}
例題:POJ-2226 Muddy Fields
題意:可以用任意長的木板覆蓋泥地,只能橫着或豎着鋪,可以重疊,問最少木板數
題解:假設只用橫木板鋪,記錄木板編號(連續泥地算一塊編號)加入X集合;假設只用豎木板鋪,同理記錄木板編號加入Y集合,每個點對應的橫木板編號與豎木板編號連線,最小點覆蓋就是所求。
#include<cstdio>
#include<iostream>
#include<stack>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<string>
#include<string.h>
#include<queue>
#include<functional>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define sz(a) (int)a.size()
#define de(a) cout<<#a<<" = "<<a<<endl
#define dd(a) cout<<#a<<" = "<<a<<" "
#define be begin
#define en end
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
const int maxn = 500;
struct Star{
int next, to;
}edge[10000];
int head[10000];
char maps[maxn+5][maxn+5];
int mapx[maxn+5][maxn+5],mapy[maxn+5][maxn+5];
bool flag[10000];
int match[10000];
bool find(int u){
for(int i = head[u];i != -1;i = edge[i].next){
int to = edge[i].to;
if(!flag[to]){
flag[to] = true;
if(match[to] == 0 || find(match[to])){
match[to] = u;
return true;
}
}
}
return false;
}
int hungary(int n){
int sum = 0;
for(int i = 1;i <= n;i++){
if(find(i))
sum++;
memset(flag, 0 ,sizeof flag);
}
return sum;
}
int main()
{
//std::ios::sync_with_stdio(false);
//std::cin.tie(0);
int r,c;
scanf("%d%d", &r, &c);
for(int i = 0;i < r;i++)
scanf("%s", maps[i]);
int cnt = 0;
bool f = false;
int n = 0;
for(int i = 0;i < r;i++){
for(int j = 0;j < c;j++){
if(f && maps[i][j] == '.')f = false;
if(maps[i][j] == '*'){
if(f == false)++cnt;
f = true;
mapx[i][j] = cnt;
n = cnt;
}
}
f = false;
}
cnt = 1;
for(int j = 0;j < c;j++){
for(int i = 0;i < r;i++){
if(f && maps[i][j] == '.') f = false;
if(maps[i][j] == '*'){
if(f == false)++cnt;
f = true;
mapy[i][j] = cnt;
}
}
f = false;
}
cnt = 1;
memset(head, -1, sizeof head);
for(int i = 0;i < r;i++)
for(int j = 0;j < c;j++){
if(maps[i][j] == '*'){
edge[cnt].to = mapy[i][j];
edge[cnt].next = head[mapx[i][j]];
head[mapx[i][j]] = cnt++;
}
}
printf("%d\n", hungary(n));
return 0;
}
最小邊覆蓋
可以把所有點連接起來的邊集合稱為邊覆蓋,用最少的邊連接所有的點稱為最小邊覆蓋。
結論:最小邊覆蓋 = 頂點數 - 最大匹配數
證明:記點數為n,最大匹配數為m,除去得到匹配的點后剩余的點數為a。則2m+a=n,最小邊覆蓋=m+a。故n-m=最小邊覆蓋。
DAG最小路徑覆蓋
最小路徑覆蓋分為兩個,基本思路是將所有的點加入二分圖的X、Y集合
不可相交最小路徑覆蓋
假設A->B,則X集合的A元素連接Y集合的B元素,連線完后即可求最大匹配數,然后套結論:
結論:不可相交最小路徑覆蓋 = 原圖的頂點數(即二分圖頂點數/2) - 最大匹配數
證明:一開始每個點都是獨立的為一條路徑,總共有n條不相交路徑。我們每次在二分圖里找一條匹配邊就相當於把兩條路徑合成了一條路徑,也就相當於路徑數減少了1。所以找到了幾條匹配邊,路徑數就減少了多少。所以有最小路徑覆蓋=原圖的結點數-新圖的最大匹配數。
可相交最小路徑覆蓋
假設A->C->B,直接將X集合的A元素與C、B元素連接,C也連接B,然后轉化為不可相交路徑覆蓋問題。思路簡單,證明就是A可以到B,不管有沒有相交。
例題:POJ-1422 Air Raid
題意:在一張DAG中投放傘兵,確保每個傘兵不相交的經過所有點,問最少的傘兵數量
題解:最小路徑覆蓋板子題
#include<iostream>
#include<cstdio>
#include<stack>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<string>
#include<string.h>
#include<queue>
#include<functional>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define sz(a) (int)a.size()
#define de(a) cout<<#a<<" = "<<a<<endl
#define dd(a) cout<<#a<<" = "<<a<<" "
#define be begin
#define en end
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
struct Star{
int next, to;
}edge[100000];
int head[200],match[200];
bool flag[200];
bool find(int u){
for(int i = head[u];i != -1;i = edge[i].next){
int to = edge[i].to;
if(!flag[to]){
flag[to] = true;
if(match[to] == 0 || find(match[to])){
match[to] = u;
return true;
}
}
}
return false;
}
int hungary(int n){
int sum = 0;
memset(match, 0, sizeof match);
for(int i = 1;i <= n;i++){
if(find(i))
sum++;
memset(flag, 0, sizeof flag);
}
return sum;
}
int main()
{
//std::ios::sync_with_stdio(false);
//std::cin.tie(0);
int T;
scanf("%d", &T);
while(T--){
int n,m;
scanf("%d%d", &n, &m);
memset(head, -1, sizeof head);
for(int i = 1;i <= m;i++){
int x,y;
scanf("%d%d", &x, &y);
edge[i].to = y;
edge[i].next = head[x];
head[x] = i;
}
printf("%d\n", n - hungary(n));
}
return 0;
}
最大獨立集
兩兩不相鄰的點集合稱為獨立集,最大的獨立集稱為最大獨立集。
結論:最大獨立集 = 頂點數 - 最大匹配數
證明:沒有匹配成功的點肯定兩兩不相交啊,相交了不就匹配成功了。
LCA(最近公共祖先)
給定節點u、v,求距離u、v最近的公共節點即為LCA問題。求解LCA問題有三種算法:倍增LCA、ST表LCA、Tarjan_LCA(離線算法),倍增LCA時間復雜度為\(O(nlogn+qlogn)\),感覺沒有另外兩個在時間上的優勢,實現和理解又有點復雜,暫時還沒了解。下面記錄一下另外兩個算法。
ST表LCA
先對樹從根節點進行前序遍歷,記錄dfs節點時經過的節點順序,但是我們這里和dfs序有所不同,當節點回溯回父親節點時,父親節點還要被記錄一次,我們稱這種順序為歐拉序。歐拉序可以在dfs的時候確定。
比如上圖節點的歐拉序為
8, 5, 9, 5, 8, 4, 6, 15, 6, 7, 6, 4, 10, 11, 10, 16, 3, 16, 12, 16, 10, 2, 10, 4, 8, 1, 14, 1, 13, 1, 8
但是這樣排序不利於我們下一步的維護,我們再稍微改一下,按照第一次遍歷到的編號對節點重新編號,比如上圖重新編號后的歐拉序為
1, 2, 3, 2, 1, 4, 5, 6, 5, 7, 5, 4, 8, 9, 8, 10, 11, 10, 12, 10, 8, 13, 8, 4, 1, 14, 15, 14, 16, 14, 1
在dfs的同時,記錄一下每個節點在歐拉序中出現的下標位置limit[i],記錄結束以后就可以來看看歐拉序的神通了:
假設詢問節點12和11的LCA,我們在歐拉序中找到對應節點在這張歐拉序中出現的第一個位置limit[12]、limit[11],即19和14,在重新編號的新表中的對應區間[14,19]之間的最小值為8,8這個編號對應的節點是10,所以結論就是節點10是他們的LCA。
原理很好理解,根據dfs的特性:
編號從根節點開始,最早遍歷到的節點編號一定小於后遍歷到的,即子節點的編號一定比父節點的編號大
在查找右子樹之前,一定已經查完左子樹並且回溯至LCA開始查右子樹
dfs沒有結束不會去遍歷父節點子樹以外的節點
根據這兩個特性結合歐拉序的定義就不難理解:
歐拉序里第一次出現兩個節點的位置一定是第一次遍歷到這個點時被記錄的;
第二個節點第一次被記錄時一定會回溯經過LCA;
LCA的編號一定是比它的子樹中的任何節點的編號小的;
dfs的第三特性保證區間內不會出現比LCA小的數。
因此歐拉序區間內最小的編號對應的點一定是LCA。
ST表的作用就是維護歐拉序的區間最小值,預處理時間復雜度\(O(nlogn)\)查詢時間只需要\(O(1)\),總時間復雜度\(O(nlogn+q)\)。
例題:POJ-1330 Nearest Common Ancestors
題意:給你一棵樹,問兩個節點的LCA
題解:LCA板子題,每棵樹的查詢都只有一次,所以暴力並查集也能做
#include<iostream>
#include<cstdio>
#include<stack>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<string>
#include<string.h>
#include<queue>
#include<functional>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define sz(a) (int)a.size()
#define de(a) cout<<#a<<" = "<<a<<endl
#define dd(a) cout<<#a<<" = "<<a<<" "
#define be begin
#define en end
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
const int maxn = 10000;
const double eps = 1e-9;
struct Star{
int next,to;
}edge[maxn+5];
int head[maxn+5];
int d[maxn<<2][64],limit[maxn+5],number[maxn+5];
//d是ST表,limit是每個節點在歐拉序中第一次出現的下標,number記錄的是重新編號對應的節點
int cnt = 0,num = 0;
void dfs(int u){
d[++cnt][0] = ++num;
int tnum = num;
limit[u] = cnt;
number[num] = u;
for(int i = head[u];i != -1;i = edge[i].next){
dfs(edge[i].to);
d[++cnt][0] = tnum;
}
return;
}
void makst(){
for(int j = 1;(1<<j) <= cnt;j++)
for(int i = 1;(i + (j << 1) - 1) <= cnt;i++)
d[i][j] = min(d[i][j-1], d[i + (1<<(j-1))][j-1]);
}
int query(int l,int r){
int k = log2(r-l+1)+eps;
return min(d[l][k], d[r-(1<<k)+1][k]);
}
bool flag[maxn+5];
int main()
{
//std::ios::sync_with_stdio(false);
//std::cin.tie(0);
int T;
scanf("%d", &T);
while(T--){
int n;
scanf("%d", &n);
cnt = 0,num = 0;
memset(flag, 0, sizeof flag);
memset(head, -1, sizeof head);
for(int i = 1;i < n;i++){
int x,y;
scanf("%d%d", &x, &y);
flag[y] = true;
edge[++cnt].to = y;
edge[cnt].next = head[x];
head[x] = cnt;
}
cnt = 0;
for(int i = 1;i <= n;i++){
if(flag[i] == 0){
dfs(i);
break;
}
}
makst();
int x,y;
scanf("%d%d", &x, &y);
if(limit[x] > limit[y]) swap(x,y);
printf("%d\n", number[query(limit[x],limit[y])]);
}
return 0;
}
Tarjan_LCA
Tarjan_LCA算法在搜索的過程中有使用路徑壓縮,會破壞原來的樹形結構,所以屬於離線算法,需要把所有的詢問全部讀取后進行搜索。
先將所有詢問復制一份掛載在相關的節點上,比如詢問[1,4],則在1節點上用隊列保存詢問節點4,並在4節點上用隊列保存詢問節點1。把所有的詢問全部掛載在樹上后,就成功離線了。
搜索過程從根節點開始,當搜索到節點1時,由於節點4暫時沒有被搜索,所以先標記節點1表示已被搜索並跳過這個詢問。當搜索另一條子鏈搜索到節點4時,利用並查集的查找思想遞歸查找父親並路徑壓縮,由於1節點已經被搜索且回溯,則當前1節點路徑壓縮后的父親一定是[1,4]的LCA。Tarjan_LCA相當於dfs掃一遍整棵樹的同時計算答案,時間復雜度\(O(n+q)\)。
具體的過程推薦這篇博客對Tarjan_LCA的深入理解,講得很好。
例題:POJ-3728 The merchant
題意:有一些城市相互連接,城市連接呈樹狀(即兩個城市之間的道路有且只有一條)。有一個商人從一座城市到達另一座城市,在路上進貨一次和賣出一次,問最大收益是多少
題解:很有意思的一道題目。我們假設商人從城市A到城市B的過程看做“上山”和“下山”的過程,LCA就是這座“山”的“山峰”,那么商人可以在這些情況下獲得收益
上山時進貨,上山時售貨
上山時進貨,下山時售貨
下山時進貨,下山時售貨
我們建立四個數組,up[x]表示從x出發上山過程中的最大收益(即第一種情況的最大收益),mi[x]表示從x出發上山過程中最低的進貨價格,mx[x]表示下山到達y點過程中最高的售貨價格,mx[y]-mi[x]表示從x點出發走到y點的最高收益(即第二種情況的最大收益),down[y]表示下山過程中到達y點的最大收益(即第三種情況的最大收益),所以我們就這么寫更新公式
up[x] = max(mx[y]-mi[x],max(up[x],up[y]));
down[x] = max(mx[x]-mi[y], max(down[x],down[y]));
mx[x] = max(mx[x],mx[y]);
mi[x] = min(mi[x],mi[y]);
#include<iostream>
#include<cstdio>
#include<stack>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<string>
#include<string.h>
#include<queue>
#include<functional>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define sz(a) (int)a.size()
#define de(a) cout<<#a<<" = "<<a<<endl
#define dd(a) cout<<#a<<" = "<<a<<" "
#define be begin
#define en end
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
const int maxn = 50000;
struct Star{
int next,to;
}edge[maxn<<2];
int head[maxn+5],cnt = 0;
int f[maxn+5],query[maxn+5];
struct Task{
int b,e,id;
Task(int a,int be,int c):b(a),e(be),id(c) { }
};
vector<Task> task[maxn+5];
vector<pair<int,int> > v[maxn+5];
int mx[maxn+5],mi[maxn+5],up[maxn+5],down[maxn+5];
int find(int x){//遞歸回溯並更新4個數組(類似並查集的路徑壓縮過程)
if(f[x] == x) return x;
int y = f[x];
f[x] = find(y);
up[x] = max(mx[y]-mi[x],max(up[x],up[y]));
down[x] = max(mx[x]-mi[y], max(down[x],down[y]));
mx[x] = max(mx[x],mx[y]);
mi[x] = min(mi[x],mi[y]);
return f[x];
}
bool flag[maxn+5];
void lca_tarjan(int u){
flag[u] = true;
vector<pair<int,int> >::iterator it = v[u].be();
for(;it != v[u].en();it++){
if(flag[(*it).fi]){//目標節點已被訪問,由於存在后繼節點,先掛載任務
int t = find((*it).fi);
if((*it).se > 0)//這題不同於普通的找LCA,詢問存在方向,保證上山方向和下山方向沒有搞錯
task[t].pb(Task((*it).fi, u, (*it).se));
else{
task[t].pb(Task(u, (*it).fi, -(*it).se));
}
}
}
for(int i = head[u];i != -1;i = edge[i].next){//先訪問后繼節點,保證樹形結構
if(!flag[edge[i].to]){
lca_tarjan(edge[i].to);
f[edge[i].to] = u;//由於可能存在當前節點u為子節點LCA的情況,在子節點沒有搜索完成前不能指向父親,否則路徑壓縮會造成樹形結構破壞以及尋找LCA失敗
}
}
vector<Task>::iterator at = task[u].be();
for(;at != task[u].en();at++){//回溯后處理任務
int x = at->b,y = at->e,id = at->id;
find(x),find(y);//尋找LCA
query[id] = max(mx[y]-mi[x],max(up[x],down[y]));
}
}
int main()
{
//std::ios::sync_with_stdio(false);
//std::cin.tie(0);
int n;
scanf("%d", &n);
memset(head, -1, sizeof head);
for(int i = 1;i <= n;i++){
int w;
scanf("%d", &w);
f[i] = i;
mi[i] = mx[i] = w;
}
for(int i = 1;i < n;i++){
int x,y;
scanf("%d%d", &x, &y);
edge[++cnt].to = y;
edge[cnt].next = head[x];
head[x] = cnt;
edge[++cnt].to = x;
edge[cnt].next = head[y];
head[y] = cnt;
}
int q;
scanf("%d", &q);
for(int i = 1;i <= q;i++){//離線詢問
int x,y;
scanf("%d%d", &x, &y);
v[x].pb(mp(y,-i));
v[y].pb(mp(x,i));
}
lca_tarjan(1);
for(int i = 1;i <= q;i++)
printf("%d\n", query[i]);
return 0;
}
后記
圖論還沒有完全學完,等待着我的還有網絡流和生成樹,等我學清楚了再來總結吧