搜索与图论(二)

搜索与图论(二)

最短路

  • n:节点数,m:边数
  • m=n^2 (稠密图)时用朴素算法否则用优化算法

在这里插入图片描述

Dijkstra

  1. 朴素Dijkstra算法O(n^2)稠密图—邻接矩阵
  • 首先维护一个已更新最短距离的集合s(初始状态只有原点),和到达原点距离的集合dist,(dist[1]=0)
  • 循环n次,对于每一个未更新最短距离的点,找到当前所有点当中距离起点最近的点t让t加入s
  • 遍历未加入到s中的点,根据t更新s中的点到起点的最短距离
  • j到原点的距离应该是
    j原先到原点的距离t到原点的距离加上t到j的距离比较的最小值

在这里插入图片描述

849 Dijkstra 求最短路I

在这里插入图片描述

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

using namespace std;

const int N =510;

int g[N][N];               //邻接矩阵

int dist[N];              //表示点到原点1的距离

bool st[N];

int n,m;

int dijkstra(){
    
    memset (dist ,0x3f,  sizeof dist );
    
    dist [1]=0;
    
    //循环n次更新距离
    for(int i=0;i<n;i++)
    {
     
     int s=-1;     
     
     //找到当前到原点最短的点s,s初始化为-1    
     for(int j=1;j<=n;j++)
         if(!st[j]&&(dist[s]>dist[j]||s==-1)) s=j;
        
     st[s]=true;    
     
     //更新各个点到原点的距离,被加入到st的不更新, 算上外循环,总共更新n次除st中点的剩余点的距离,一共m次,时间复杂度为m
     for(int j=1;j<=n;j++)
         if(!st[j])dist[j]=min(dist[j],dist[s]+g[s][j]);   //因为g默认为无穷,所以如果sj之间没有通路也不会更新错误
        
    }    
        
    if(dist[n]==0x3f3f3f3f)return -1;          //如果dist[n]等于无穷说明走不到n
    
    else  return  dist[n];
    
}


int main(){
    
    cin>>n>>m;
    
    memset(g,0x3f,sizeof g);
    
    while(m--){
        
        int a,b,c;
        
        cin>>a>>b>>c;
        
        g[a][b]=min(g[a][b],c);               //因为可能存在重边,取重边当中的最小值即可,,用邻接表不需要考虑是否有重边
    }
    
    
    cout<<dijkstra();
    
    
    return 0;
}

850 Dijkstra 求最短路II

  1. 堆优化版算法O(mlogn)稀疏图—邻接表
  • 首先分析一下朴素版的时间复杂度
  • 一共要维护n个点,所以外层循环是n次
  • 对于步骤1找到不在s中的距原点距离最小点t,时间复杂度是O(n),共计O(n^2)
  • 对于步骤二将找到的距离最小点t加入到s中时间复杂度是O(1),共计O(n)
  • 对于步骤3用t更新s中其他点的距离,时间复杂度是O(n^2)其实只更新了m条边,实际上为O(m)
  • 若采用堆优化的方式—查询最小值操作复杂度为O(1),修改堆中值操作为O(logn)
  • 则步骤123的复杂度分别更新为O(n),O(n),O(mlogn),取最大值即复杂度为Omlogn

本题模拟堆的方式直接使用优先队列(小根堆)——priority_queue(会有数据冗余),不用手写堆

在这里插入图片描述

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

using namespace std;

typedef pair<int,int>  PII;

const int N=1e5+10;

int n,m;

int h[N],e[N],w[N],ne[N],idx;                                 //w为边权

priority_queue<PII, vector <PII>,greater<PII>>  heap;         //初始化小根堆

bool st[N];                                                   //是否遍历标记

int dist[N];                                

void  insert (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;
    
    heap.push({0,1});                             //first为距离,second为节点,注意优先队列根据first元素排序,所以距离写在第一个
    
    while(heap.size())                         //有可能遍历到重复边,所以可能循环次数超过n次
    {
        
        auto t= heap.top();                      
        
        heap.pop();                               //找到堆中的最小点
        
        int ver=t.second,distance=t.first;
        
        if(st[ver])continue;                       //如果之前已经遍历过这个元素,就取消后面的操作
        
        st[ver]=true;
        
        //根据当前点更新距离的操作
        
        for(int i=h[ver];i!=-1;i=ne[i])           //i是ver下一个节点的idx,,, j是他的节点编号
        {
            
            int j=e[i];
            
            if(dist[j]>distance+w[i])             //如果满足条件,更新并加入堆,如果不更新也加入堆,会使其复杂,但不错
            {
                
                dist[j]=distance+w[i];
                
                heap.push({dist[j],j});
            }
        }
        
    }
    
    if(dist[n]==0x3f3f3f3f)  return -1;
    
    return dist [n];
    
    
}



int main(){
 
    cin>>n>>m;
    
    memset(h,-1,sizeof h);
    
    while(m--)
    {
        
        int a,b,c;
        
        cin>>a>>b>>c;
        
        insert(a,b,c);
        
    }
    
    cout<<dijkstra();
    
    
    return 0;
} 

写一下dijkstra算法的总结:
可以看到无论是朴素还是优化版的dijkstra算法,无非就是重复这几个步骤
1是先初始化dist为无穷,原点为0
2是每次找到距离最近的点加入st[]
为什么加入st?因为每次距离最短的点根据图的性质一定是距离最小点,所以必定不会再更新了,所以每次更新的时候也不用考虑st里的数
3根据当前点更新其他点的距离
朴素版是更新所有点的距离(其实到达不了的点也更新不了,只是遍历了)
优化版的是只更新当前点可以到达的点的距离

bellman—ford —O(nm)

基本操作
外循环迭代n次(迭代k次表示从起点到每个点最多只能经过k条边,,n相同)
每次遍历所有边,更新距离
bellman_ford 算法存储不一定用到邻接表,只需要能够让它遍历到m条边即可
所以使用简单的结构体

在这里插入图片描述
经过bellman—ford之后一定满足如下三角不等式
在这里插入图片描述
如图:如果路径之中存在一个负权环,那么不存在最短路径(可以在负环内无限循环)但如果限制了只能经过k条边,那么负权环就没有影响
在这里插入图片描述

853有边数限制的最短路

说一下为什么需要一个备份
每次迭代会更新1条路径
比如第一次迭代,会更新所有点到原点经历1条边的最短距离
第二次迭代,会更新所有点到原点经历2条边的最短距离

拿下图做比方:
第一次更新2号点应当变成1,3号点应当变成3,
如果有点经历1条边到不了原点,那么他保持无穷不变

但是如果不做备份,当遍历到2—>3这条边时,如果1—>2已经更新,就会把3距离更新成2这是不允许的,更新成2就相当于从原点经历两条边到3

为了避免这种错误,只需要将2更新前的数值(无穷)保存,
就会避免第一次迭代时3更新成距离2

在这里插入图片描述
在这里插入图片描述

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

using namespace std;

const int N=510,M=10010;

int n,m,k;

struct edge                         //结构体存储m条边
{
 
 int a,b,c;
    
}edges[M];

int dist[N];

int package[N];                               //每次遍历前的备份

int bellman(){
    
    memset(dist,0x3f,sizeof dist);
    
    dist[1]=0;
    
    for(int i=0;i<k;i++){
        
        memcpy(package,dist,sizeof dist);                         //备份dist,使各条边在同时更新的时候不互相产生影响
        
        for(int j=0;j<m;j++){
            
            int a=edges[j].a,b=edges[j].b,c=edges[j].c;          //a是边的左节点,b是右节点,c是边权,更新这条边相当于更新右节点b到原点的距离
            
            dist[b]=min(dist[b],package[a]+c);                   //更新b时用的是上一次迭代的a,因为a与b同时更新,a更新后可能会影响到b
        }
    }
    
    if(dist[n]>0x3f3f3f3f/2)return -1;                           //最后一个点到达不了,但有可能不是0x3f3f3f3f,如果前一节点也是0x3f3f3f3f且到n为一负权边,那么更新的距离会比0x3f3f3f3f小
    
    else return dist[n];
   
}



int main(){
    
    cin>>n>>m>>k;
     
    for(int i=0;i<m;i++){
          
        int a,b,c;
      
        cin>>a>>b>>c;
          
        edges[i]={a,b,c};
          
    }
    
    if(bellman()==-1)cout<<"impossible";
      
    else  cout<<bellman();    
    
    return 0;
}

spfa

1优化的bellmanford算法
bellman-ford算法操作如下:
for n次
for 所有边 a,b,w (松弛操作)
dist[b] = min(dist[b],back[a] + w)

spfa算法对第二行中所有边进行松弛操作进行了优化,原因是在bellman—ford算法中,即使该点的最短距离尚未更新过,但还是需要用尚未更新过的值去更新其他点,由此可知,该操作是不必要的,我们只需要找到更新过的值去更新其他点即可

在这里插入图片描述

2、spfa算法步骤
queue <– 1
while queue 不为空
(1) t <– 队头
queue.pop()
(2)用 t 更新所有出边 t –> b,权值为w
queue <– b (若该点被更新过,则拿该点更新其他点)

时间复杂度 一般:O(m) 最坏:O(nm)
n为点数,m为边数

3、spfa也能解决权值为正的图的最短距离问题,且一般情况下比Dijkstra算法还好

841 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],ne[N],w[N],idx;

int dist[N];

int st[N];

void  insert (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);
    
    queue <int> q;                                    //定义距离变小的点的队列q
    
    dist[1]=0;
    
    q.push(1);
    
    st[1]=true;                                       //已入队标记成true
    
    while(q.size()){
        
        auto t=q.front();
        
        q.pop();
        
        st[t]=false;                                 //出队之后更新成false ??与dijkstra不同的是spfa每个入队的点并不能保证入队的点是所有点距离最短的点
                                                    //人话就是可能以后还会更新这个点,所以出队后要做标记,可能还会入队
        
        
        //优化更新操作
        for(int i=h[t];i!=-1;i=ne[i]){
            
            int j=e[i];
            
            if(dist[j]>dist[t]+w[i]){              //因为点t距离变小,所以查看他后面的所有点距离是否变小
                
                dist[j]=dist[t]+w[i];
                
                if(!st[j]){                        //如果j距离变小且不在队列里,把他加入队列
                    
                    q.push(j);
                    
                    st[j]=true;                   //入队标记
                }
            }
            
        }
        
    }
    
    if(dist[n]==0x3f3f3f3f)  return -1;
    
    else  return dist[n];
    
}


int main(){
    
    cin>>n>>m;
    
    memset(h,-1,sizeof h);
    
    while(m--){
        
        int a,b,c;
        
        cin>>a>>b>>c;
        
        insert (a,b,c);
        
    }
    
    
    if(spfa()==-1)  cout<<"impossible";
    
    else  cout<<spfa();
    
    return 0;
}

842 spfa求负环

使用spfa算法解决是否存在负环问题

1、dist[x] 记录当前1到x的最短距离

2、cnt[x] 记录当前最短路的边数,初始每个点到1号点的距离为0,只要他能再走n步,即cnt[x] >= n,则表示该图中一定存在负环,由于从1到x至少经过n条边时,则说明图中至少有n + 1个点,表示一定有点是重复使用

3、若dist[j] > dist[t] + w[i],则表示从t点走到j点能够让权值变少,因此进行对该点j进行更新dist[j]=dist[t]+w[i],并且对应cnt[j] = cnt[t] + 1即j对应的边要比t多走一条(如下图)

注意:该题是判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点

在这里插入图片描述

有负环的话相当于某些点到起点的距离为负无穷,然后SPFA算法是正确的,且初始时这些点的距离为0,0大于负无穷,所以一定会把这些距离为负无穷的点不断更新————yxc

在这里插入图片描述

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

using namespace  std;

const int N=2010,M=10010;

int h[N],w[M],e[M],ne[M],idx;                //这里注意M条边开的w,e,ne都应该是M,一共只有n节点所以节点头h开N

int n,m;

queue <int> q;                              //如果手写队列应该开成循环队列,点入队的次数可能较多

int dist[N],cnt[N];

int st[N];

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

int spfa(){
    
    for(int i=1;i<=n;i++){                             //因为负环不一定存在于节点1打头路径上,所以把所有节点都当做都当头遍历一次
                                   
        q.push(i);                                     //因为不找最短路径所以不初始化dist
        
        st[i]=true;
        
    }
    
    while(q.size()){
        
        auto 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]){                    //正常的正权点更不更新不影响寻找负环,因为负环存在的点距离原点相当于负无穷,所以他的cnt一定会无限更新
                
                dist[j]=dist[t]+w[i];
                
                cnt[j]=cnt[t]+1;                         
                
                if(cnt[j]>=n)return 1;                  //如果边数超过点数,就一定存在负环
                
                if(!st[j]){
                    
                    q.push(j);
                    
                    st[j]=true;
                    
                }
            }
        }
        
    }
    
    return 0;
    
}

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



Floyd

算法复杂度O(n^3)
使用邻接矩阵实现

在这里插入图片描述
floyd算法原理:如果存在顶点k,使得当以k为中介点时,i到j的距离缩短
即dist[i][j]>dist[i][k]+dist[k][j]那么就把距离更新成后者

那么更新完之后整个邻接矩阵

更新完之后,邻接矩阵存的是每一个点到其他点的最短距离,可能不存在

854 Floyd 求最短路

在这里插入图片描述

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

using namespace  std;

const int N=210, INF=1e9;

int g[N][N];

int 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++)
          g[i][j]=min(g[i][j],g[i][k]+g[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)g[i][j]=0;             //对角线初始化成0,其余初始化成INF
           
           else g[i][j]=INF;
       }
       
    while(m--){
        
        int a,b,c;
        
        cin>>a>>b>>c;
        
        g[a][b]=min(g[a][b],c);
        
    }
    
    floyd();
    
    while(k--){
        
        int a,b;
        
        cin>>a>>b;
        
        if(g[a][b]>INF/2)cout<<"impossible"<<endl;        //同dijkstra算法,因为可能存在负权边,所以超过INF/2时也判定为无法到达
        
        else cout<<g[a][b]<<endl;
        
    }
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值