搜索与图论2

总览:

最短路

1.最短路

难点在建图,将问题抽象成最短路问题,如何定义图,如何定义边

1.单源最短路

求一个点到其他点的最短问题

①所有边权都是正数

<i>朴素的Dijkstra算法

时间复杂度:O(n^2)    n指点数  m为边的数量

比较适合边数比较多的图(稠密图) 稠密图是m ~ n^2   稀疏图 m ~ n

算法思路:

求的是一号点到其他所有 点的最短距离,集合s储存当前已确定最短距离的点

①初始化距离第一个点dis[1] = 0, 其他点dis[i] = 正无穷,也就是不确定的

②迭代过程for(i从0到n),找到(最短距离未确定的点)不在s中的距离最近的点,为t

③将t加入到s中, 用t更新其他点的距离,        

搜索与图论中说过,稠密图用邻接矩阵存储

稀疏图用邻接表存储

例题:

题目:

给定一个 n个点 m条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 −1。

输入格式
第一行包含整数 n和 m


接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为z。

输出格式
输出一个整数,表示 1号点到 n号点的最短距离。

如果路径不存在,则输出 −1


数据范围
1 ≤ n ≤ 500
,
1 ≤ m ≤ 10^5

图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

代码:

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N = 510;

int n, m;
int g[N][N]; // g[1][2]表示1到2的距离,当距离等于0x3f时说明不存在这条边
int dist[N]; // 代表一个点到源点的距离
bool st[N]; // st[i]为true时表示i这个点到源点的最小距离值已经确定了

int dijkstra()
{
    memset (dist, 0x3f, sizeof dist); //初始化距离数组,每个点到源点的距离都是0x3f无穷大
    dist[1] = 0; // 初始化 源点到自己的距离为0
    
    for (int i = 0; i < n - 1; i ++ ) //每次循环就能得到一个点到源点的最短距离值,循环n遍就能得出所有点到源点的最短距离
    {                                 //这里循环n - 1可以理解为第一个源点已经知道了
                                      //当然i < n也可以ac
        int t = -1; //在每次迭代开始时初始化为 -1。这里的目的是为了找到当前阶段(即尚未被标记的节点中)距离起点最近的节点。
                    //在第一次循环时,t 被设置为第一个满足 !st[j] 条件的节点 j,以便作为起始节点。
                    //可以理解t是一个中介点,先将t拉出s集合外,然后当后面在s中遍历点时突然遍历到未确定最短距离的点j时,直接将j附给t,这样再在后续遍历中比较最短距离并不断更新最短距离的点
        
        //遍历所有没有确定最短距离的点,找到距离最近的点
        for (int j = 1; j <= n; j ++) 
            if (!st[j] && (t == -1 || dist[t] > dist[j])) //因为t=-1当  第一个  满足!st[j]的j点出现时,&&后面的条件也一定符合
                t = j;
                
            //找到距离最小的点t,并用最小的点t去更新其他的点到起点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]); //计算t能到达的点的距离,因为假如t和j两点没有边,那dist[j] = min(0x3f, dist[t] + 0x3f)很显然还是0x3f
        
         st[t] = true; //当前点t已经知道了最短距离标记一下
    }
    
    if (dist[n] == 0x3f3f3f3f) return -1; //如果源点到达不了点n,那点n到源点的距离将会是初始化时的0x3f3f3f3f)返回-1
    return dist[n]; //反之就返回距离
    
}

int main()
{
    cin >> n >> m;
    
    memset (g, 0x3f, sizeof g); //所有节点之间的距离初始化为无穷大0x3f
    
    while (m --) //m条边
    {
        int a, b, c;
        cin >> a >> b >> c;
        
        g[a][b] = min(g[a][b], c); //如果有重边,就保留权重最小的那条边
    }
    
    cout << dijkstra() << endl;
    
    return 0;
}

<ii>堆优化版的Dijkstra算法

时间复杂度:O(mlogn)

m和n一个级别时,都是十万时,可以用这个(稀疏图)

优化的地方:

遍历找不在s中的距离最近的点,时间复杂度是n^2的

我们可以用堆来优化,

例题:

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y的有向边,边长为 z。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1


数据范围
1 ≤ n,m ≤ 1.5×10^5
,
图中涉及边长均不小于 0,且不超过 10000

数据保证:如果最短路存在,则最短路的长度不超过 109

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3

代码:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

// 稀疏图用邻接表存储
int n, m;
int e[N], w[N], h[N], ne[N], idx; // w[N]存储权重
int dist[N]; 
bool st[N]; // true代表这个点已经确定最短距离了

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], 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, distance = t.first;
        
        if (st[ver]) continue; // 如果这个点确定过了就直接跳过,因为有重边,所以堆里面有很多沉余的点
                               // 比如这个点是a距离是3,但是有重边存的有两个 一个是3a,另一个假如距离是4,存的4a,3a肯定在4a前面弹出来
                               // 所以这个点就在4a弹出来前已经确定最短距离了,再弹出这个点就直接跳过
        st[ver] = true;
        
        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i]; // i是下标,就是那个不断变的idx,e[N] 存储的才是这个点
            if (dist[j] > distance + w[i]) // 更新距离
            {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    
    cout << dijkstra() << endl;
    
    return 0;
}

②存在负权边

<i>Bellman-Ford

使用情况:有边数限制的最短路

时间复杂度:O(nm)

求不超过k条边的最短路

存在原因:

为什么存在负权边就不能用Dijkstra算法了?

用dijkstra算法在图中的最短路径是1  -> 2 -> 4 -> 5 最短距离是11,但是还有一条路径是1 -> 3 -> 4 -> 5 最短距离是-97,显然这条路径距离更短,因此dijkstra算法失效

有负权回路时,有可能没有最短路:

会发现,2 -> 3 -> 4这一圈,没走一圈距离就会减1,所以1 到 5 就没有最短路,就会陷入死循环

所以一般的最短路问题,没有负环

算法思路:

松弛完后,会发现对所有边都满足dist[b] <= dist[a] + w

bakcup数组存在的原因:

为了避免串联情况的发生,

当边数限制是1时,1到3的最短路应该是3,但是如果没有backup数组在第二层循环里,第一次是 1 -> 2 这条边更新了2,距离是1,第二次j是 2 -> 3 这条边更新了3,距离是1+1为2,在后面更新中,更新到 1 -> 3 这条边,3的距离依旧是 2,很显然到最后结果就错了,因为 距离2的这个路径有两条边,这就是串联的情况发生了,因为你不知道下个点更新了会不会超过k的限制,所以我们要用bakcup数组存储上次的dist 

如果有了bakcup数组,当第二次j是 2 -> 3 这条边时,dist[3] = min(dist[3], dist[2] + w)显然dist[3]是无穷,在上次dist中dist[2]也是无穷,这种串联情况发生后,dist[3]不会因此更新,当我们更新到 1 -> 3 这条边时,dist[3] = min(dist[3], dist[1] + w),更新后是3,很正确

为什么最后判断时,判断的是dist[n] > 0x3f3f3f3f / 2而不是dist[n] == 0x3f3f3f3f:

假如这种情况,要求1到5的最短路,发现1根本走不到5,但是4经过松弛操作后会更新5号点,显然0x3f3f3f3f < 0x3f3f3f3f - 999,,所以5号点会更新成0x3f3f3f3f - 999,所以这时候判断dist[n] == 0x3f3f3f3f显然不合适,边长的绝对值不超过100000,减的也不会太大,和0x3f3f3f3f在一个数量级即可。

例题:

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。

注意:图中可能 存在负权回路 。

输入格式
第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

点的编号为 1∼n。

输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible。

数据范围
1 ≤ n, k ≤ 500,
1 ≤ m ≤ 10000,
1 ≤ x, y ≤ n

任意边长的绝对值不超过 10000。

输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3

代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 510, M = 10010;

struct Edge{
    int a;
    int b;
    int w;
}edges[M];

int n, m, k;
int dist[N];
int bakcup[N];

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    for (int i = 0; i < k; i ++ )
    {
        memcpy(bakcup, dist, sizeof dist);
        
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b], bakcup[a] + w);
        }
    }
    
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m >> k;
    
    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        cin >> a >> b >> w;
        edges[i] = {a, b, w};
    }
    
    bellman_ford();
    
    if (dist[n] > 0x3f3f3f3f / 2) cout << "impossible" << endl;
    else cout << dist[n];
    
    return 0;
}

<ii>SPFA

spfa是对Bellman-Ford的优化,但并不是所有情况都能用

可以处理有负权和没有负权的问题,但是可能会被卡

时间复杂度:一般O(m),    最坏O(nm)

算法思路和分析:

① Bellman_ford算法会遍历所有的边,但是有很多边遍历是没有意义的,这里我们可以优化一下,我们发现,只有当这个点的前驱结点更新了,这个点才有可能会更新,所以我们只用遍历那些到源点距离变小的点所连接的边即可,这里,我们可以用一个队列来存储距离被更新的结点。

② SPFA算法看上去和Dijkstra算法比较像,我们可以用Dijkstra的模版来改一下,当然他们还是有区别的,Dijkstra算法中使用了优先队列,是为了存储那些没有确定最短距离的点,目的是方便取出当前距离源点距离最近的点,而SPFA算法中使用的队列是用来存储那些距离源点距离改变的点,这里的队列也可以用其他数据结构。

③ Bellman_ford算法中最后的判断我们写的是dist[n] > 0x3f3f3f3f / 2 这是为了防止终点和起点不连通,导致无穷(0x3f3f3f3f)被更新了,但是SPFA算法中遍历的是与当前结点相连通的结点,所以当有那种不连通的情况发生时,0x3f3f3f3f不会被更新。

一般的Dijkstra算法可用SPFA算法直接来做,当然有时候出题人专门设置题目,让用SPFA算法的时间复杂度达到最坏的情况O(nm)。

一般用SPFA算法求负环,思路和Bellman_ford判负环是一样的,但是很显然用SPFA算法时间复杂度更快,算法方法是用一个cnt数组记录每个点到源点的边,每次更新距离时更新cnt数组,这个点的边数等于前驱的边数加1,一旦有点的边数达到了n,就证明有负环。

例题:

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出 1号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。

数据保证不存在负权回路。

输入格式
第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式
输出一个整数,表示 1号点到 n 号点的最短距离。

如果路径不存在,则输出 impossible。

数据范围
1 ≤ n, m ≤ 10^5

图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2

代码:

模版:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool st[N]; // 存储这个点是否在队列里

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int spfa()
{
    memset(dist, 0x3f, sizeof dist); // 初始化dist
    dist[1] = 0;
    
    queue<int> q; // 存储距离更新了的点
    q.push(1);  
    st[1] = true; // 说明点1在队列里了
    
    while (q.size())
    {
        int t = q.front();  // 取队头
        q.pop();
        
        st[t] = false; 
        
        // 遍历这个点能到的所有点
        for (int i = h[t]; i != -1; 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()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);
    }
    
    int t = spfa();
    if (t == 0x3f3f3f3f) cout << "impossible";
    else cout << t;
    
    
    return 0;
}
利用spfa判负环:
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[N], cnt[N];
bool st[N]; 

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

bool spfa()
{
    queue<int> q;
    
    // 因为知识判断负环,但是负环的位置不知道,为了防止负环存在的位置和起点不连通,把所有点都先加入队列中
    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }
    
    while (q.size())
    {
        int t = q.front();  
        q.pop();
        
        st[t] = false; 
        
        for (int i = h[t]; i != -1; 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;  //  更新边数
                
                if (cnt[j] >= n) return true;
                
                if (!st[j]) 
                {
                    q.push(j); 
                    st[j] = true;
                }
            }
        }
    }
     
    return false; 
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m;
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);
    }
    
    if (spfa()) puts("Yes");
    else puts("No");
    
    
    return 0;
}

2.多源汇最短路

源点就是起点   汇点就是终点
有多个询问,从其中一个点走到一个点,起点和终点是不确定的

<i>Floyd算法

时间复杂度:O(n^3)

可以处理负权,当然不能有负环,不然会负无穷

算法思路: ,

Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,支持负权边。

基于动态规划的思想

d[i, j]表示从i到j的最短距离

d[k,i,j] 表示 从i这个点出发,只经过1到k这些点到j的最短距离,所以d[k, i, j] = d[k - 1, i, k] + d[k - 1, k, j] 所以这样更新d[k, i, j], 然后第一维可以去掉 d[i, j] = d[i, k] + d[k, j] 

例题:

题目:

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y的最短距离,如果路径不存在,则输出 impossible。

数据保证图中不存在负权回路。

输入格式
第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。

输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出  

数据范围
1 ≤ n ≤ 200,
1 ≤ k ≤ n2
1 ≤ m ≤ 20000
图中涉及边长绝对值均不超过 10000

输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1

代码:
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210, INF = 1e9;

int n, m, Q;
int d[N][N];

void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m >> Q;
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;  // 为了排除自环,像d[1][1]这种自环,将其距离初始化0就行了
            
    while (m -- )
    {
        int a, b, w;
        cin >> a >> b >> w;
        d[a][b] = min(d[a][b], w);
    }
    
    floyd();
    
    while (Q --)
    {
        int a, b;
        cin >> a >> b;
        
        if (d[a][b] > INF / 2 ) cout << "impossible" << endl;
        else cout << d[a][b] << endl;
    }
    
    return 0;
}

  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值