搜索与图论(二)

搜索与图论(二)

最短路

n:节点数,m:边数
m=n^2 (稠密图)时用朴素算法否则用优化算法
在这里插入图片描述

Dijkstra

1)朴素Dijkstra算法(稠密图—邻接矩阵
外层循环n次
内层找当前距离最近的点n次(n^2)

在这里插入图片描述
步骤:
1.找到当前所有点当中距离起点最近的点t
2.让t加入s
3根据t更新其他点到起点的最短距离
dist[j]=min(dist[j],dist[s]+g[s][j]);
j到原点的距离应该是
j自己到原点的边s到原点的边加上s到j的边 比较的最小值
4重复步骤
在这里插入图片描述
斜对角线是各个顶点到自己的距离g[ 1 ] [ 1 ] 是点1到1 的距离为0(不允许自环)
g[ 1 ] [ 2 ] 是点1到2的距离

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    算上外循环n次时间复杂度为n^2
     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

堆优化版算法 (稀疏图——邻接表
找最小点的操作优化成o(n)原来是o(n^2)
修改路径的操作复杂度变为mlog(2)n——堆的修改操作复杂度是log(2)n,一共修改m条边所以是 mlog(2)n
只有在mlog(2)n<n^2时使用堆优化版本
所以m满足 m<n^2(稀疏图)

本题模拟堆的方式直接使用优先队列(小根堆)——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())
    {
        
        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;
}
posted @ 2019-11-17 22:09  zzcxxoo  阅读(220)  评论(0)    收藏  举报