图论(2):最短路

最短路

  • 一、模板
    • [1. Floyd](#1. Floyd)
    • [2. 01BFS](#2. 01BFS)
    • [3. SPFA](#3. SPFA)
    • [4. Dijkstra(弱化版)](#4. Dijkstra(弱化版))
    • [5. Dijkstra(优化版)](#5. Dijkstra(优化版))
  • 二、例题
    • [1. Floyd](#1. Floyd)
      • [1.1 传送门](#1.1 传送门)
      • [1.2 无向图最小环](#1.2 无向图最小环)
      • [1.3 灾后重建](#1.3 灾后重建)
      • [1.4 飞猪](#1.4 飞猪)
    • [2. 01BFS](#2. 01BFS)
      • [2.1 Kathiresan](#2.1 Kathiresan)
      • [2.2 障碍路线](#2.2 障碍路线)
      • [2.3 奇妙的棋盘](#2.3 奇妙的棋盘)
    • [3. SPFA](#3. SPFA)
      • [3.1 奶牛派对](#3.1 奶牛派对)
      • [3.2 营救](#3.2 营救)
      • [3.3 航空路线](#3.3 航空路线)
      • [3.4 牛奶管道](#3.4 牛奶管道)
    • [4. Dijkstra](#4. Dijkstra)
      • [4.1 路径统计](#4.1 路径统计)
      • [4.2 平面上的点](#4.2 平面上的点)
      • [4.3 邀请函](#4.3 邀请函)

一、模板

1. Floyd

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e2+8;
const int INF=0x3f3f3f3f;
int n,m,dis[MAXN][MAXN];
int main(){
    cin>>n>>m;
    memset(dis,INF,sizeof(dis));
    for(int u=1;u<=n;u++)dis[u][u]=0;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,dis[u][v]=min(dis[u][v],w);
    for(int k=1;k<=n;k++)
        for(int u=1;u<=n;u++)
            for(int v=1;v<=n;v++)
                dis[u][v]=min(dis[u][v],dis[u][k]+dis[k][v]);
    for(int u=1;u<=n;u++)
        for(int v=1;v<=n;v++)
            cout<<(dis[u][v]==INF?-1:dis[u][v])<<" \n"[v==n];
    return 0;
}

2. 01BFS

原题:求 K K K 的所有非 0 0 0 倍数中十进制表示下数码和的最小值

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+8;
const int INF=0x3f3f3f3f;
int k,dis[MAXN];//除以k余数为x的数中最小的数码和
int bfs(){
    deque<int>dque;
    dis[1]=1,dque.push_back(1);
    while(!dque.empty()){
        int x=dque.front(),nx;
        dque.pop_front();
        if(x==0)return dis[x];
        //代价为0的操作,放在队首
        nx=x*10%k;//x后面直接加0,十进制数码和不变,放在队首
        if(dis[nx]>dis[x])
            dis[nx]=dis[x],dque.push_front(nx);
        //代价为1的操作,放在队尾
        nx=(x+1)%k;//x+1和x的数码和差1,放在队尾
        if(dis[nx]>dis[x]+1)
            dis[nx]=dis[x]+1,dque.push_back(nx);
    }
}
int main(){
    cin>>k;
    memset(dis,INF,sizeof(dis));
    cout<<bfs();
    return 0;
}

3. SPFA

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
int n,m,s,dis[MAXN];
bool in_que[MAXN];
vector<Edge>adj[MAXN];
void spfa(int s){
    queue<int>que;
    dis[s]=0,in_que[s]=true,que.push(s);
    while(!que.empty()){
        int u=que.front();
        in_que[u]=false,que.pop();
        for(auto[v,w]:adj[u])
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                if(!in_que[v])in_que[v]=true,que.push(v);
            }
    }
}
int main(){
    cin>>n>>m>>s;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,adj[u].push_back({v,w});
    memset(dis,INF,sizeof(dis));
    spfa(s);
    for(int i=1;i<=n;i++)
        cout<<(dis[i]==INF?INT32_MAX:dis[i])<<" ";
    return 0;
}

4. Dijkstra(弱化版)

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
bool vis[MAXN];
int n,m,s,t,dis[MAXN];
vector<Edge>adj[MAXN];
void dijkstra(int s){
    dis[s]=0;
    for(int i=1;i<=n;i++){
        int u=0;
        for(int j=1;j<=n;j++)
            if(!vis[j]&&dis[j]<dis[u])
                u=j;
        if(!u)return;
        vis[u]=true;
        for(auto[v,w]:adj[u])
            if(!vis[v]&&dis[v]>dis[u]+w)
                dis[v]=dis[u]+w;
    }
}
int main(){
    cin>>n>>m>>s>>t;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,adj[u].push_back({v,w}),adj[v].push_back({u,w});
    memset(dis,INF,sizeof(dis));
    dijkstra(s);
    cout<<dis[t];
    return 0;
}

5. Dijkstra(优化版)

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
bool vis[MAXN];
int n,m,s,t,dis[MAXN];
vector<Edge>adj[MAXN];
void dijkstra(int s){
    //两种方法均可,推荐没有注释的代码
    priority_queue<pair<int,int> >pq;//{-dis[u],u}
    dis[s]=0,pq.push({0,s});
    while(!pq.empty()){
        auto u=pq.top().second;
        pq.pop();
        if(vis[u])continue;
        vis[u]=true;
        for(auto[v,w]:adj[u])
            if(dis[v]>dis[u]+w)
                dis[v]=dis[u]+w,pq.push({-dis[v],v});
    }
    // priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >pq;
    // dis[s]=0,pq.push({0,s});
    // while(!pq.empty()){
    //     auto[d,u]=pq.top();
    //     pq.pop();
    //     if(d>dis[u])continue;
    //     for(auto[v,w]:adj[u])
    //         if(dis[v]>dis[u]+w) {
    //             dis[v]=dis[u]+w;
    //             pq.push({dis[v],v});
    //         }
    // }
}
int main(){
    cin>>n>>m>>s;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,adj[u].push_back({v,w});
    memset(dis,INF,sizeof(dis));
    dijkstra(s);
    for(int i=1;i<=n;i++)
        cout<<(dis[i]==INF?INT32_MAX:dis[i])<<" ";
    return 0;
}

二、例题

1. Floyd

1.1 传送门

香蕉大学里有 n n n 栋教学楼,有 m m m 条双向通行道路连接这些教学楼,不存在重边和自环。每条道路都有一定的长度,而且所有教学楼之间都可以直接或者间接的通过道路到达。我们可以很容易的求出这些教学楼之间的最短路。

为了使交通更为顺畅,校方决定在两个教学楼里增设一对传送门。传送门可以将这对教学楼的距离直接缩短为 0 0 0。利用传送门,某些教学楼之间的最短路的距离就变短了。

由于预算有限,学校里只能安装一对传送门。但是校长希望尽可能方便学生,使任意两点之间的最短路长度的总和最小。当然啦,从 x x x 教学楼到 y y y 教学楼的长度和从 y y y 教学楼到 x x x 教学楼的长度只需要统计一次就可以了。


标准的一道暴力枚举题。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e2+8;
const int INF=0x3f3f3f3f;
int n,m,dis[MAXN][MAXN];
int main(){
    cin>>n>>m;
    memset(dis,INF,sizeof(dis));
    for(int u=1;u<=n;u++)
        dis[u][u]=0;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,dis[u][v]=dis[v][u]=min(dis[u][v],w);
    for(int k=1;k<=n;k++)
        for(int u=1;u<=n;u++)
            for(int v=1;v<=n;v++)
                dis[u][v]=min(dis[u][v],dis[u][k]+dis[k][v]);
    int ans=INF;
    for(int i=1;i<=n;i++)//枚举传送门端点A
        for(int j=i+1;j<=n;j++){//枚举传送门端点B
            int sum=0;//存储安装传送门后所有点对的最短路总和
            for(int u=1;u<=n;u++)//枚举所有起点计算最短路总和
                for(int v=u+1;v<=n;v++)//枚举所有终点计算最短路总和   
                    sum+=min({//因为i和j间有传送门,视其距离为0
                        dis[u][v],
                        dis[u][i]+dis[j][v],
                        dis[u][j]+dis[i][v]
                    });
            ans=min(ans,sum);//每次更新答案到最小值
        }
    cout<<ans;
    return 0;
}

1.2 无向图最小环

给定一个 n n n 个点 m m m 条带正权边且无重边的无向图,请输出图中简单环的最小权值和。特别地,如果图中没有简单环则输出 "No Simple Cycle"。本题中的简单环是指,不重复经过同一个点,且不重复经过同一条边的环。


比较经典的一道松弛类型的题目。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e2+8;
const int INF=0x3f3f3f3f;
int t,dis[MAXN][MAXN],tmp[MAXN][MAXN];
int main(){
    cin>>t;
    while(t--){
        int n,m;
        cin>>n>>m;
        memset(dis,INF,sizeof(dis));
        memset(tmp,INF,sizeof(tmp));
        for(int u=1;u<=n;u++)
            dis[u][u]=tmp[u][u]=0;
        for(int j=1,u,v,w;j<=m;j++){
            cin>>u>>v>>w;
            dis[u][v]=dis[v][u]=min(dis[u][v],w);
            tmp[u][v]=tmp[v][u]=min(tmp[u][v],w);
        }
        int ans=INF;//最小环的权值和
        for(int k=1;k<=n;k++){//寻找所有i<k和j<k的通过k的形如i-j-k-i的环
            for(int i=1;i<k;i++)
                for(int j=i+1;j<k;j++)
                    if(dis[i][j]!=INF&&tmp[j][k]!=INF&&tmp[k][i]!=INF)
                        ans=min(ans,dis[i][j]+tmp[j][k]+tmp[k][i]);//使用tmp而非dis,防止重复顶点的出现
            for(int i=1;i<=n;i++)//用Floyd松弛
                for(int j=1;j<=n;j++)
                    dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
        }
        if(ans==INF)cout<<"No Simple Cycle\n";
        else cout<<ans<<"\n";
    }
    return 0;
}

1.3 灾后重建

题目传送门


由于题目保证每次输入都是不下降的,考虑寻找已经重建好的村庄 k k k 作为中转站,以寻找两村庄之间的最短路进行更新和输出。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e2+8;
const int INF=0x3f3f3f3f;
int n,m,q,tms[MAXN],dis[MAXN][MAXN];
int main(){
    cin>>n>>m;
    memset(dis,INF,sizeof(dis));
    for(int u=0;u<n;u++)
        dis[u][u]=0;
    for(int i=0;i<n;i++)
        cin>>tms[i];
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,dis[u][v]=dis[v][u]=min(dis[u][v],w);
    cin>>q;
    for(int i=1,x,y,t,k=0;i<=q;i++){
        cin>>x>>y>>t;
        while(k<n&&tms[k]<=t){
            for(int u=0;u<n;u++)
                for(int v=0;v<n;v++)
                    dis[u][v]=min(dis[u][v],dis[u][k]+dis[k][v]);
            k++;
        }
        if(tms[x]<=t&&tms[y]<=t&&dis[x][y]<INF)cout<<dis[x][y]<<"\n";
        else cout<<"-1\n";
    }
    return 0;
}

1.4 飞猪

给定一个 n n n 个点 m m m 条边的边带权简单连通无向图,在 0 0 0 时刻你在点 1 1 1 上。

假设当前是 t t t 时刻,你在点 v v v 上,你可以选择两种操作:

  • 仍停留在点 v v v 上,操作后到 t + 1 t+1 t+1 时刻。
  • 选择一条边 ( a , b , w ) (a,b,w) (a,b,w) 满足 a = v a=v a=v 或 b = v b=v b=v,则你到这条边连接的另一个点上,操作后到 t + w t+w t+w 时刻。

有 k k k 条信息,每一条信息形如 ( t i , v i ) (t_i,v_i) (ti,vi) 表示在 t i t_i ti 时刻,点 v i v_i vi 上会出现一只飞猪,其编号为 i i i,若该时刻你在 v i v_i vi 上,则你捕获到了 i i i 号飞猪。

现在你需要求出你能捕获到的飞猪数量的最大值。


看到中间分的两种情况可以立刻想到动态规划的做法,再结合要从 a a a 到达 b b b 想到最短路。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e2+8;
const int MAXK=5e3+8;
const int INF=0x3f3f3f3f;
int n,m,k,dis[MAXN][MAXN],dp[MAXK];//dp[i]表示捕获第i只飞猪时的最大飞猪数量
struct Pig{
    int t,idx;
    bool operator<(const Pig&rhs)const{return t<rhs.t;}
}pigs[MAXK];
int main(){
    cin>>n>>m>>k;
    memset(dis,INF,sizeof(dis));
    for(int u=1;u<=n;u++)dis[u][u]=0;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,dis[u][v]=dis[v][u]=min(dis[u][v],w);
    for(int k=1;k<=n;k++)//Floyd
        for(int u=1;u<=n;u++)
            for(int v=1;v<=n;v++)
                dis[u][v]=min(dis[u][v],dis[u][k]+dis[k][v]);
    for(int i=1;i<=k;i++)cin>>pigs[i].t>>pigs[i].idx;
    sort(pigs+1,pigs+k+1);
    memset(dp,-INF,sizeof(dp));
    pigs[0].idx=1,dp[0]=0;//0时刻在节点1(初始位置)捕获0只飞猪
    for(int i=1;i<=k;i++)//遍历每只飞猪i
        for(int j=0;j<i;j++)//尝试从之前的飞猪j转移过来
            if(pigs[j].t+dis[pigs[j].idx][pigs[i].idx]<=pigs[i].t)//从j的时间和位置,能否在i出现前到达i的位置
                dp[i]=max(dp[i],dp[j]+1);//若可达,则更新dp[i]为捕获j后再捕获i的数量
    int ans=0;
    for(int i=1;i<=k;i++)ans=max(ans,dp[i]);
    cout<<ans;
    return 0;
}

2. 01BFS

2.1 Kathiresan

Kathiresan 被关在一个高度戒备的矩形监狱中的单元格 ( 0 , 0 ) (0, 0) (0,0)。他必须到达 ( R − 1 , C − 1 ) (R−1, C−1) (R−1,C−1) 处的大门才能逃出监狱。Kathiresan 可以从任何单元格移动到其四个相邻单元格之一,如果 Kathiresan 当前位于 ( x 1 , y 1 ) (x_1,y_1) (x1,y1),那么他可以移动到 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 当且仅当它们的曼哈距离为 1 1 1 且 0 ≤ x 2 < R 0≤x_2<R 0≤x2<R 且 0 ≤ y 2 < C 0≤y2<C 0≤y2<C。Kathiresan 设法获得了监狱的地图。如果 map x 1 , y 1 = map x 2 , y 2 \text{map}{x_1,y_1}=\text{map}{x_2,y_2} mapx1,y1=mapx2,y2,则 Kathiresan 可以从 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 移动到 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 而不打晕任何守卫。如果 map x 1 , y 1 ≠ map x 2 , y 2 \text{map}{x_1,y_1} \ne \text{map}{x_2,y_2} mapx1,y1=mapx2,y2,则 Kathiresan 可以通过打晕一名守卫从 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 移动到 ( x 2 , y 2 ) (x_2,y_2) (x2,y2)。给定监狱的地图,找出为了逃出监狱,Kathiresan必须打晕的最少守卫数量。


显然,代价为 0 0 0 的操作是直接移动,代价为 1 1 1 的操作是打倒一名守卫然后移动。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e3+8;
const int INF=0x3f3f3f3f;
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};
int t,n,m,dis[MAXN][MAXN],grid[MAXN][MAXN];//dis:到每个单元格的最小代价
int bfs(int sx,int sy){
    deque<pair<int,int> >dque;
    dis[sx][sy]=0,dque.push_back({sx,sy});
    while(!dque.empty()){
        auto[x,y]=dque.front();
        dque.pop_front();
        if(x==n&&y==m)return dis[n][m];
        for(int i=0;i<4;i++){
            int nx=x+dx[i],ny=y+dy[i],w=grid[nx][ny]!=grid[x][y];
            if(grid[nx][ny]==-1||dis[nx][ny]<=dis[x][y]+w)continue;//如果超出地图或者该格有当前最优解
            dis[nx][ny]=dis[x][y]+w;
            //保证队列始终按距离排序,先处理距离近的单元格
            w?dque.push_back({nx,ny}):dque.push_front({nx,ny});
        }
    }
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>t;
    while(t--){
        memset(dis,INF,sizeof(dis));
        memset(grid,-1,sizeof(grid));
        cin>>n>>m;
        char ch;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                cin>>ch,grid[i][j]=ch;
        cout<<bfs(1,1)<<"\n";
    }
    return 0;
}

2.2 障碍路线

考虑一个 N × N N \times N N×N( 1 ≤ N ≤ 100 1 \le N \le 100 1≤N≤100)的由一个个方格组成的正方形牧场。有些方格是奶牛们不能踏上的,它们被标记为了 'x'。例如下图:

复制代码
. . B x .
. x x A .
. . . x .
. x . . .
. . x . .

贝茜发现自己恰好在点 A A A 处,她想去 B B B 处的舔盐块。但是奶牛是一种缓慢而且笨拙的动物,十分讨厌转弯。尽管如此,必要的时候她们还是会转弯的。对于一个给定的牧场,请你计算从 A A A 到 B B B 最少的转弯次数。开始的时候,贝茜可以面对任意一个方向,并且贝茜知道她一定可以到达 B B B 处。


遇到转弯类题目一定要把 dis 数组多开一个方向维度。可以发现,代价为 0 0 0 的操作是不转弯,代价为 1 1 1 的操作是转弯。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e2+8;
const int INF=0x3f3f3f3f;
const int dx[]={-1,0,1,0};
const int dy[]={0,1,0,-1};
int n,sx,sy,ex,ey,grid[MAXN][MAXN],dis[MAXN][MAXN][5];
int bfs(int sx,int sy){
    deque<tuple<int,int,int> >dque;
    for(int i=0;i<4;i++)
        dis[sx][sy][i]=0,dque.push_back({sx,sy,i});
    while(!dque.empty()){
        auto[x,y,dir]=dque.front();
        dque.pop_front();
        if(x==ex&&y==ey)return dis[x][y][dir];
        for(int i=0;i<4;i++){
            int nx=x+dx[i],ny=y+dy[i],w=i!=dir;
            if(grid[nx][ny]<0||dis[nx][ny][i]<=dis[x][y][dir]+w)continue;
            dis[nx][ny][i]=dis[x][y][dir]+w;
            w?dque.push_back({nx,ny,i}):dque.push_front({nx,ny,i});
        }
    }
}
int main(){
    memset(dis,INF,sizeof(dis));
    memset(grid,-1,sizeof(grid));
    cin>>n;
    char ch;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            cin>>ch;
            if(ch=='.')grid[i][j]=0;
            if(ch=='A')sx=i,sy=j,grid[i][j]=0;
            if(ch=='B')ex=i,ey=j,grid[i][j]=0;
        }
    cout<<bfs(sx,sy);
    return 0;
}

2.3 奇妙的棋盘

小猴在玩一个神奇的游戏,游戏中给出了一个 n × m n\times m n×m 的棋盘,棋盘中的格子有的黑,有的白。我们每次可以选择任意一个格子进行操作,然后这个格子和所有与它颜色相同的相邻的同色连通块中的所有格子的颜色全部取反。小猴想知道至少需要多少次操作可以使所有格子变成白色。


代价为 0 0 0 的操作显然是相邻格子颜色相同,而代价为 1 1 1 则是相邻格子颜色不同。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e2+8;
const int INF=0x3f3f3f3f;
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};
int n,m,col[MAXN][MAXN],dis[MAXN][MAXN];
//col:存储格子颜色 1黑0白
//dis:到达(x,y)所需的最少操作次数
int bfs(int sx,int sy){
    deque<pair<int,int> >dque;
    dis[sx][sy]=0,dque.push_back({sx,sy});
    int ret=0;//记录最终需要的最少操作次数
    while(!dque.empty()){
        auto[x,y]=dque.front();
        dque.pop_front();
        //更新处理到该位置时累计需要的操作次数
        ret=max(ret,dis[x][y]+col[x][y]);//(x,y)是黑色说明要翻转一次,否则不用翻转
        for(int i=0;i<4;i++){
            int nx=x+dx[i],ny=y+dy[i],w=col[nx][ny]!=col[x][y];
            if(col[x][y]<0||dis[nx][ny]<=dis[x][y]+w)continue;
            dis[nx][ny]=dis[x][y]+w;//更新到达新位置的操作次数
            w?dque.push_back({nx,ny}):dque.push_front({nx,ny});
        }
    }
    return ret;
}
int main(){
    cin>>n>>m;
    memset(col,-1,sizeof(col));
    char ch;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>ch,col[i][j]=ch=='B';
    int ans=INF;
    for(int x=1;x<=n;x++)
        for(int y=1;y<=m;y++)
            if(col[x][y]!=col[x-1][y]&&col[x][y]!=col[x][y-1])
                memset(dis,INF,sizeof(dis)),ans=min(ans,bfs(x,y));
    cout<<ans;
    return 0;
}

3. SPFA

3.1 奶牛派对

寒假到了, n n n 头牛都要去参加一场在编号为 x x x 的牛的农场举行的派对,农场之间有 m m m 条有向路,每条路都有一定的长度。每头牛参加完派对后都必须回家,无论是去参加派对还是回家,每头牛都会选择最短路径,求这 n n n 头牛的最短路径(一个来回)中最长的一条路径长度。


按照朴素写法,我们可以先计算出 n − 1 n-1 n−1 个结点到 x x x 的最短距离,然后再计算出 x x x 到 n − 1 n-1 n−1 个结点的最短距离。时间复杂度 O ( n k m + k m ) O(nkm+km) O(nkm+km),显然会超时。

考虑反向图优化。由于一个结点 a a a 到 b b b 的反向图搜索最短路就相当于 b b b 到 a a a 的正向图搜索最短路。这样在主函数中,可以只搜索两次,都以派对作为起点进行搜索。时间复杂度 O ( 2 k m ) O(2km) O(2km),不会超时。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e3+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
bool in_que[MAXN];
vector<Edge>adj[2][MAXN];
int n,m,x,dis[MAXN],ans[MAXN];
void spfa(int s,int rev){
    queue<int>que;
    dis[s]=0,in_que[s]=true,que.push(s);
    while(!que.empty()){
        int u=que.front();
        in_que[u]=false,que.pop();
        for(auto[v,w]:adj[rev][u])
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                if(!in_que[v])in_que[v]=true,que.push(v);
            }
    }
}
int main(){
    cin>>n>>m>>x;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,adj[0][u].push_back({v,w}),adj[1][v].push_back({u,w});
    memset(dis,INF,sizeof(dis));
    spfa(x,0);//第一次计算所有奶牛从派对回家的最短路
    for(int i=1;i<=n;i++)ans[i]=dis[i];
    memset(dis,INF,sizeof(dis));
    spfa(x,1);//第二次计算所有奶牛从家去派对的最短路
    int mx=0;
    for(int i=1;i<=n;i++)
        ans[i]+=dis[i],mx=max(mx,ans[i]);
    cout<<mx;
    return 0;
}

3.2 营救

妈妈下班回家,街坊邻居说小明被一群陌生人强行押上了警车!妈妈丰富的经验告诉她小明被带到了 t t t 区,而自己在 s s s 区。该市有 m m m 条大道连接 n n n 个区,一条大道将两个区相连接,每个大道有一个拥挤度。小明的妈妈虽然很着急,但是不愿意拥挤的人潮冲乱了她优雅的步伐。所以请你帮她规划一条从 s s s 至 t t t 的路线,使得经过道路的拥挤度最大值最小。


SPFA 的变种,贼水。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
int n,m,s,t,dis[MAXN];
bool in_que[MAXN];
vector<Edge>adj[MAXN];
void spfa(int s){
    queue<int>que;
    dis[s]=0,in_que[s]=true,que.push(s);
    while(!que.empty()){
        int u=que.front();
        in_que[u]=false,que.pop();
        for(auto[v,w]:adj[u])
            if(dis[v]>max(dis[u],w)){
                dis[v]=max(dis[u],w);
                if(!in_que[v])in_que[v]=true,que.push(v);
            }
    }
}
int main(){
    cin>>n>>m>>s>>t;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,adj[u].push_back({v,w}),adj[v].push_back({u,w});
    memset(dis,INF,sizeof(dis));
    spfa(s);
    cout<<dis[t];
    return 0;
}

3.3 航空路线

Bessie 对她农场那寒冷的天气感到厌烦,于是她准备坐飞机到一个更温暖的地方度假。不幸的是,她发现只有一个航空公司:Air Bovinia,愿意卖票给牛......并且这些票的结构有些复杂。Air Bovinia 拥有 N N N( 1 ≤ N ≤ 1000 1≤N≤1000 1≤N≤1000)条航线,每条航线都经过两个及以上的城市。举个例子:某航线的一次航班可以从城市 1 1 1 出发,然后飞往城市 5 5 5,再飞到城市 2 2 2,最后飞到城市 8 8 8。任何城市都不会在同一条航线上出现多次。如果 Bessie 选择了一条航线,那么她可以从航线上的任意一个城市上飞机,然后在途中任意一个城市下飞机。她不必从航线的起点上飞机,再从航线的终点下飞机。每条航线都有一个确定的花费,只要它搭乘了这个航班,她就必须支付这个航班的全额花费,不论她途经了几个城市。如果 Bessie 多次搭乘了某个航班,那么每次搭乘 Bessie 都必须支付航班的花费。Bessie 想要找到从她农场所在的城市(城市 A A A)到她目的地所在城市(城市 B B B)最便宜的路线。请你告诉她最少要花多少钱,并告诉她在此基础上她最少经过多少个城市(坐飞机经过也算)。


抓住最后要求的重点:(1)要花多少钱;(2)要最少经过多少个城市。显然这是多关键字排序的经典题目。多关键字排序,即先比较第一关键字(钱数),若第一关键字相同,则比较第二关键字(城市数)。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+8;
const int MAXL=1e2+8;
const long long INF=0x3f3f3f3f3f3f3f3f;
struct Edge{
    int v;
    long long w1,w2;//w1:花费 w2:经过城市个数
};
bool in_que[MAXN];
long long dis[MAXN];
vector<Edge>adj[MAXN];
int n,s,t,cty[MAXL],sum[MAXN];
/**
 * cty[]: 每次航班的花费
 * dis[]: 起点到每个城市的最小花费
 * sum[]: 起点到每个城市的最少经过城市数量
**/
void spfa(int s){
    queue<int>que;
    dis[s]=0,sum[s]=0,in_que[s]=true,que.push(s);
    while(!que.empty()){
        int u=que.front();
        in_que[u]=false,que.pop();
        for(auto[v,w1,w2]:adj[u])
            if(dis[v]>dis[u]+w1||(dis[v]==dis[u]+w1&&sum[v]>sum[u]+w2)){//按照关键字优先级更新
                dis[v]=dis[u]+w1,sum[v]=sum[u]+w2;
                if(!in_que[v])in_que[v]=true,que.push(v);
            }
    }
}
int main(){
    cin>>s>>t>>n;
    for(int i=1,c,m;i<=n;i++){
        cin>>c>>m;
        for(int j=1;j<=m;j++){
            cin>>cty[j];
            for(int k=1;k<j;k++)
                adj[cty[k]].push_back({cty[j],c,j-k});
        }
    }
    memset(dis,INF,sizeof(dis));
    spfa(s);
    if(dis[t]>=INF)cout<<"-1 -1";
    else cout<<dis[t]<<" "<<sum[t];
    return 0;
}

3.4 牛奶管道

农民约翰的农场有一套老旧的管网,管网由 M M M 条管道( 1 ≤ M ≤ 500 1≤M≤500 1≤M≤500)构成,用于将牛奶从谷仓运到储奶罐。他想在明年移除和更新大部分管道,但他想原封不动地保留一条完整的路径,这样他仍然可以把牛奶从谷仓输送到储罐。管网由 N N N 个节点( 1 ≤ N ≤ 500 1≤N≤500 1≤N≤500)组成,每个点都可以作为一组管道的端点。结点 1 1 1 是谷仓,结点 N N N 是储罐。 M M M 条双向管道中的每一条都连接一对节点,并且都有一个延迟值(牛奶达到管的另一端的用时)和容量值(单位时间内可以稳定通过管道的牛奶量)。多条管道可以连接同一对节点。对于一条连接谷仓与储罐的路径,路径的延迟等于沿途所有管道的延迟之和,路径的容量等于沿途管道最小的容量(因为这是制约牛奶运送的"瓶颈")。如果约翰通过一条延迟为 L L L、容量为 C C C 的管道运送 X X X 个单位的牛奶,需要的时间为 L + X C L+\frac{X}{C} L+CX。给出约翰的管网结构,请帮助他选择一条路径,使得他从谷仓到储罐运送 X X X 个单位牛奶的总时间最少。


SPFA 变形。原汁原味,十分经典。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,l,c;};
bool in_que[MAXN];
vector<Edge>adj[MAXN];
int n,m,x,dis[MAXN],lmts[MAXN];
//dis[]:起点到各节点的最小延迟总和
//lmts[]:所有管道的容量
void spfa(int s,int lmt){
    queue<int>que;
    dis[s]=0,in_que[s]=true,que.push(s);
    while(!que.empty()){
        int u=que.front();
        in_que[u]=false,que.pop();
        for(auto[v,l,c]:adj[u])
            if(c>=lmt&&dis[v]>dis[u]+l){//可以通过且时间更短
                dis[v]=dis[u]+l;
                if(!in_que[v])in_que[v]=true,que.push(v);
            }
    }
}
int main(){
    cin>>n>>m>>x;
    for(int i=1,u,v,l,c;i<=m;i++){
        cin>>u>>v>>l>>c;
        adj[u].push_back({v,l,c});
        adj[v].push_back({u,l,c});
        lmts[i]=c;
    }
    int ans=INF;
    for(int i=1;i<=m;i++){
        memset(dis,INF,sizeof(dis));
        memset(in_que,false,sizeof(in_que));
        spfa(1,lmts[i]);
        ans=min(ans,dis[n]+x/lmts[i]);
    }
    cout<<ans;
    return 0;
}

4. Dijkstra

4.1 路径统计

"RP餐厅"(?)的员工素质就是不一般,在齐刷刷的算出同一个电话号码之后,就准备让 HZH,TZY 去送快餐了,他们将自己居住的城市画了一张地图,已知在他们的地图上,有 N N N 个地方,而且他们目前处在标注为 1 1 1 的小镇上,而送餐的地点在标注为 N N N 的小镇。除此之外还知道这些道路都是单向的,从小镇 I I I 到 J J J 需要花费 D[I,J] 的时间,为了更高效快捷的将快餐送到顾客手中,他们想走一条从小镇 1 1 1 到小镇 N N N 花费最少的一条路,但是他们临出发前,撞到因为在路上堵车而生气的 FYY,深受启发,不能仅知道一条路线,万一...于是,他们邀请 FYY 一起来研究起了下一个问题:这个最少花费的路径有多少条?


显然,我们需要新开一个数组 cnt[] 来存储从起点到每个节点的最短路径的数量(即输出的第二个数据)。然后分两种情况:

  • 情况 1:发现一条更短的路径到达节点 v v v(即 dis[v]>dis[u]+w
    • 更新 dis[v] 为新的最短距离。
    • 重置 cnt[v]=cnt[u](因为到达 v v v 的最短路径现在只能通过 u u u 实现,路径数量与到达 u u u 的数量相同)。
  • 情况 2:当发现一条与当前最短路径等长的路径(即 dis[v]=dis[u]+w
    • 累加 cnt[v]+=cnt[u](到达 v v v 的最短路径又多了 cnt[u] 条,这些路径是通过 u u u 到达 v v v 的)。
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
bool vis[MAXN];
vector<Edge>adj[MAXN];
int n,m,dis[MAXN],cnt[MAXN];
void dijkstra(int s){
    priority_queue<pair<int,int> >pq;
    dis[s]=0,cnt[s]=1,pq.push({0,s});
    while(!pq.empty()){
        auto u=pq.top().second;
        pq.pop();
        if(vis[u])continue;
        vis[u]=true;
        for(auto[v,w]:adj[u])
            if(dis[v]>dis[u]+w)
                dis[v]=dis[u]+w,cnt[v]=cnt[u],pq.push({-dis[v],v});
            else if(dis[v]==dis[u]+w)
                cnt[v]+=cnt[u];
    }
}
int main(){
    cin>>n>>m;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,adj[u].push_back({v,w});
    memset(dis,INF,sizeof(dis));
    dijkstra(1);
    if(dis[n]==INF)cout<<"No answer";
    else cout<<dis[n]<<" "<<cnt[n];
    return 0;
}

4.2 平面上的点

给定平面上的 n n n 个点,定义 ( x 1 , y 1 ) → ( x 2 , y 2 ) (x_1,y_1)\rightarrow(x_2,y_2) (x1,y1)→(x2,y2) 的费用为它们的最小距离分量,求从 1 1 1 号点走到 n n n 号点的最小费用。


很显然,有一个 O ( n 2 ) O(n^2) O(n2) 的建图方法:

cpp 复制代码
for(int u=1;u<=n;u++)
    for(int v=u+1;v<=n;v++){
        adj[u].push_back({v,min(abs(p[u].x-p[v].x),abs(p[u].y-p[v].y))});
        adj[v].push_back({u,min(abs(p[u].x-p[v].x),abs(p[u].y-p[v].y))});
    }

但可以考虑投影优化。

首先,将平面上的点 ( x , y ) (x, y) (x,y) 投射到 x x x 轴上,得到投影点 ( x , 0 ) (x, 0) (x,0),两点的 x x x 轴投影距离为 ∣ x 1 − x 2 ∣ |x_1 - x_2| ∣x1−x2∣(即横向距离);然后,将平面上的点 ( x , y ) (x, y) (x,y) 投射到 y y y 轴上,得到投影点 ( 0 , y ) (0, y) (0,y),两点的 y y y 轴投影距离为 ∣ y 1 − y 2 ∣ |y_1 - y_2| ∣y1−y2∣(即纵向距离)。

接下来,利用投影的相邻性质,即在同一投影轴上(如 x x x 轴),任意两个非相邻投影点的距离,等于它们之间所有相邻投影点的距离之和。

然后,再利用 Dijkstra 等其他最短路算法的性质,我们知道,每次在计算起点到当前节点的距离的时候,都会处理出真正的最短距离。因此我们只需要把 ∣ x 1 − x 2 ∣ |x_1-x_2| ∣x1−x2∣ 和 ∣ y 1 − y 2 ∣ |y_1-y_2| ∣y1−y2∣ 一起放入邻接矩阵即可。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
struct Point{int x,y,idx;}p[MAXN];
bool vis[MAXN];
int n,m,s,t,dis[MAXN];
vector<Edge>adj[MAXN];
bool cmpx(Point p1,Point p2){return p1.x<p2.x;}
bool cmpy(Point p1,Point p2){return p1.y<p2.y;}
void dijkstra(int s){
    priority_queue<pair<int,int> >pq;//{-dis[u],u}
    dis[s]=0,pq.push({0,s});
    while(!pq.empty()){
        auto u=pq.top().second;
        pq.pop();
        if(vis[u])continue;
        vis[u]=true;
        for(auto[v,w]:adj[u])
            if(dis[v]>dis[u]+w)
                dis[v]=dis[u]+w,pq.push({-dis[v],v});
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>p[i].x>>p[i].y,p[i].idx=i;
    sort(p+1,p+n+1,cmpx);
    for(int i=1;i<n;i++){
        adj[p[i].idx].push_back({p[i+1].idx,p[i+1].x-p[i].x});
        adj[p[i+1].idx].push_back({p[i].idx,p[i+1].x-p[i].x});
    }
    sort(p+1,p+n+1,cmpy);
    for(int i=1;i<n;i++){
        adj[p[i].idx].push_back({p[i+1].idx,p[i+1].y-p[i].y});
        adj[p[i+1].idx].push_back({p[i].idx,p[i+1].y-p[i].y});
    }
    memset(dis,INF,sizeof(dis));
    dijkstra(1);
    cout<<dis[n];
    return 0;
}

4.3 邀请函

在电视时代,没有多少人观看戏剧表演。 剧场老板意识到了这一事实,他想宣传剧院,尤其是古典的喜剧片。

他已经打印了邀请函和所有必要的信息和计划。许多学生被雇来分发这些邀请函。每个学生被指定了一个确切的公共汽车站,他或她将留在那里一整天,邀请人们观看戏剧表演。

这里的公交系统是非常特殊的:共有 n n n 个站点和 m m m 个线路,所有的线路都是单向的,连接两个站点。公共汽车离开起始点,到达目的地之后又空车返回起始点。

学生每天早上从总部所在的 1 1 1 号站点出发,乘公交车到一个预定的站点邀请乘客。每个站点都被安排了一名学生。在一天结束的时候,所有的学生都回到总部。现在需要知道的是,学生所需的公交费用的总和最小是多少。


见 3.1 奶牛派对(就在本篇博客)。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+8;
const int INF=0x3f3f3f3f;
struct Edge{int v,w;};
bool vis[MAXN];
vector<Edge>adj[2][MAXN];
int n,m,dis[MAXN],ans[MAXN];
void dijkstra(int s,int rev){
    memset(dis,INF,sizeof(dis));
    memset(vis,false,sizeof(vis));
    priority_queue<pair<int,int> >pq;
    dis[s]=0,pq.push({0,s});
    while(!pq.empty()){
        auto u=pq.top().second;
        pq.pop();
        if(vis[u])continue;
        vis[u]=true;
        for(auto[v,w]:adj[rev][u])
            if(dis[v]>dis[u]+w)
                dis[v]=dis[u]+w,pq.push({-dis[v],v});
    }
}
int main(){
    cin>>n>>m;
    for(int i=1,u,v,w;i<=m;i++)
        cin>>u>>v>>w,adj[0][u].push_back({v,w}),adj[1][v].push_back({u,w});
    dijkstra(1,0);
    for(int i=1;i<=n;i++)ans[i]=dis[i];
    dijkstra(1,1);
    long long ret=0;
    for(int i=1;i<=n;i++)
        ans[i]+=dis[i],ret+=ans[i];
    cout<<ret;
    return 0;
}
相关推荐
hope_wisdom42 分钟前
C/C++数据结构之单向链表
c语言·数据结构·c++·链表·单向链表
西猫雷婶2 小时前
python学智能算法(二十六)|SVM-拉格朗日函数构造
人工智能·python·算法·机器学习·支持向量机
jstart千语3 小时前
【力扣】第42题:接雨水
算法·leetcode·职场和发展
墨染点香3 小时前
LeetCode 刷题【10. 正则表达式匹配】
算法·leetcode·职场和发展
cookqq3 小时前
mongodb源代码分析createCollection命令由create.idl变成create_gen.cpp过程
数据库·算法·mongodb·nosql
clock的时钟3 小时前
数据结构-线性表顺序表示
数据结构·算法
WanderInk4 小时前
深入解析:Java Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); 一行代码的背后功力
java·后端·算法
এ᭄画画的北北4 小时前
力扣-70.爬楼梯
算法·leetcode
nako_sayuri4 小时前
二分查找:区间内查询数字的频率
数据结构·算法
404未精通的狗5 小时前
(数据结构)线性表(中):SLIst单链表
c语言·数据结构·链表