【图论基础】习题集锦

目录

1.最小生成树

[1.1 最小生成树【模板】](#1.1 最小生成树【模板】)

Prim算法

邻接矩阵存图

vector数组存图

Kruskal算法

[1.2 买礼物(非联通图)](#1.2 买礼物(非联通图))

[1.3 繁忙的都市(瓶颈生成树)](#1.3 繁忙的都市(瓶颈生成树))

[1.4 滑雪(有向图)](#1.4 滑雪(有向图))

[2. 拓扑排序](#2. 拓扑排序)

[2.1 【模板】拓扑排序/家谱树](#2.1 【模板】拓扑排序/家谱树)

[2.2 摄像头](#2.2 摄像头)

[2.3 最大食物链计数(拓扑排序+路径类dp)](#2.3 最大食物链计数(拓扑排序+路径类dp))

[2.4 杂务 (拓扑排序+递推)](#2.4 杂务 (拓扑排序+递推))

[3. 单源最短路](#3. 单源最短路)

[3.1 【模板】单源最短路径(弱化版)](#3.1 【模板】单源最短路径(弱化版))

朴素Dijkstra

[3.2 【模板】单源最短路径(标准版)](#3.2 【模板】单源最短路径(标准版))

优先级队列优化Dijkstra

[bellman-ford(BF) 算法(负权处理)](#bellman-ford(BF) 算法(负权处理))

[spfa(Shortest Path Faster Algorithm) 算法(BF优化)](#spfa(Shortest Path Faster Algorithm) 算法(BF优化))

[3.3 【模板】负环](#3.3 【模板】负环)

[bf 算法](#bf 算法)

[spfa 算法](#spfa 算法)

[3.4 单源最短路总结](#3.4 单源最短路总结)

[3.5 邮递员送信(Dijkstra+建反图)](#3.5 邮递员送信(Dijkstra+建反图))

[3.6 采购特价商品(无向图+坐标图)](#3.6 采购特价商品(无向图+坐标图))

[3.7 拉近距离(负环)](#3.7 拉近距离(负环))

[3.8 最短路计数(重边处理+dp)](#3.8 最短路计数(重边处理+dp))

插曲:最小边权相同的重边是否保留问题?

BFS实现

Dijkstra实现

[4. 多源最短路](#4. 多源最短路)

[4.1 【模板】Floyd](#4.1 【模板】Floyd)

[4.2 Clear And Present Danger S](#4.2 Clear And Present Danger S)

[4.3 灾后重建](#4.3 灾后重建)

[4.4 无向图的最小环问题](#4.4 无向图的最小环问题)


1.最小生成树

最小生成树一般基于无向图的场景,特殊情况下可以接入有向图

1.1 最小生成树【模板】

P3366 【模板】最小生成树 - 洛谷

代码实现如下:

Prim算法

核心:不断加点

时间复杂度n^2

邻接矩阵存图
cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N=5010,M=2e5+10,INF=0x3f3f3f3f;

int n,m;
int dist[N],edge[N][N];//邻接矩阵存图
bool st[N];

int prime()
{
    memset(dist,INF,sizeof(dist));

    dist[1]=0;
    int ret=0;
    for (int i=1;i<=n;++i)//循环加入n个点
    {
        //1.找最近点,必须满足:(1)未加入生成树(2)距离最小
        int t=0;
        for (int j=1;j<=n;++j)
            if (!st[j]&&dist[j]<dist[t])
                t=j;

        //判断是否联通,不连通说明存在某个点不能与其他点相连
        if (dist[t]==INF)return INF;

        ret+=dist[t];
        st[t]=true;//别忘了标记

        //2.更新该点到能接触到的点的最小距离
        for (int j=1;j<=n;++j)
            dist[j]=min(dist[j],edge[t][j]);
    }
    return ret;
}

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    memset(edge,INF,sizeof(edge));
    while(m--)
    {
        int u,v,w;
        cin>>u>>v>>w;
        edge[u][v]=edge[v][u]=min(edge[u][v],w);
    }

    int ans=prime();
    if (ans==INF)cout<<"orz"<<endl;
    else cout<<ans<<endl;

    return 0;
}
vector数组存图
cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N=5010,M=2e5+10,INF=0x3f3f3f3f;
typedef pair<int,int> PII;

int n,m;
vector<PII> edge[N];//vector数组存图
int dist[N];
bool st[N];

int prime()
{
    memset(dist,INF,sizeof(dist));

    dist[1]=0;
    int ret=0;
    for (int i=1;i<=n;++i)//循环加入n个点
    {
        //1.找最近点
        int t=0;
        for (int j=1;j<=n;++j)
            if (!st[j]&&dist[j]<dist[t])
                t=j;

        //判断是否联通
        if (dist[t]==INF)return INF;
        ret+=dist[t];
        st[t]=true;//别忘了标记

        //2.更新该点到能接触到的点的最小距离
        for (auto&[v,w]:edge[t])
        {
            dist[v]=min(dist[v],w);
        }
    }
    return ret;
}

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    while(m--)
    {
        int u,v,w;
        cin>>u>>v>>w;
        //不用考虑重边,因为在prim中我们取min
        edge[u].push_back({v,w});
        edge[v].push_back({u,w});
    }

    int ans=prime();
    if (ans==INF)cout<<"orz"<<endl;
    else cout<<ans<<endl;

    return 0;
}

Kruskal算法

核心:不断加边

由于只和边相关,所以我们不用考虑建图,时间复杂度mlogm

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N=5010,M=2e5+10,INF=0x3f3f3f3f;

struct node
{
    int u,v,w;
}a[M];

int n,m,fa[N];//基于并查集
bool cmp(const node &a,const node &b)
{
    return a.w<b.w;
}
int find(int x)
{
    return x==fa[x]?x:fa[x]=find(fa[x]);
}
int kruskal()
{
    sort(a+1,a+m+1,cmp);

    int cnt=0;
    int ret=0;
    for (int i=1;i<=m;++i)
    {
        int u=a[i].u,v=a[i].v,w=a[i].w;
        int fu=find(u),fv=find(v);
        if (fu!=fv)
        {
            ++cnt;
            ret+=w;
            fa[fu]=fv;
        }
    }
    return cnt==n-1?ret:INF;
}


int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;

    for (int i=1;i<=m;++i)cin>>a[i].u>>a[i].v>>a[i].w;
    //初始化并查集
    for (int i=1;i<=n;++i)fa[i]=i;

    int ans=kruskal();
    if (ans==INF)cout<<"orz"<<endl;
    else cout<<ans<<endl;

    return 0;
}

1.2 买礼物(非联通图)

P1194 买礼物 - 洛谷

代码实现如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N=500*500+10;

int a,n,fa[N];
struct node
{
    int u,v,w;
}e[N];

int find(int x)
{
    return x==fa[x]?x:fa[x]=find(fa[x]);
}

bool cmp(node a,node b)
{
    return a.w<b.w;
}
int pos,cnt,ret;
void kruskal()
{
    sort(e+1,e+pos+1,cmp);

    for (int i=1;i<=pos;++i)
    {
        int u=e[i].u,v=e[i].v,w=e[i].w;
        int fu=find(u),fv=find(v);
        if (fu!=fv)
        {
            ++cnt;//可能为非联通,标记树的个数
            ret+=w;
            fa[fu]=fv;
        }
    }
}
int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>a>>n;

    for (int i=1;i<=n;++i)fa[i]=i;

    for (int i=1;i<=n;++i)
    {
        for (int j=1;j<=n;++j)
        {
            int k;cin>>k;
            //只存有效边
            if (i>=j||k>a||k==0)continue;
            ++pos;
            e[pos].u=i;e[pos].v=j,e[pos].w=k;
        }
    }

    kruskal();
    cout<<(n-cnt)*a+ret<<endl;
    return 0;
}

1.3 繁忙的都市(瓶颈生成树)

P2330 [SCOI2005] 繁忙的都市 - 洛谷

最小生成树即为题意要求的瓶颈生成树(最大边权最小的生成树),然后kruskal中维护边权最大值即可。

代码实现如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N=5010,M=2e5+10,INF=0x3f3f3f3f;

struct node
{
    int u,v,w;
}a[M];

int n,m,fa[N];//基于并查集
bool cmp(const node &a,const node &b)
{
    return a.w<b.w;
}
int find(int x)
{
    return x==fa[x]?x:fa[x]=find(fa[x]);
}

int ans;
void kruskal()
{
    sort(a+1,a+m+1,cmp);

    for (int i=1;i<=m;++i)
    {
        int u=a[i].u,v=a[i].v,w=a[i].w;
        int fu=find(u),fv=find(v);
        if (fu!=fv)
        {
            ans=max(ans,w);
            fa[fu]=fv;
        }
    }
}


int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    cout<<n-1<<" ";
    for (int i=1;i<=m;++i)cin>>a[i].u>>a[i].v>>a[i].w;
    //初始化并查集
    for (int i=1;i<=n;++i)fa[i]=i;

    kruskal();
    cout<<ans<<endl;

    return 0;
}

1.4 滑雪(有向图)

P2573 [SCOI2012] 滑雪 - 洛谷

同样是最小生成树问题,本题选择kruskal算法解决。

不同以往,为符合题意回溯的特性(1为起点,也就是说要从1开始访问所有能到达的点),选边得先考虑去往高度更高的点 再考虑权重,这样从1开始代码往后运行,高到低的那条路径的高点,一定能和1相连(直接或间接),这样才能符合从1开始这个设定。(见如上例子,正确权重和为7,非2)

如此道路才是以1为根,由高到低,且以最低权重和访问所有点的。

代码实现如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
#define endl '\n'
const int N=1e5+10,M=2e6+10;

int n,m,fa[N],h[N];
vector<PII> edges[N];//建图

struct node
{
    int u,v,w;
}e[M];

int find(int x)
{
    return x==fa[x]?x:fa[x]=find(fa[x]);
}

ll pos,cnt,ret;
bool vis[N];//防止重复计数
void dfs(int u)
{
    ++cnt;
    vis[u]=true;
    for (auto&[v,w]:edges[u])
    {
        //同时创建边集
        ++pos;
        e[pos].u=u,e[pos].v=v,e[pos].w=w;
        if (!vis[v])dfs(v);
    }
}

bool cmp(node a,node b)
{
    //先考虑高度,再考虑权重
    int v1=a.v,v2=b.v;
    if (h[v1]!=h[v2])return h[v1]>h[v2];//先去往更高的
    return a.w<b.w;
}

void kruskal()
{
    sort(e+1,e+pos+1,cmp);

    for (int i=1;i<=pos;++i)
    {
        int u=e[i].u,v=e[i].v,w=e[i].w;
        int fu=find(u),fv=find(v);
        if (fu!=fv)
        {
            ret+=w;
            fa[fu]=fv;
        }
    }
}
int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    for (int i=1;i<=n;++i)cin>>h[i];

    for (int i=1;i<=n;++i)fa[i]=i;

    for (int i=1;i<=m;++i)
    {
        int u,v,w;
        cin>>u>>v>>w;
        //建图,只能从高到低滑,相等时是双向边,所以两个if
        if (h[u]>=h[v])edges[u].push_back({v,w});
        if (h[v]>=h[u])edges[v].push_back({u,w});
    }

    dfs(1);//找出从1能访问到的全部节点,同时存边以用kruskal
    cout<<cnt<<" ";
    kruskal();
    cout<<ret<<endl;

    return 0;
}

2. 拓扑排序

不同于最小生成树,拓扑排序是基于有向图的算法

实际上就是BFS+入度数组

2.1 【模板】拓扑排序/家谱树

B3644 【模板】拓扑排序 / 家谱树 - 洛谷

代码实现如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const int N=1010;

int n;

void solve()
{
    cin>>n;
    vector<int> in(n+1);
    unordered_map<int,vector<int>> comp;

    int i=1;
    while(i<=n)
    {
        int x;
        while (cin>>x)
        {
            if (x==0)break;
            ++in[x];
            comp[i].push_back(x);
        }
        ++i;
    }

    queue<int> q;
    for(int i=1;i<=n;i++)
    {
        if (in[i]==0)q.push(i);
    }

    vector<int> ans;
    while(!q.empty())
    {
        int sz=q.size();
        while(sz--)
        {
            int x=q.front();
            q.pop();
            ans.push_back(x);
            for (int a:comp[x])
            {
                if (--in[a]==0)q.push(a);
            }
        }
    }
    for (int i=0;i<n;i++)cout<<ans[i]<<" ";
    cout<<endl;
}

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    solve();

    return 0;
}

2.2 摄像头

P2712 摄像头 - 洛谷

代码实现如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=510;
int n,in[N];
vector<int> edges[N];
bool vis[N];//标记摄像头


int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    //建图
    cin>>n;
    for(int i=0;i<n;i++)
    {
        int x,m,y;
        cin>>x>>m;
        vis[x]=true;
        while (m--)
        {
            cin>>y;
            ++in[y];
            edges[x].push_back(y);
        }
    }

    queue<int> q;
    for(int i=0;i<N;i++)
    {
        if (vis[i]&&in[i]==0)q.push(i);
    }

    while(!q.empty())
    {
        int sz=q.size();
        while(sz--)
        {
            int x=q.front();
            q.pop();
            for (auto i:edges[x])
            {
                if (vis[i]&&--in[i]==0)q.push(i);
            }
        }
    }

    int ans=0;
    for(int i=0;i<N;i++)
    {
        if (vis[i]&&in[i]!=0)++ans;
    }
    
    if(ans==0)cout<<"YES"<<endl;
    else cout<<ans<<endl;

    return 0;
}

2.3 最大食物链计数(拓扑排序+路径类dp)

P4017 最大食物链计数 - 洛谷

依照先前路径类dp问题的启发,这题也可以转换成求抵达最终点的路径条数

利用拓扑排序确定填表顺序

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=5010,MOD=80112002;
int n,m,in[N],out[N],f[N];
vector<int> edges[N];

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    //建图
    cin>>n>>m;
    while (m--)
    {
        int u,v;
        cin>>u>>v;
        edges[u].push_back(v);
        ++in[v],++out[u];
    }

    queue<int> q;
    for(int i=1;i<=n;i++)
    {
        if (in[i]==0)
        {
            f[i]=1;//初始化
            q.push(i);
        }
    }

    while(!q.empty())
    {
        int sz=q.size();
        while(sz--)
        {
            int u=q.front();
            q.pop();
            for (auto v:edges[u])
            {
                f[v]=(f[v]+f[u])%MOD;
                if (--in[v]==0)q.push(v);
            }
        }
    }

    int ans=0;
    for(int i=1;i<=n;i++)
    {
        //出度为0的点是食物链的终点
        if (out[i]==0)ans=(ans+f[i])%MOD;
    }

    cout<<ans<<endl;

    return 0;
}

2.4 杂务 (拓扑排序+递推)

P1113 杂务 - 洛谷

代码实现如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=1e4+10;
int n,m,in[N],len[N],f[N];//f[i]表示做完i需要的最大时长(最大保证在这之前能处理完全部准备事务)
vector<int> edges[N];

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    //建图
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int v,u;
        cin>>v>>len[v];
        while (cin>>u&&u)
        {
            edges[u].push_back(v);
            ++in[v];
        }
    }

    queue<int> q;
    for(int i=1;i<=n;i++)
    {
        if (in[i]==0)
        {
            q.push(i);
        }
    }

    int ans=0;
    while(!q.empty())
    {
        int sz=q.size();
        while(sz--)
        {
            int u=q.front();
            q.pop();

            f[u]+=len[u];
            ans=max(ans,f[u]);

            for (auto v:edges[u])
            {
                //初始化f[v],保证做v活动之前,为了做v,最耗时间的做完了
                f[v]=max(f[v],f[u]);
                if (--in[v]==0)q.push(v);
            }
        }
    }
    

    cout<<ans<<endl;

    return 0;
}

3. 单源最短路

3.1 【模板】单源最短路径(弱化版)

P3371 【模板】单源最短路径(弱化版) - 洛谷

代码实现如下:

朴素Dijkstra

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

typedef pair<int, int> PII;
const int N=1e4+10,INF=2147483647;

int n,m,s;
vector<PII> edges[N];

int dist[N];
bool vis[N];

void dijsktra()
{
    //初始化
    for (int i=0;i<=n;++i)dist[i]=INF;
    dist[s]=0;
    //将剩余n-1个点加入路径
    for (int i=1;i<n;++i)
    {
        //1.找出没有确定最短路的一系列点中,当前最短路最小的点
        int t=0;
        for (int j=1;j<=n;++j)
            if (!vis[j]&&dist[j]<dist[t])
                t=j;

        //2.打上标记,然后依据该点松弛
        vis[t]=true;
        for (auto&[v,w]:edges[t])
        {
            if (dist[t]+w<dist[v])
            {
                dist[v]=dist[t]+w;
            }
        }
    }

    for (int i=1;i<=n;++i)cout<<dist[i]<<" ";
}

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m>>s;
    while (m--)
    {
        int u,v,w;
        cin>>u>>v>>w;
        edges[u].push_back({v,w});
    }

    //dijsktra算法找最短路
    dijsktra();
    
    return 0;
}

3.2 【模板】单源最短路径(标准版)

P4779 【模板】单源最短路径(标准版) - 洛谷

优先级队列优化Dijkstra

使用优先级队列就不用遍历去找 当前最短路 距离最小的点了.

代码参考如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

typedef pair<int, int> PII;
const int N=1e5+10;

int n,m,s;
vector<PII> edges[N];//{距离,节点}

int dist[N];
bool vis[N];//标记已经确定最短路的节点
//小根堆
priority_queue<PII, vector<PII>, greater<PII> > heap;

void dijkstra()
{
    //初始化
    memset(dist,0x3f,sizeof(dist));
    dist[s]=0;
    heap.push({0,s});

    while(!heap.empty())
    {
        int u=heap.top().second;
        heap.pop();
        if (vis[u])continue;
        vis[u]=true;

        for (auto[v,w] : edges[u])
        {
            if(dist[v]>dist[u]+w)
            {
                dist[v]=dist[u]+w;
                heap.push({dist[v],v});
            }
        }
    }

    for (int i=1;i<=n;++i)cout<<dist[i]<<" ";
}
int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m>>s;
    for(int i=0;i<m;i++)
    {
        int u,v,w;cin>>u>>v>>w;
        edges[u].push_back({v,w});
    }

    dijkstra();
    return 0;
}

bellman-ford(BF) 算法(负权处理)

Dijkstra在处理有负权的最短路问题时会出错,这时可以用bellman-ford 算法来解决.

bellman-ford 算法是⼀种基于松弛操作的最短路算法,可以求出++有负权++的图的最短路,并可以对最短路不存在(负环)的情况进⾏判断。

算法核⼼思想:不断尝试对图上每⼀条边进⾏松弛,直到所有的点都⽆法松弛为⽌。

代码参考如下(会超时):

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

typedef pair<int, int> PII;
const int N=1e5+10;

// struct Node
// {
//     int u,v,w;
// }e[N];
int n,m,s,dist[N];
vector<PII> edges[N];

void bf()
{
    memset(dist,0x3f,sizeof(dist));
    dist[s]=0;

    for (int i=1;i<n;++i)
    {
        bool flag=true;
        for (int u=1;u<=n;++u)
        {
            for (auto[v,w]:edges[u])
            {
                if (dist[v]>dist[u]+w)
                {
                    dist[v]=dist[u]+w;
                    flag=false;
                }
            }
        }
        if (flag)break;
    }

    for (int i=1;i<=n;++i)cout<<dist[i]<<" ";
}

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m>>s;
    for(int i=0;i<m;i++)
    {
        int u,v,w;cin>>u>>v>>w;
        edges[u].push_back({v,w});
    }

    bf();

    return 0;
}

spfa(Shortest Path Faster Algorithm) 算法(BF优化)

本质就是用队列对BF进行优化

代码参考如下(同样会超时,所以没有负权优先Dijkstra算法):

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

typedef pair<int, int> PII;
const int N=1e5+10;

// struct Node
// {
//     int u,v,w;
// }e[N];
int n,m,s,dist[N];
vector<PII> edges[N];
bool st[N];

void spfa()
{
    memset(dist,0x3f,sizeof(dist));
    dist[s]=0;
    st[s]=true;
    queue<int> q;
    q.push(s);

    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        st[u]=false;
        for (auto[v,w]:edges[u])
        {
            if (dist[v]>dist[u]+w)
            {
                dist[v]=dist[u]+w;
                if (!st[v])
                {
                    st[v]=true;
                    q.push(v);
                }
            }
        }
    }
    
    for (int i=1;i<=n;++i)cout<<dist[i]<<" ";
}

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m>>s;
    for(int i=0;i<m;i++)
    {
        int u,v,w;cin>>u>>v>>w;
        edges[u].push_back({v,w});
    }

    spfa();

    return 0;
}

3.3 【模板】负环

P3385 【模板】负环 - 洛谷

bf 算法

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=2e3+10,M=3e3+10;

struct Node
{
    int u,v,w;
}e[M*2];
int t,pos,n,m,dist[N];


bool bf()
{
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;


    for (int i=1;i<=n;++i)//n-1轮是最大的松弛操作次数,再跑一遍第n轮,如果还能松弛就是有负环
    {
        bool flag=true;
        for (int j=1;j<=pos;++j)
        {
            int u=e[j].u,v=e[j].v,w=e[j].w;
            if (dist[u]==0x3f3f3f3f)continue;//还没更新这个点的时候不能考虑
            if (dist[u]+w<dist[v])
            {
                flag=false;
                dist[v]=dist[u]+w;
            }
        }
        //没有进行松弛操作->存在最短路->不存在负环
        if (flag)return false;
    }

    return true;
}

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        pos=0;//清空!!!
        for(int i=0;i<m;i++)
        {
            int u,v,w;cin>>u>>v>>w;
            ++pos;
            e[pos].u=u,e[pos].v=v,e[pos].w=w;
            if (w>=0)
            {
                ++pos;
                e[pos].u=v,e[pos].v=u,e[pos].w=w;
            }
        }

        if (bf())cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
    return 0;
}

spfa 算法

存在负环时,spfa算法会死循环

此时用cnt[i]标记从起点到达 i 位置经过多少条边即可,cnt[i]上限为n-1,超过n-1就是有负环。

代码实现如下:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long

typedef pair<int,int> PII;
const int N=2e3+10;

int t,pos,n,m,dist[N];
vector<PII> edges[N];
bool st[N];//标记哪些点在队列中
int cnt[N];//判断负环

bool spfa()
{
    memset(dist,0x3f,sizeof(dist));
    memset(st,false,sizeof(st));
    memset(cnt,0,sizeof(cnt));

    dist[1]=0;
    st[1]=true;
    queue<int> q;
    q.push(1);

    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        st[u]=false;

        if (dist[u]==0x3f3f3f3f)continue;
        for (auto[v,w] : edges[u])
        {
            if (dist[v]>dist[u]+w)
            {
                dist[v]=dist[u]+w;

                cnt[v]=cnt[u]+1;
                if (cnt[v]>=n)return true;

                if (!st[v])
                {
                    st[v]=true;
                    q.push(v);
                }
            }
        }
    }
    return false;
}
int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        //多组数据别忘了清空
        for (int i=1;i<=n;++i)edges[i].clear();

        while(m--)
        {
            int u,v,w;cin>>u>>v>>w;
            edges[u].push_back({v,w});
            if (w>=0)edges[v].push_back({u,w});
        }

        if (spfa())cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
    return 0;
}

3.4 单源最短路总结

3.5 邮递员送信(Dijkstra+建反图)

P1629 邮递员送信 - 洛谷

起点都一样->正向图求1,到任何节点的最短距离,

终点都一样->建反向图再跑一遍dijkstra就是任何节点返回1的最短距离

由于n的范围不是很大,我们用邻接矩阵存图

代码实现如下:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long

typedef pair<int,int> PII;
const int N=1e3+10;

int n,m;
int e[N][N];//邻接矩阵存图
int dist[N];
bool st[N];

void dijkstra()
{
    memset(dist,0x3f,sizeof(dist));
    memset(st,false,sizeof(st));
    dist[1]=0;
    priority_queue<PII,vector<PII>,greater<PII> > pq;
    pq.push({0,1});

    while (!pq.empty())
    {
        int u=pq.top().second;
        pq.pop();
        if (st[u])continue;
        st[u]=true;

        for (int v=1;v<=n;++v)
        {
            if (e[u][v]!=0x3f3f3f3f && dist[v]>dist[u]+e[u][v])
            {
                dist[v]=dist[u]+e[u][v];
                pq.push({dist[v],v});
            }
        }
    }
    
}
int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    memset(e,0x3f,sizeof(e));
    while (m--)
    {
        int u,v,w;
        cin>>u>>v>>w;
        e[u][v] = min(w, e[u][v]);//处理重边
    }

    dijkstra();

    int ans=0;
    for (int i=1;i<=n;++i)ans+=dist[i];

    //建反图
    for (int i=1;i<=n;++i)
        for (int j=i+1;j<=n;++j)
            swap(e[i][j],e[j][i]);

    dijkstra();

    for (int i=1;i<=n;++i)ans+=dist[i];
    cout<<ans<<endl;

    return 0;
}

3.6 采购特价商品(无向图+坐标图)

P1744 采购特价商品 - 洛谷

无向图/双向图,边数组不开两倍大小的时候,松弛时两边同时同时即可

因为bf写起来最简单,所以就写bf了

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long

const int N=110,M=1010;


int n,m,s,t,x[N],y[N];
double dist[N];
struct node
{
    int u,v;
    double w;
}e[M];//无向图,不开二倍大小也行,松弛操作时两边都松弛即可

double calc(int p1,int p2)
{
    int dx=x[p1]-x[p2];
    int dy=y[p1]-y[p2];
    return sqrt(dx*dx+dy*dy);
}
void bf()
{
    //注意初始化
    for (int i=1;i<=n;++i)dist[i]=1e10;
    dist[s]=0;

    for (int i=1;i<n;++i)
    {
        for (int j=1;j<=m;++j)
        {
            int u=e[j].u,v=e[j].v;
            double w=e[j].w;

            if (dist[v]>dist[u]+w)
            {
                dist[v]=dist[u]+w;
            }
            if (dist[u]>dist[v]+w)
            {
                dist[u]=dist[v]+w;
            }
        }
    }
}
int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n;
    for (int i=1;i<=n;++i)cin>>x[i]>>y[i];
    cin>>m;
    for (int i=1;i<=m;++i)
    {
        int point1,point2;
        cin>>point1>>point2;
        e[i].u=point1,e[i].v=point2;e[i].w=calc(point1,point2);
    }
    cin>>s>>t;
    bf();
    cout<<fixed<<setprecision(2)<<dist[t]<<endl;
    return 0;
}

3.7 拉近距离(负环)

数据范围小,直接写bf了

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e3+10,M=1e4+10;

int pos,n,m,dist[N];
struct node
{
    int u,v,w;
}e[M];

bool bf(int start)
{
    memset(dist,0x3f,sizeof(dist));
    dist[start]=0;

    for (int i=1;i<=n;++i)
    {
        bool flag=true;
        for (int j=1;j<=pos;++j)
        {
            int u=e[j].u,v=e[j].v,w=e[j].w;
            if (dist[u]==0x3f3f3f3f)continue;

            if (dist[v]>dist[u]+w)
            {
                dist[v]=dist[u]+w;
                flag=false;
            }
        }
        if (flag)return false;
    }
    return true;
}

int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    while(m--)
    {
        int u,v,w;cin>>u>>v>>w;
        ++pos;
        e[pos].u=u,e[pos].v=v,e[pos].w=-w;//存负值
    }

    //1作起点
    bool st=bf(1);
    if (st)
    {
        cout<<"Forever love"<<endl;
        return 0;
    }
    int ret=dist[n];

    //n作起点
    st=bf(n);
    if (st)
    {
        cout<<"Forever love"<<endl;
        return 0;
    }
    ret=min(ret,dist[1]);
    cout<<ret<<endl;

    return 0;
}

3.8 最短路计数(重边处理+dp)

P1144 最短路计数 - 洛谷

无向无权的图当然优先BFS了

这里的重边不能删去,因为是当作路径来考虑的,当两个点出现多条边,这几条边都是可以走的,都算一条路径。

如果边权不全相等就得保留最短的,当然如果有多条最短的都要保留。

插曲:最小边权相同的重边是否保留问题?

这里最小边权相同的重边是否保留,貌似是根据题目描述?

比如:

存在一条连接a,b的边,权值为w 和 a,b间存在一条权值为w边相连

前者可能需要保留最小边权重边,后者可能不需保留。

本题需要保留最小边权相同的边,不保留最小边权相同的边的路径统计问题链接:

P1608 路径统计 - 洛谷

回到本题,

注意每次更新状态方程的时候,需要拿出最短距离的节点,因为这里边权都相等,所以bfs,dijkstra,bf,spfa算法都是可用的,如果边权不等,那只能用dijkstra(bfs逻辑错误,bf/spfa超时)。

状态如何更新?

首先我们得确保到达v的时候是最短路,所以如果遇到更短路径直接赋值,这时就是第一次遇到该节点(bfs),赋值直接更新路径条数即可,然后纳入队列。

后续如果遇到相同小的路径+=操作即可。

注意这里是不需要bool数组标记节点状态,因为我们要统计全部可能路径,如果一个节点是多条路径的交叉点,那么这个所有到达该点的路径都要考虑,如果标记了会漏掉一些路径。在Dijkstra算法中是需要标记节点状态的,因为每次标记的节点已经百分百确定是最短路。

代码实现如下:

BFS实现

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=1e6+10,M=2e6+10,MOD=100003;

int n,m,f[N],dist[N];
vector<int> edges[N];

void bfs()
{
    f[1]=1;
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;
    queue<int> q;
    q.push(1);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for (auto v:edges[u])
        {
            if (dist[v]>dist[u]+1)
            {
                dist[v]=dist[u]+1;
                f[v]=f[u];
                q.push(v);//能更新说明是第一次遍历到v点
            }
            else if (dist[v]==dist[u]+1)
            {
                f[v]=(f[v]+f[u])%MOD;
            }

        }
    }

}
int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    while(m--)
    {
        int u,v,w;cin>>u>>v;
        edges[u].push_back(v);
        edges[v].push_back(u);
    }
    bfs();
    for (int i=1;i<=n;++i)cout<<f[i]<<endl;
    return 0;
}

Dijkstra实现

注意标记状态,以确定节点已在最短路

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

typedef pair<int,int> PII;
const int N=1e6+10,M=2e6+10,MOD=100003;

int n,m,f[N],dist[N];
vector<int> edges[N];
bool st[N];//标记已经确定最短路的节点

void dijkstra()
{
    f[1]=1;//初始化!!
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;

    priority_queue<PII,vector<PII>,greater<PII> > pq;
    pq.push({0,1});
    while(!pq.empty())
    {
        int u=pq.top().second;
        pq.pop();
        if (st[u])continue;
        st[u]=true;

        for (int v:edges[u])
        {
            if (dist[u]+1<dist[v])
            {
                dist[v]=dist[u]+1;
                f[v]=f[u];
                pq.push({dist[v],v});
            }
            else if (dist[u]+1==dist[v])
            {
                f[v]=(f[v]+f[u])%MOD;
            }
        }
    }
}
int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    while(m--)
    {
        int u,v,w;cin>>u>>v;
        edges[u].push_back(v);
        edges[v].push_back(u);
    }
    dijkstra();
    for (int i=1;i<=n;++i)cout<<f[i]<<endl;
    return 0;
}

4. 多源最短路

4.1 【模板】Floyd

B3647 【模板】Floyd - 洛谷

打表有助于理解

代码实现如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N=110;
int n,m,f[N][N];

int main()
{
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    memset(f,0x3f,sizeof(f));//初始化
    for (int i=1;i<=n;++i)f[i][i]=0;

    for (int j=1;j<=m;++j)
    {
        int u,v,w;
        cin>>u>>v>>w;
        f[u][v]=f[v][u]=min(f[u][v],w);
    }
    
    //按1~n顺序,选则插入点作为桥梁
    for (int k=1;k<=n;++k)
     for (int i=1;i<=n;++i)
        for (int j=1;j<=n;++j)
            f[i][j]=min(f[i][j],f[i][k]+f[k][j]);

    for (int i=1;i<=n;++i)
    {
        for (int j=1;j<=n;++j)
        {
            cout<<f[i][j]<<" ";
        }
        cout<<endl;
    }
    return 0;
}

4.2 Clear And Present Danger S

P2910 [USACO08OPEN] Clear And Present Danger S - 洛谷

跑一遍多源最短路算法即可

代码实现如下:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=110,M=1e4+10;

int n,m,road[M],f[N][N];

int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    for (int i=1;i<=m;++i)cin>>road[i];
    for (int i=1;i<=n;++i)
        for (int j=1;j<=n;++j)
            cin>>f[i][j];

    for (int k=1;k<=n;++k)
        for (int i=1;i<=n;++i)
            for (int j=1;j<=n;++j)
                f[i][j]=min(f[i][j],f[i][k]+f[k][j]);

    ll ans=0;
    for (int i=1;i<=m-1;++i)
    {
        int s=road[i],d=road[i+1];
        ans+=f[s][d];
    }
    cout<<ans<<endl;

    return 0;
}

4.3 灾后重建

P1119 灾后重建 - 洛谷

修复时间非递减 以及 询问t是不下降的保证了Floyd算法的可行,询问是t的就把修复时间小于等于t的点插入。

代码实现如下:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=210,INF=0x3f3f3f3f;

int n,m,q,t[N],f[N][N];

//将序号为k点插入
void floyd(int k)
{
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;
    for(int i=0;i<n;i++)cin>>t[i];

    //表格初始化
    memset(f,0x3f,sizeof(f));
    for(int i=0;i<n;i++)f[i][i]=0;

    //建图
    while(m--)
    {
        int u,v,w;cin>>u>>v>>w;
        f[u][v]=f[v][u]=min(f[u][v],w);
    }

    cin>>q;
    int index=0;
    while(q--)
    {
        int u,v,time;cin>>u>>v>>time;
        while (index<n&&t[index]<=time)floyd(index++);//插入序号为index的点

        //两个村子存在一个没修好,或者都修好了但是time时间时不连通,输出-1
        if (t[u]>time||t[v]>time||f[u][v]==INF)cout<<-1<<endl;
        else cout<<f[u][v]<<endl;
    }
    return 0;
}

4.4 无向图的最小环问题

P6175 无向图的最小环问题 - 洛谷

于是我们可以利用floyd算法,求出环编号最大为k时的最小环,环的权值和如下

在插入第k个点之前,i到j是考虑了前k-1个点当作桥梁确定i到j的最短路的,此时i到j就是最小路径,于是再添加k点即可组成编号最大为k的最小环

代码实现如下:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'

const int N=110,INF=1e8;//1e9*3超int

int n,m,e[N][N],f[N][N];

int main()
{
    std::cin.tie(NULL);
    std::cout.tie(NULL);
    std::ios::sync_with_stdio(false);

    cin>>n>>m;

    // 0x3f*3会溢出,改用1e8
    // memset(e,0x3f,sizeof(e));
    // memset(f,0x3f,sizeof(f));
    for (int i=1;i<=n;++i)
        for (int j=1;j<=n;++j)
            e[i][j]=f[i][j]=(i==j?0:INF);

    for (int i=1;i<=m;++i)
    {
        int u,v,w;cin>>u>>v>>w;
        e[u][v]=e[v][u]=f[u][v]=f[v][u]=min(e[u][v],w);
    }

    int ret=INF;
    for (int k=1;k<=n;++k)
    {
        for (int i=1;i<k;++i)
            for (int j=i+1;j<k;++j)
                ret=min(ret,f[i][j]+e[i][k]+e[k][j]);//小心溢出

        for (int i=1;i<=n;++i)
            for (int j=1;j<=n;++j)
                f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
    }

    if(ret==INF)cout<<"No solution."<<endl;
    else cout<<ret<<endl;

    return 0;
}

------------------END------------------

相关推荐
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc10 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
偷吃的耗子10 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
2013编程爱好者10 小时前
【C++】树的基础
数据结构·二叉树··二叉树的遍历
NEXT0610 小时前
二叉搜索树(BST)
前端·数据结构·面试
化学在逃硬闯CS10 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar12311 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS11 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
lanhuazui1011 小时前
C++ 中什么时候用::(作用域解析运算符)
c++
charlee4411 小时前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索