图论学习总结(一万字+)

图论中, 既然是图论, 首先要学会的就是建图.
建图一共有两种方式:
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 算法是基于迭代的思想


步骤:

  1. 扫描所有边 ( 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]
  2. 重复上述步骤,直到没有更新操作发生

#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. 建立队列,最初的队列只有起点1
  2. 取出头结点 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 入队
  3. 重复上述操作,直到队列为空
    在这里利用了三角不等式,即 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[k1][i][j],f[k1][i][k]+f[k1][k][j]);

因为这里只用到了 k − 1 k-1 k1 层状态所以可以进行降维操作,类似于 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 n1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。

在这里插入图片描述

一个有 N N N 个点的图,边一定是大于等于 N − 1 N-1 N1 条的。图的最小生成树,就是在这些边中选择 N − 1 N-1 N1 条出来,连接所有的 N N N 个点。这 N − 1 N-1 N1 条边的边权之和是所有方案中最小的。

定理
任 意 一 棵 最 小 生 成 树 一 定 包 含 无 向 图 中 权 值 最 小 的 边 任意一棵最小生成树一定包含无向图中权值最小的边

解决最小生成树问题有两种方案

  1. p r i m prim prim算法
  2. 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 算法总是维护无向图的最小生成森林, 最初可认为生成森林由零条边构成,每个节点各自构成一颗仅包含一个点的树,


思路: 建立并查集,首先将所有边从小到大排序, 每次只选取最小的边,如果这两个点未被连通,那么我们将这条边连起来, 如果已经被连通则忽略该边,利用贪心的思想,每次连最小的并且两端未被连通的边.
步骤:

  1. 建立并查集,每个点各自构成一个集合
  2. 把所有边按权值从小到大排序, 依次扫描每条边 ( x , y , z ) (x,y,z) (x,y,z)
  3. x , y x,y x,y 属于同一个集合(连通),则忽略这条边,继续扫描另一条边
  4. 否则,合并 x , y x,y x,y 所在集合,并且把 z z z 累加到答案中
  5. 所有边扫描完后,第 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 n1 轮迭代, 算法结束(所有边满足三角不等式), 则图中无负环

#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 XiXjCk, 其中 C k C_k Ck 是常数, 1 ≤ i , j ≤ N , 1 ≤ k ≤ M 1≤i,j≤N, 1 ≤k≤M 1i,jN,1kM, 我们要解决的问题就是: 求一组解, 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+XjCk 可以变形为 X i ≤ X j + C k X_i ≤ X_j + C_k XiXj+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. 求不等式组的可行解
  2. 求最大值,最小值问题

1.求不等式组的可行解

首先有一个必须要满足的条件, 那就是从源点出发能经过每一条边
步骤:

  1. 先将每个不等式 X i ≤ X j + C k X_i ≤ X_j + C_k XiXj+Ck, 转化 为 X j X_j Xj 连向 X i X_i Xi 的一条长度为 $C_k $的边
  2. 找到一个超级源点使得该源点一定能够遍历所有边
  3. 从源点求一遍单源最短路

结果1: 如果存在负环, 则原不等式组一定无解
结果2: 如果不存在负环, 则 d i s t [ i ] dist[i] dist[i] 是一个可行解


2.求最大值和最小值

结论: 最小值求最长路, 最大值求最短路
假设 X i ≤ C X_i ≤ C XiC —> X i ≤ 0 + C X_i ≤ 0 + C Xi0+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 k1
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[i1][k1]][k1];

步骤:
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

  • 9
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

广西小蒟蒻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值