Vue管理系統前端系列六動態路由-許可權管理實現
目錄
LCA
1. 演算法分析
1.1 求LCA的四種方法
1.樹上倍增法:
倍增思想:\(f[i][j]\)表示i這個位置向上走2^j步後到達x,則有狀態轉移:\(f[y][j] = f[[y][j-1]][j-1]\),利用這個不斷處理出f陣列,樹上倍增法能夠得到 \(f[x][i]\) 陣列和 \(d[i]\) 陣列,利用這兩個陣列可以求出很多的東西。這是線上做法
2.tarjan演算法:
dfs的特性和並查集的特性。把所有點分成三類,第一類:正在搜尋的點,第二類:已經回溯完的點,第三類:還沒有搜尋過的點,每次搜尋的時候,記當前點為x,把點x做個標記,然後判斷和這個點對應的點y是否已經回溯過了,如果y已經回溯過了,那麼x和y的lca即為y的get(y)得到的節點。這是離線
3.dfs+ST
4.樹剖求lca
1.2 求lca的兩種場景
- 求任意兩個點的lca
- 求集合的lca:求一個集合的lca就是求這個集合中dfs序最小的點和dfs序最大的點的lca
2. 板子
2.1 樹上倍增法
HDU 2586
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 10; int f[N][20], d[N], dist[N]; // f[i][j]表示從i開始,往上走2^j步到達的點,d為深度,dist為距離 int e[N], ne[N], h[N], idx, w[N]; int T, n, m, t; // t為數的深度 queue<int> q; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } // 預處理:得到每個點的深度,距離,f陣列 void bfs() { q.push(1); // 把根放入佇列,注意這裡有可能根不是1 d[1] = 1; while (q.size()) { int x = q.front(); q.pop(); for (int i = h[x]; i != -1; i = ne[i]) { int y = e[i]; if (d[y]) continue; d[y] = d[x] + 1; // 更新深度 dist[y] = dist[x] + w[i]; // 更新距離 // 進行dp更新 f[y][0] = x; for (int j = 1; j <= t; ++j) { f[y][j] = f[f[y][j - 1]][j - 1]; // 分兩段處理 } q.push(y); } } } // 查詢x和y的最近公共祖先 int lca(int x, int y) { if (d[x] > d[y]) swap(x, y); // 保證x的深度淺一點 for (int i = t; i >= 0; --i) if (d[f[y][i]] >= d[x]) y = f[y][i]; // 讓x和y到同一個深度 if (x == y) return x; for (int i = t; i >= 0; --i) // 讓x和y之差一步就能相遇 { if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i]; } return f[x][0]; } int main() { cin >> T; while (T--) { memset(h, -1, sizeof h); idx = t = 0; cin >> n >> m; t = (int)(log(n) / log(2)) + 1; // 得到樹的深度 // 讀入一棵樹 for (int i = 0; i < n - 1; ++i) { int a, b, c; scanf("%d %d %d", &a, &b, &c); add(a, b, c), add(b, a, c); } bfs(); // 回答詢問 for (int i = 1; i <= m; ++i) { int a, b; scanf("%d %d", &a, &b); printf("%d\n", dist[a] + dist[b] - 2 * dist[lca(a, b)]); } } return 0; }
2.2 tarjan演算法
HDU 2586
#include <bits/stdc++.h> using namespace std; int const N = 1e5 + 10; int e[N], ne[N], idx, ans[N], v[N], fa[N], d[N], h[N], w[N]; vector<int> query[N], query_id[N]; int t, n, m; void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a] , h[a] = idx++; } // 並查集查詢+路徑壓縮 int get(int x) { if (fa[x] != x) fa[x] = get(fa[x]); return fa[x]; } // tarjan演算法求lca void tarjan(int x) { // 記錄這個點走過一次,但是還沒有回溯 v[x] = 1; // 遍歷每一個和x相連的點 for (int i = h[x]; i != -1; i = ne[i]) { int y = e[i]; if (v[y]) continue; // 這個點走過的話,不進行後面的操作 d[y] = d[x] + w[i]; tarjan(y); // 得到y為根節點的所有子樹的d fa[y] = x; // 更新y的父節點 } // 判斷和x有關的lca詢問 for (int i = 0; i < query[x].size(); ++i) { int y = query[x][i]; int id = query_id[x][i]; if (v[y] == 2) { int lca = get(y); // 獲得lca:如果y點回溯,那麼lca為get(y) ans[id] = min(ans[id], d[x] + d[y] - 2 * d[lca]); // 更新答案 } } v[x] = 2; // 標記x點回溯 } int main() { cin >> t; while (t--) { cin >> n >> m; // 初始化 memset(h, -1, sizeof h); idx = 0; for (int i = 1; i <= n; ++i) { fa[i] = i; query[i].clear(), query_id[i].clear(); } // 讀入樹邊 for (int i = 1; i < n; ++i) { int a, b, c; scanf("%d %d %d", &a, &b, &c); add(a , b, c), add(b, a, c); } // 讀入詢問的邊 for (int i = 1; i <= m; ++i) { int a, b; scanf("%d %d", &a, &b); if (a == b) { ans[i] = 0; } else { query[a].push_back(b), query[b].push_back(a); query_id[a].push_back(i), query_id[b].push_back(i); ans[i] = (1 << 30); } } // 做dfs求lca tarjan(1); // 輸出答案 for (int i = 1 ; i <= m; ++i) { cout << ans[i] << endl; } } return 0; }
2.3 dfs+ST
/*
我們需要維護陣列oula[i]= j表示j的dfs序為i,pos[i]=j表示i第一次在dfs序中出現的位置是j
len記錄dfs序的長度,dp[i][j]表示從i點出發走2^j步的範圍內最小的深度的點的座標,de[i]=j表示i的深度為j
本演算法為dfs+st表求lca,預處理時間O(nlogn), 查詢O(1)
演算法步驟:
1. dfs:預處理出dfs序
2. ST:預處理dp陣列
3. 給定任意兩個點:通過dp陣列得出這兩個點間的深度最小的點,即為lca
*/
#include <bits/stdc++.h>
using namespace std;
int const N = 1e5 + 1, M = N * 2;
int e[M], ne[M], h[N], idx;
int t, n, m;
int oula[M], len, pos[N], dp[M][23], de[M];
typedef pair<int, int> PII;
set<PII> s;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 求dfs序
void dfs(int u, int fa, int d)
{
oula[++len] = u; // 記錄第len個為u點
pos[u] = len; // 記錄u點的dfs序為len(只記錄u點第一次出現的dfs序即可)
de[len] = d; // 記錄深度
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dfs(j, u, d + 1);
oula[++len] = u; // 回溯時還要記錄
de[len] = d;
}
}
// 得到x和y中深度比較小的那個
int Min(int x, int y)
{
return de[x] > de[y]? y: x;
}
// 處理dp陣列
void ST()
{
for (int i = 1; i <= len; ++i) dp[i][0] = i;
for (int j = 1; (1 << j) <= len; ++j )
for (int i = 1; i + (1 << j) - 1 <= len; ++i )
dp[i][j] = Min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]); // dp[i][j]是dp[i][j - 1]和dp[i + (1 << (j - 1))][j - 1]中深度更小的那個點的下標
}
// 求lca
int lca(int x, int y)
{
// int x = pos[x], y = pos[y]; // 得到x和y點的dfs序的下標
if (x > y) swap(x, y);
int k = log2(y - x + 1); // 計算在dfs序列中y和x的距離
return Min(dp[x][k], dp[y - (1 << k) + 1][k]); // lca為x~y範圍內深度最小的那個dfs序的下標
}
int main()
{
cin >> t;
while (t--)
{
scanf("%d", &n);
s.clear();
memset(h, -1, sizeof h);
idx = 0, len = 0;
memset(dp, 0, sizeof dp);
memset(oula, 0, sizeof oula);
memset(de, 0, sizeof de);
memset(pos, 0, sizeof pos);
for (int i = 1; i <= n - 1; ++i)
{
int a, b;
scanf("%d %d", &a, &b);
add(a, b);
add(b, a);
}
int root = 0;
dfs(root, 0, 1); // dfs得出dfs序
ST(); // 得出dp陣列
scanf("%d", &m);
while (m--)
{
getchar();
char op = getchar();
int num;
scanf("%d", &num);
if (op == '+') s.insert({pos[num], num}); // 插入
else // 刪除
{
auto it = s.lower_bound({pos[num], num});
s.erase(it);
}
// 輸出答案
if (s.size() == 1) printf("%d\n", (*s.begin()).second);
else if (s.size() >= 2)
{
auto left = (*s.begin()).first;
auto right = (*prev(s.end())).first;
printf("%d\n", oula[lca(left, right)]);
}
else printf("-1\n");
}
}
return 0;
}
2.4 樹剖求lca
見 樹鏈剖分.md
3. 典型例題
acwing356 次小生成樹
題意: 給定一張 N 個點 M 條邊的無向圖,求無向圖的嚴格次小生成樹。設最小生成樹的邊權之和為sum,嚴格次小生成樹就是指邊權之和大於sum的生成樹中最小的一個。\(N≤10^5,M≤3*10^5\)
題解: 本題的思路是在求出最小生成樹的基礎上,找出一條非樹邊a->b,然後再樹上找出a->b的最大值,刪除這個最大值,加上非樹邊。
基於這個思路,目標就是要找出這個a->b在樹邊的最大值。
找出a->b在樹邊的最大值,可以先在樹上進行預處理,\(fa[i][j]\)表示i向上走\(2^j\)步到達的點,\(d1[i][j]\)表示i向上走\(2^j\)步範圍內的最大值,\(d2[i][j]\)表示i向上走\(2^j\)步範圍內的次大值,然後每次在找lca時順便找出,x到lca的最大值和次大值,y到lca的最大值和次大值,比較即可
程式碼:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
struct Edge
{
int a, b, w;
bool used;
bool operator< (const Edge &t) const
{
return w < t.w;
}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 找最小生成樹
LL kruskal()
{
for (int i = 1; i <= n; i ++ ) p[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; i ++ )
{
int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
if (a != b)
{
p[a] = b;
res += w;
edge[i].used = true;
}
}
return res;
}
// 建樹
void build()
{
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
if (edge[i].used)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
add(a, b, w), add(b, a, w);
}
}
// 預處理fa,d1,d2,depth
void bfs()
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
q[0] = 1; // 把1當成根節點
int hh = 0, tt = 0;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)
{
depth[j] = depth[t] + 1; // 更新j的深度
q[ ++ tt] = j;
fa[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -INF; // 求出d1和d2
for (int k = 1; k <= 16; k ++ )
{
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; u ++ )
{
int d = distance[u];
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
// 找出a和b的lca,順便求出a到b之間的最大值和次大值
int lca(int a, int b, int w)
{
static int distance[N * 2];
int cnt = 0;
if (depth[a] < depth[b]) swap(a, b);
// 把a和b拉到同一個深度
for (int k = 16; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
a = fa[a][k];
}
// 把a和b之間的d1和d2的所有備選項求出來
if (a != b)
{
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
distance[cnt ++ ] = d1[b][k];
distance[cnt ++ ] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
distance[cnt ++ ] = d1[a][0];
distance[cnt ++ ] = d1[b][0];
}
// 把a和b之間的d1和d2求出來
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++ )
{
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2) dist2 = d;
}
if (w > dist1) return w - dist1;
if (w > dist2) return w - dist2;
return INF;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
LL sum = kruskal(); // 計算最小生成樹的值
build(); // 把所有的樹邊建樹
bfs(); // 預處理出d1,d2,fa,depth陣列
LL res = 1e18;
for (int i = 0; i < m; i ++ )
if (!edge[i].used) // 找出每條非樹邊,然後替換掉最大的那條樹邊
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
res = min(res, sum + lca(a, b, w));
}
printf("%lld\n", res);
return 0;
}
Arab Collegiate Programming Contest 2015
題意: 求一個集合的lca
題解: 求一個集合的lca就是求這個集合中dfs序最小的點和dfs序最大的點的lca。dfs+ST處理。
程式碼:
#include <bits/stdc++.h>
using namespace std;
int const N = 1e5 + 1, M = N * 2;
int e[M], ne[M], h[N], idx;
int t, n, m;
int oula[M], len, pos[N], dp[M][23], de[M];
typedef pair<int, int> PII;
set<PII> s;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 求dfs序
void dfs(int u, int fa, int d)
{
oula[++len] = u; // 記錄第len個為u點
pos[u] = len; // 記錄u點的dfs序為len(只記錄u點第一次出現的dfs序即可)
de[len] = d; // 記錄深度
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dfs(j, u, d + 1);
oula[++len] = u; // 回溯時還要記錄
de[len] = d;
}
}
// 得到x和y中深度比較小的那個
int Min(int x, int y)
{
return de[x] > de[y]? y: x;
}
// 處理dp陣列
void ST()
{
for (int i = 1; i <= len; ++i) dp[i][0] = i;
for (int j = 1; (1 << j) <= len; ++j )
for (int i = 1; i + (1 << j) - 1 <= len; ++i )
dp[i][j] = Min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]); // dp[i][j]是dp[i][j - 1]和dp[i + (1 << (j - 1))][j - 1]中深度更小的那個點的下標
}
// 求lca
int lca(int x, int y)
{
// int x = pos[x], y = pos[y]; // 得到x和y點的dfs序的下標
if (x > y) swap(x, y);
int k = log2(y - x + 1); // 計算在dfs序列中y和x的距離
return Min(dp[x][k], dp[y - (1 << k) + 1][k]); // lca為x~y範圍內深度最小的那個dfs序的下標
}
int main()
{
cin >> t;
while (t--)
{
scanf("%d", &n);
s.clear();
memset(h, -1, sizeof h);
idx = 0, len = 0;
memset(dp, 0, sizeof dp);
memset(oula, 0, sizeof oula);
memset(de, 0, sizeof de);
memset(pos, 0, sizeof pos);
for (int i = 1; i <= n - 1; ++i)
{
int a, b;
scanf("%d %d", &a, &b);
add(a, b);
add(b, a);
}
int root = 0;
dfs(root, 0, 1); // dfs得出dfs序
ST(); // 得出dp陣列
scanf("%d", &m);
while (m--)
{
getchar();
char op = getchar();
int num;
scanf("%d", &num);
if (op == '+') s.insert({pos[num], num}); // 插入
else // 刪除
{
auto it = s.lower_bound({pos[num], num});
s.erase(it);
}
// 輸出答案
if (s.size() == 1) printf("%d\n", (*s.begin()).second);
else if (s.size() >= 2)
{
auto left = (*s.begin()).first;
auto right = (*prev(s.end())).first;
printf("%d\n", oula[lca(left, right)]);
}
else printf("-1\n");
}
}
return 0;
}