图论中, 既然是图论, 首先要学会的就是建图.
建图一共有两种方式:
1.邻接矩阵
2.邻接表
邻接矩阵 使用于稠密图 即 点,边数量差不多 优点: 查询某两个点是否存在边, 十分迅速O(n) 缺点: 当边数较少,点数较多时,会产生巨大的空间浪费
邻接表: 使用于稀疏图 即 各种图 优点: 无优点 缺点 无缺点
建图方式
int e[N], ne[N], h[N], w[N], idx;
void add(int a, int b, int c) //当建无向图时就把w和c去掉即可
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}
//记得初始化头结点
memset(h, -1, sizeof h);
1.Dijkstra算法朴素版
时间复杂度 O ( n 2 ) O(n^2) O(n2)
D i j k s t r a Dijkstra Dijkstra 算法 是基于贪心的思想,它只适用于正权图,每次总是寻找距离起点最近的点, 因为这个点距离起点最近, 所以从其他点走到这点都比该距离要远,故可以确定这个距离为最短距离,再用这个点去更新这个点到其他点的距离,如此操作几次,从而得到答案.
#include<bits/stdc++.h>
using namespace std;
const int N = 510, M = 1e5 + 10, INF = 0x3f3f3f3f;
int g[N][N], dist[N], n, m;
bool st[N];
int dijkstra()
{
memset(dist, 0x3f, sizeof dist); //初始化距离
dist[1] = 0; //初始化起点
for(int i = 0; i < n ; i ++ ) //除去起点后,有n-1个点,所以需要更新n-1次
{
int t = -1;
for(int j = 1; j <= n; j ++ ) //找到一个未被标记的结点中dist最小的节点,用这个节点来更新其他节点
if(!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
st[t] = true; //标记该节点为最短距离
for(int j = 1; j <= n; j ++ ) //更新该节点到其他节点的最短距离
if(dist[j] > dist[t] + g[t][j])
dist[j] = dist[t] + g[t][j];
}
if(dist[n] == INF) return -1;
else return dist[n];
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g); //初始化距离
for(int i = 1; i <= m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
cout << dijkstra();
}
Dijkstra算法堆优化版本
时间复杂度: O ( m l o g n ) O(mlogn) O(mlogn)
因为朴素版本的 D i j k s t r a Dijkstra Dijkstra 算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2) ,显然不够理想,因为此我们就想到了给它进行优化, 这时候就要用到一个很牛的数据结构, 叫优先队列, 用它来维护 d i s t dist dist 数组, 用 O ( n l o g n ) O(nlogn) O(nlogn) 的时间获取最小值并且从堆中删除, 用 O ( l o g n ) O(logn) O(logn) 的时间执行一条边的扩展和更新, 最终可在 O ( m l o g n ) O(mlogn) O(mlogn) 的时间内实现 D i j k s t r a Dijkstra Dijkstra 算法
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f;
typedef pair<int,int>PII;
int e[N], ne[N], w[N], h[N], dist[N], idx, n, m;
bool st[N];
void add(int a, int b, int c) //建边
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist); //初始化距离
dist[1] = 0; //初始化起点
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); //入队
while(heap.size())
{
auto t = heap.top(); //取出堆顶
heap.pop();
int ver = t.second; //节点编号
if(st[ver]) continue; //如果这个状态已经被取出过就忽略
st[ver] = true;
for(int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[ver] + w[i]) //更新
{
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j}); //入队
}
}
}
if(dist[n] == INF) return -1;
else return dist[n];
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 1; i <= m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cout << dijkstra();
}
有的人可能不懂为什么出队一次后,就直接可以 c o n t i n u e continue continue, 我们可以回忆一下优先队列 B F S BFS BFS,每次取出的状态一定是最优的,队列中的其他状态一定不如这个状态, 所以如果某个状态第二次被取出,那么它一定不是最优的,所以可以直接忽略
bellman_ford 算法
b e l l m a n _ f o r d bellman\_ford bellman_ford 算法是基于迭代的思想
步骤:
- 扫描所有边 ( x , y , z ) (x,y,z) (x,y,z) 如果 d i s t [ y ] > d i s t [ x ] + z dist[y] > dist[x] + z dist[y]>dist[x]+z, 则用dist[x] + z 来更新 d i s t [ y ] dist[y] dist[y]
- 重复上述步骤,直到没有更新操作发生
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, M = 1e5 + 10, INF = 0x3f3f3f3f;
int n, m;
struct node
{
int a, b, w;
}edge[N];
int dist[N];
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist); //初始化距离
dist[1] = 0; //初始化起点
for(int i = 0; i < n - 1; i ++ ) //
for(int j = 1; j <= m; j ++ )
{
auto e = edge[j];
if(dist[e.b] > dist[e.a] + e.w)
dist[e.b] = dist[e.a] + e.w;
}
if(dist[n] == INF) return -1;
else return dist[n];
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
edge[i] = {a, b, c};
}
cout << bellman_ford();
}
Spfa算法
尽管 S p f a Spfa Spfa 已经死了, 但是我们还是要讲 s p f a spfa spfa 算法, spfa 算法本质上就是队列优化的bellman_ford算法,步骤:
- 建立队列,最初的队列只有起点1
- 取出头结点 x x x,扫描它的所有出边 ( x , y , z ) (x,y,z) (x,y,z),若 d i s t [ y ] > d i s t [ x ] + z dist[y] > dist[x] + z dist[y]>dist[x]+z,则使用 d i s t [ x ] + z dist[x] + z dist[x]+z 更新 d i s t [ y ] dist[y] dist[y], 同时若 y y y 不在队列中,则把 y y y 入队
- 重复上述操作,直到队列为空
在这里利用了三角不等式,即 d i s t [ y ] ≤ d i s t [ x ] + z dist[y] ≤ dist[x] + z dist[y]≤dist[x]+z, 所以通过以上操作, 最终图会收敛成全部满足三角不等式的状态,它的优点在于通过队列避免的bellman_ford算法中对不需要扩展的节点的重复扫描,提升了效率, 时间复杂度为 O ( k m ) O(km) O(km), k k k 是一个很小的常数, 但是在特殊的图上, 该算法可能会退化为 O ( n m ) O(nm) O(nm), 这就是为什么说 s p f a spfa spfa 它已经死了,所以 s p f a spfa spfa 谨慎使用
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int e[N], w[N], ne[N], h[N], idx, dist[N], n, m;
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist); //初始化距离
dist[1] = 0; //初始化起点
st[1] = true; //标记起点在队列中
queue<int>q;
q.push(1);
while(q.size())
{
auto t = q.front(); //取出队头
q.pop();
st[t] = false; //标记不在队列中
for(int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i]) //利用三角不等式进行更新
{
dist[j] = dist[t] + w[i];
if(!st[j]) //如果该点不在队列中,那么入队
{
q.push(j);
st[j] = true; //标记它在队列中
}
}
}
}
return dist[n];
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 1; i <= m; i ++ ) //读入建边
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
if(t == 0x3f3f3f3f) cout << "impossible" << '\n';
else cout << t;
}
这里会有一个问题, 为什么
b
e
l
l
m
a
n
f
o
r
d
bellman_ford
bellmanford 算法 最后判断无解是通过
d
i
s
t
[
n
]
>
0
x
3
f
3
f
3
f
3
f
dist[n] > 0x3f3f3f3f
dist[n]>0x3f3f3f3f , 而
s
p
f
a
spfa
spfa 算法判断无解是通过
d
i
s
t
[
n
]
=
=
0
x
3
f
3
f
3
f
3
f
dist[n] == 0x3f3f3f3f
dist[n]==0x3f3f3f3f 呢? 因为
s
p
f
a
spfa
spfa 算法计 算的是每一点到起点的距离, 如果无解,那么
d
i
s
t
[
n
]
dist[n]
dist[n] 就会等于
0
x
3
f
3
f
3
f
3
f
0x3f3f3f3f
0x3f3f3f3f,只有能走到的点才会被更新
而
b
e
l
l
m
a
n
f
o
r
d
bellman_ford
bellmanford 算法 是连续的进行松弛, 无论能不能到达
n
n
n 点, 都会进行更新,所以此时的
d
i
s
t
[
n
]
dist[n]
dist[n] 因为存在 负权边可能会小于
0
x
3
f
3
f
3
f
3
f
0x3f3f3f3f
0x3f3f3f3f, 所以判断应该为
>
0
x
3
f
3
f
3
f
3
f
/
2
> 0x3f3f3f3f / 2
>0x3f3f3f3f/2
Floyd算法
F l o y d Floyd Floyd 算法本质是基于动态规划,为了求出图中任意两点之间的最短路径, 当然可以把每个点作为起点,求解 N N N 次单源最短路径问题, 不过,在任意两点间最短路问题中, 图一般为稠密图, 使用 Floyd 算法可以在 O ( n 3 ) O(n^3) O(n3) 来求解
利用动态规划的思想来写:
状态表示:
f
[
k
]
[
i
]
[
j
]
:
f[k][i][j]:
f[k][i][j]: 从
i
i
i 点走到
j
j
j 点,并且中间经过
k
k
k 点的集合
状态属性: 距离的最小值
状态计算:
f
[
k
]
[
i
]
[
j
]
=
m
i
n
(
f
[
k
−
1
]
[
i
]
[
j
]
,
f
[
k
−
1
]
[
i
]
[
k
]
+
f
[
k
−
1
]
[
k
]
[
j
]
)
;
f[k][i][j] = min(f[k - 1][i][j], f[k - 1][i][k] + f[k - 1][k][j]);
f[k][i][j]=min(f[k−1][i][j],f[k−1][i][k]+f[k−1][k][j]);
因为这里只用到了 k − 1 k-1 k−1 层状态所以可以进行降维操作,类似于 01 01 01 背包降维
#include<bits/stdc++.h>
using namespace std;
const int N = 210;
int dist[N][N], n, m, k;
void Floyd()
{
for(int k = 1; k <= n; k ++ )
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++ )
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
int main()
{
cin >> n >> m >> k;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++ )
if(i == j) dist[i][j] = 0;
else dist[i][j] = 0x3f3f3f3f;
for(int i = 1; i <= m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
dist[a][b] = min(dist[a][b], c);
}
while(k -- )
{
int x, y;
cin >> x >> y;
int t = dist[x][y];
if(t > 0x3f3f3f3f / 2) cout << "impossible\n";
else
cout << dist[x][y] << '\n';
}
}
最小生成树
一个图中可能存在多条相连的边,我们一定可以从一个图中挑出一些边生成一棵树。这仅仅是生成一棵树,还未满足最小,当图中每条边都存在权重时,这时候我们从图中生成一棵树( n − 1 n - 1 n−1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。
一个有 N N N 个点的图,边一定是大于等于 N − 1 N-1 N−1 条的。图的最小生成树,就是在这些边中选择 N − 1 N-1 N−1 条出来,连接所有的 N N N 个点。这 N − 1 N-1 N−1 条边的边权之和是所有方案中最小的。
定理
任
意
一
棵
最
小
生
成
树
一
定
包
含
无
向
图
中
权
值
最
小
的
边
任意一棵最小生成树一定包含无向图中权值最小的边
任意一棵最小生成树一定包含无向图中权值最小的边
解决最小生成树问题有两种方案
- p r i m prim prim算法
- K r u s k a l Kruskal Kruskal 算法
prim算法
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
p
r
i
m
prim
prim 算法与
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra 算法类似, 每次找不在集合中且距离集合最近的点,用一个数组标记节点是否属于
T
T
T, 每次从未标记的节点中选出
d
d
d 值最小的,把它标记(新加入
T
T
T ), 同时扫描所有出边, 更新另一个端点的
d
d
d 值.
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int dist[N], g[510][510], n, m;
bool st[N];
int prim()
{
memset(dist, 0x3f, sizeof dist); //初始化距离
int res = 0;
for(int i = 0; i < n; i ++ )
{
int t = -1;
for(int j = 1; j <= n; j ++ ) //找到不在集合中,并且距离集合最近的点
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
//如果集合存在 并且不连通,那么不存在最小生成树
if(i && dist[t] == 0x3f3f3f3f) return 0x3f3f3f3f;
if(i) res += dist[t]; //当集合存在时 加上边权
st[t] = true; //标记为在集合中
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]); //更新
}
return res;
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for(int i = 1; i <= m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c); //保留最小的边
}
int t = prim();
if(t == 0x3f3f3f3f) cout << "impossible\n";
else cout << t;
}
K r u s k a l Kruskal Kruskal算法
时间复杂度: O ( m l o g m ) O(mlogm) O(mlogm)
K r u s k a l Kruskal Kruskal 算法总是维护无向图的最小生成森林, 最初可认为生成森林由零条边构成,每个节点各自构成一颗仅包含一个点的树,
思路: 建立并查集,首先将所有边从小到大排序, 每次只选取最小的边,如果这两个点未被连通,那么我们将这条边连起来, 如果已经被连通则忽略该边,利用贪心的思想,每次连最小的并且两端未被连通的边.
步骤:
- 建立并查集,每个点各自构成一个集合
- 把所有边按权值从小到大排序, 依次扫描每条边 ( x , y , z ) (x,y,z) (x,y,z)
- 若 x , y x,y x,y 属于同一个集合(连通),则忽略这条边,继续扫描另一条边
- 否则,合并 x , y x,y x,y 所在集合,并且把 z z z 累加到答案中
- 所有边扫描完后,第 4 4 4 步中处理过的边就构成最小生成树
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, INF = 0x3f3f3f3f;
struct node
{
int a, b, w;
bool operator<(const node & t)
{
return w < t.w;
}
}edge[N];
int p[N], n, m;
int find(int x) // 并查集
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int Kruskal()
{
sort(edge + 1, edge + 1 + m); //排序
int res = 0,cnt = 0;
for(int i = 1; i <= m; i ++ ) //从小到大遍历每一条边
{
int a = edge[i].a, b = edge[i].b, c = edge[i].w;
a = find(a), b = find(b);
//如果a,b未联通, 那么就将他们连起来
if( a != b) p[a] = b, res += c, cnt ++ ;
}
if(cnt < n - 1) return INF;
return res;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) p[i] = i; //初始化并查集
for(int i = 1; i <= m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
edge[i] = {a, b, c};
}
int t = Kruskal();
if(t == INF) cout << "impossible";
else cout << t;
}
负环
负环, 如果图中存在环,并且环上各边的权值之和是负数,则称这个环为负环
利用 b e l l m a n f o r d bellman_ford bellmanford 算法
若经过 n n n 轮迭代, 算法仍未结束(仍有可能产生更新的边), 则图中存在负环
若经过 n − 1 n - 1 n−1 轮迭代, 算法结束(所有边满足三角不等式), 则图中无负环
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
struct node //结构体存边
{
int a, b, c;
}edge[N];
int dist[N], n, m;
bool bellman_ford()
{
memset(dist,0x3f,sizeof dist); //初始化距离
for(int i = 0; i < n; i ++ ) //迭代n - 1轮
{
for(int j = 1; j <= m; j ++ )
{
auto e = edge[j];
if(dist[e.b] > dist[e.a] + e.c)
dist[e.b] = dist[e.a] + e.c;
}
}
for(int i = 1; i <= m; i ++ )
//迭代第 n 轮,如果存在边不满足三角形不等式,则图中有负环
{
auto e = edge[i];
if(dist[e.b] > dist[e.a] + e.c) return 1;
}
return 0;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i ++ ) //读入
{
int a, b, c;
cin >> a >> b >> c;
edge[i] = {a, b, c};
}
int t = bellman_ford();
if(!t) cout << "No\n";
else cout << "Yes\n";
}
利用
s
p
f
a
spfa
spfa 算法
方法1: 设
c
n
t
[
x
]
cnt[x]
cnt[x] 表示从
1
1
1 到
x
x
x 的最短路径包含的边数,当执行更新
d
i
s
t
[
y
]
=
d
i
s
t
[
x
]
+
z
dist[y] = dist[x] + z
dist[y]=dist[x]+z 时, 同样更新
c
n
t
[
y
]
=
c
n
t
[
x
]
+
1
cnt[y] = cnt[x] + 1
cnt[y]=cnt[x]+1,此时若发现
c
n
t
[
y
]
≥
n
cnt[y] ≥ n
cnt[y]≥n,则图中有负环,若算法正常结束,则图中没有负环
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int e[N], ne[N], h[N], w[N], dist[N], idx, n, m, cnt[N];
bool st[N];
void add(int a, int b, int c) //建边
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}
bool spfa()
{
queue<int>q; //队列
for(int i = 1; i <= n; i ++) //因为可能图不连通,所以需要将所有点入队
st[i] = true, q.push(i); //标记为在队列中
while(q.size())
{
auto t = q.front(); //取出队头
q.pop();
st[t] = false; //标记为不在队列中
for(int i = h[t]; ~i; i = ne[i]) //遍历
{
int j = e[i];
if(dist[j] > dist[t] + w[i]) //可以更新
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1; //边数+1
q.push(j);
if(cnt[j] >= n) return 1; //边数≥n,则说明存在负环
}
}
}
return 0;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 1; i <= m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
if(t) cout << "Yes\n";
else cout << "No\n";
}
方法2:判断负环,通过记录每个点的入队次数, 次数达到
n
n
n 则说明有负环,
但是该方法不如方法1,效率较低
与负环相关的问题: 01 01 01 分数规划
-----> 本蒟蒻还没学数论,就先不讲了
差分约束
差分约束系统是一种特殊的 N N N 元一次不等式组, 它包含 N N N 个遍历 $X_1 $ ~ X N X_N XN, 以及 M M M 个约束条件, 每个约束条件都是由两个变量作差构成的, 形如 X i − X j ≤ C k X_i - X_j ≤ C_k Xi−Xj≤Ck, 其中 C k C_k Ck 是常数, 1 ≤ i , j ≤ N , 1 ≤ k ≤ M 1≤i,j≤N, 1 ≤k≤M 1≤i,j≤N,1≤k≤M, 我们要解决的问题就是: 求一组解, X 1 = a 1 , X 2 = a 2 . . . X N = a N X_1 = a_1, X_2 = a_2...X_N = a_N X1=a1,X2=a2...XN=aN,使所有与约束条件都满足,差分约束系统的每个约束条件 X i + X j ≤ C k X_i +X_j ≤ C_k Xi+Xj≤Ck 可以变形为 X i ≤ X j + C k X_i ≤ X_j + C_k Xi≤Xj+Ck, 这时候你会发现与 s p f a spfa spfa 算法中的三角不等式 d i s t [ y ] ≤ d i s t [ x ] + z dist[y] ≤ dist[x] + z dist[y]≤dist[x]+z 很相似, 因此, 我们能够将 X_i 看作图中的一个点,
差分约束能够解决两种问题
- 求不等式组的可行解
- 求最大值,最小值问题
1.求不等式组的可行解
首先有一个必须要满足的条件, 那就是从源点出发能经过每一条边
步骤:
- 先将每个不等式 X i ≤ X j + C k X_i ≤ X_j + C_k Xi≤Xj+Ck, 转化 为 X j X_j Xj 连向 X i X_i Xi 的一条长度为 $C_k $的边
- 找到一个超级源点使得该源点一定能够遍历所有边
- 从源点求一遍单源最短路
结果1: 如果存在负环, 则原不等式组一定无解
结果2: 如果不存在负环, 则
d
i
s
t
[
i
]
dist[i]
dist[i] 是一个可行解
2.求最大值和最小值
结论: 最小值求最长路, 最大值求最短路
假设
X
i
≤
C
X_i ≤ C
Xi≤C —>
X
i
≤
0
+
C
X_i ≤ 0 + C
Xi≤0+C
可转化为
0
0
0 指向
i
i
i 的一条长度为
C
C
C 的边
最近公共祖先
求最近公共祖先常用的方法
1.向上标记法
对于每个点,沿着树往上走, 直到走到根节点, 将其中经过的结点都打上标记,
对于某两个点, 他们第一个公共标记的点就为他们的最近公共祖先
2.倍增法
倍增的关键就是二进制拼凑, 计算出
2
k
2^k
2k 能够到达的节点
这里我们可以定义一个状态
状态表示:
f
[
i
]
[
j
]
f[i][j]
f[i][j] : 表示节点
i
i
i 向上跳
k
k
k 步的集合
状态属性: 跳到的节点
状态计算: 从
i
i
i 节点跳
k
k
k 步 可以 转移成跳两次
k
−
1
k-1
k−1 步
f
a
[
i
]
[
k
]
=
f
a
[
f
a
[
i
−
1
]
[
k
−
1
]
]
[
k
−
1
]
fa[i][k] = fa[fa[i - 1][k - 1]][k - 1]
fa[i][k]=fa[fa[i−1][k−1]][k−1];
步骤:
1.通过bfs 预处理出fa数组
2.先让深度大的节点跳到另一节点相同深度的地方,然后如果此时为相同节点,直接返回即可,不同,则说明最近公共祖先还在两节点的上方
3.让两个节点一起向上跳,并且跳的终点不能相同,并且从大到小枚举,(因为如果跳的结点相同,说明跳过了,通过从大到小枚举,可以保证,第一个满足条件的节点就是最近公共祖先的下一次,所以此时再从当前节点跳
2
0
2^0
20 步即可
预处理:
void bfs(int root)
{
memset(depth,0x3f,sizeof depth);
depth[root] = 1, depth[0] = 0;
q.push(root);
while(q.size())
{
auto t = q.front();
q.pop();
for(int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if(depth[j] > depth[t] + 1 )
{
depth[j] = depth[t] + 1;
q.push(j);
fa[j][0] = t; // 跳1步 可以从 t 跳到j
for(int k = 1; k <= 15; k ++ )
fa[j][k] = fa[fa[j][k - 1]][k - 1]; //2^k = 2^k-1 + 2^k-1;
}
}
}
}
LCA:
int lca(int a,int b)
{
if(depth[a] < depth[b]) swap(a,b);
for(int k = 15; k >= 0; k -- ) // 把x 往上跳 深度减少
if(depth[fa[a][k]] >= depth[b]) //能够跳
a = fa[a][k];
if(a == b ) //跳到了同一深度
return a;
for(int k = 15; k >= 0; k --)
if(fa[a][k] != fa[b][k])
a = fa[a][k], b = fa[b][k];
return fa[a][0];
}
对于求
L
C
A
LCA
LCA 还有另一种做法:
t
a
r
j
a
n
tarjan
tarjan 离线做法
其本质是对向上标记法的优化 时间复杂度为
O
(
n
+
m
)
O(n + m)
O(n+m)
讲解 见图片
询问要加两次因为:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, M = 2 * N;
typedef pair<int,int>PII;
int e[M], ne[M], h[N], w[M], idx, dist[N], res[M], st[N], p[N], n, m;
vector<PII>query[M];
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}
void dfs(int u, int fa)
{
for(int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if(j == fa) continue;
dist[j] = dist[u] + w[i]; //计算到根节点的距离
dfs(j, u); //搜
}
}
int find(int x) //并查集模板
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void tarjan(int u)
{
st[u] = 1; // 正在搜索
for(int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if(!st[j])
{
tarjan(j); //先搜到底部
p[j] = u; //将j合并到其根节点u
}
}
for(auto it : query[u]) //遍历与u有关的询问
{
int b = it.first, i = it.second;
if(st[b] == 2) //如果已经回溯了那么直接计算
res[i] = dist[b] + dist[u] - 2 * dist[find(b)];
}
st[u] = 2; //已经回溯
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 1; i <= n; i ++ ) p[i] = i; //初始化并查集
for(int i = 1; i <= n - 1; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c); //建双向边
}
for(int i = 1; i <= m; i ++ )
{
int a, b;
cin >> a >> b;
query[a].push_back({b, i}); //询问
query[b].push_back({a, i}); //建双向的 原因在图片里面有说
}
dfs(1, -1); //深搜算距离
tarjan(1); //计算询问
for(int i = 1; i <= m; i ++ ) //输出答案
cout << res[i] << "\n";
}
更新中ing