图论-学习整理

(题目来源:Acwing)

一.图的遍历Acwing847

给定一个n个点m条边的有向图,图中可能存在重边和自环。
所有边的长度都是1,点的编号为1~n。
请你求出1号点到n号点的最短距离,如果从1号点无法走到n号点,输出-1。

输入格式

第一行包含两个整数n和m。接下来m行,每行包含两个整数a和b,表示存在一条从a走到b的长度为1的边。(1≤n,m ≤ 10^{5})

输出格式

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

因为每条边的长度都是1,所以bfs一遍,即可得到1号点到n号点的最短距离

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

using namespace std;

const int N = 1e5+100,inf = 0x3f3f3f3f;

int n,m;
int dist[N];
int h[N],e[N],ne[N],idx;
bool st[N];
queue<int> q;

// 使用邻接表来存图
void add(int a,int b)
{
    e[idx] =b,ne[idx] = h[a],h[a] = idx++;
}
void bfs()
{
    memset(dist,0x3f,sizeof dist);
    
    q.push(1);
    dist[1] = 0;
    st[1] = true;
    
    while(q.size())
    {
        int t = q.front();
        q.pop();
        
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(!st[j] && dist[j] > dist[t]+1)
            {
                dist[j] = dist[t]+1;
                q.push(j);
                st[j] =true;
            }
        }
    }
}
int main()
{
    cin>>n>>m;
    // 邻接表头的初始化!!!
    memset(h,-1,sizeof h);
    while(m --)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
    }
    
    bfs();
    
    if(dist[n] == inf)  cout<<-1<<endl;
    else cout<<dist[n]<<endl;
    
    return 0;
}

二.有向图的拓扑排序(Acwing848)

给定一个n个点m条边的有向图,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出-1。
若一个由图中所有点构成的序列A满足:对于图中的每条边(x, y),x在A中都出现在y之前,则称A是该图的一个拓扑序列。

输入格式

第一行包含两个整数n和m (1≤n,m ≤ 10^{5})
接下来m行,每行包含两个整数x和y,表示存在一条从点x到点y的有向边(x, y)。

输出格式

共一行,如果存在拓扑序列,则输出拓扑序列。
否则输出-1。

一个有向无环图一定存在至少一个拓扑序列

我们可以将所有入度为0的点入队,这些点均可以作起点( 拓扑序列不唯一),每次将队头的点取出,将这个点的所有邻点的入度减1, 减完后如果该点的入度为0,将其入队,重复此过程,直到所有的点的入度都减为0,根据出队的顺序,就可以得到一个拓扑序列。

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 1e5+100;

int n,m;
int h[N],e[N],ne[N],idx;
int q[N],d[N];  //d数组用来维护某个点的入度

//使用邻接表来存图
void add(int a,int b)
{
    e[idx] = b, ne[idx] = h[a],h[a] = idx++;
}

bool bfs()
{
    int hh = 0,tt = -1;
    
    //所有入度为0的点都可以放到拓扑序列的前面
    //队列的点入度全部为0
    for(int i = 1; i<=n; i++)
        if(d[i] == 0)
            q[++tt] = i;
    
    while(hh<=tt)
    {
        int t = q[hh++];
        for(int i = h[t]; ~i; i = ne[i])
        {
            //每次取出队头,将与之相连的点的入度减1
            //如果减完后该点的入度也为1,将它入队
            int j = e[i];
            if(--d[j]==0)
                q[++tt] = j;
        }
    }
    return n-1 == tt;
}

int  main()
{
    cin>>n>>m;

    memset(h,-1,sizeof h);
    
    for(int i = 1; i<=m; i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
        //b点的入度+1
        d[b]++;
    }
    
    if(bfs())
        for(int i = 0; i<n; i++)
            cout<<q[i]<<" ";
    else
        cout<<-1<<endl;
    
    return 0;
}

三.单源最短路

1.朴素Dijkstra算法( O(n^2))(Acwing849)

给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。

输入格式

第一行包含整数n和m。(1≤n≤500, 1≤m≤10^5)
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出格式

输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。

我们用集合S来表示已经确定最短距离的点的集合,集合U表示未确定最短距离的点的集合。 开始将1号点与每个点的距离设置成正无穷,遍历1号点的每一个邻点,找到一条权重最小的边,将这个点加入到S中,用这个点来更新1号点到其他点的距离,然后再从U中找到距离1号点最短的点,将其加入S中,并用此点来更新其他点到1号点的距离,重复上述过程,直到所有点均在S中。

#include <iostream>
#include <cstring>
#include <cstdio>

using namespace std;

const int N = 510;

int n,m;
int g[N][N];
int dist[N];
bool st[N];

int dijkstra()
{
    //初始化距离
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    for(int i = 0; i<n; i++)
    {
        int t = -1;
        for(int j = 1; j<=n ; j++)
        {
            if(!st[j] && (t==-1 || dist[j]<dist[t]))
                t = j;
        }
        st[t] = true;
        //用该点来更新到其他点的距离
        for(int j = 1; j<=n ; j++)
            dist[j] = min(dist[j],dist[t]+g[t][j]);
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}
int main()
{
    cin>>n>>m;
    //稠密图,且点数较少,可用邻接矩阵来存图
    memset(g,0x3f,sizeof g);
    
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b] = min(g[a][b],c);
    }
    
    int ans = dijkstra();
    cout<<ans<<endl;
    return 0;
}

2.堆优化版的Dijkstra算法(O(mlogn))(Acwing850)

题目描述输入输出与上面一题均相同,但点数和边数有变,1≤n,m≤10^5
朴素版的Dijkstra算法时间复杂度与边数无关,所以一般可处理稠密图,而这一题很明显是稀疏图,如果用朴素版来做,时间大概是10^{10},会tle,而堆优化版的Dijkstra时间复杂度是O(mlogn),可以用来解决稀疏图。

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

using namespace std;

const int N = 1e5+100;

typedef pair<int,int>PII; //距离,编号

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

//用静态邻接表存储图
void add(int s,int to,int d)
{
    e[idx] = to,w[idx] = d,ne[idx] = head[s],head[s] = 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 distance = t.first,u = t.second;
        if(st[u]) continue;
        st[u]= true;
        
        for(int i = head[u]; ~i ;i = ne[i])
        {
            int k = e[i];
            //e[i]存储边的终点
            if(dist[k]>distance + w[i])
            {
                dist[k] = distance + w[i];
                heap.push({dist[k],k});
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
int main()
{
    cin>>n>>m;
    
    memset(head,-1,sizeof(head));
    
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    int ans = dijkstra();
    cout<<ans<<endl;
    
    return 0;
}

3.Spfa算法(Acwing851)

给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible。
数据保证不存在负权回路。

输入格式

第一行包含整数n和m。(1≤n,m≤10^5)
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出格式

输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出”impossible”。

平均情况下O(m),最坏情况下O(nm)
Spfa算法是由Bellman-ford算法(之前的一篇博客简单介绍过,戳这里)通过队列优化过的算法,用一个队列存储待优化的点,一开始先加入起点,当队列不为空时,每次取出队头,遍历该点的所有出边,如果某个邻点的距离可以更新,进行松弛操作,并将这个点加入队列中。这样不断从队列中取出点进行松弛操作,直到队列为空为止。

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

using namespace std;

const int N = 1e5+100;

typedef pair<int,int>PII; //距离,编号

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

//用静态邻接表存储图
void add(int s,int to,int d)
{
    e[idx] = to,w[idx] = d,ne[idx] = head[s],head[s] = idx++;
}

int spfa()
{
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    
    queue<int> q;
    q.push(1);
    st[1] = true;
    
    while(q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;
        
        for(int i = head[t]; ~i ;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] =false;
                }
            }
        }
    }
    
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}
int main()
{
    cin>>n>>m;
    
    memset(head,-1,sizeof(head));
    
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    int ans = spfa();
    if(ans == -1)
        puts("impossible");
    else  
        cout<<ans;
    
    return 0;
}

4.用spfa算法判断负环(Acwing852)

给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。

输入格式

第一行包含整数n和m。(1≤n≤2000,1≤m≤10000,)
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出格式

如果图中存在负权回路,则输出“Yes”,否则输出“No”。

再spfa算法中,我们每次遍历某个点的邻边时,如果可以进行松弛操作,说明dist[t]+w[i]<dist[j],而边权未负时此性质也一定满足,而且该边会进行松弛操作(每走一次负边,最短距离都会变得更短),所以我们可以利用一个cnt数组来维护起点到某个点最短路包含的边数(每次进行松弛操作时,同时更新cnt的信息),如果cnt[x]>=n,说明从起点到x点经过了n条边,等价于经过了n+1个点,但图中一共只有n个点,由抽屉原理知,一定存在相同的点,即存在环,而且该点经过松弛操作,那么这个环一定是负环。

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

using namespace std;

const int M = 1e5+100,N = 2010;

int n,m;
int e[M],w[M],head[M],ne[M],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] = head[a],head[a] = idx ++;
}

bool spfa()
{
    queue<int>q;
    //所有的顶点都入队,因为负环不一定在以1为起点的路径上
    for(int i = 1; i<=n ;i++)
    {
        q.push(i);
        st[i] = false;
    }
    
    while(q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;
        
        for( int i = head[t] ; ~i ; 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] = false;
                }
            }
        }
    }
    return false;
}
int main()
{
    cin>>n>>m;
    memset(head,-1,sizeof head);
    
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    
    bool ans = spfa();
    
    if(ans) cout<<"Yes"<<endl;
    else cout<<"No"<<endl;
    
    return 0;
}

5.Kruskal算法求最小生成树(Acwing859)

给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。

输入格式

第一行包含两个整数n和m。( 1≤n≤10^5,1≤m≤2∗10^5)
接下来m行,每行包含三个整数u,v,w,表示点u和点v之间存在一条权值为w的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

对一个具有n个顶点,m条边的图来说,我们假设边集为空,则这个图只含有n个点,也可以看做这是一个n个根节点的森林,我们将边集按照权重进行排序,枚举每一条边,如果这条边的两个顶点属于不同的连通块(树),将两个点合并到一个集合中,如果这条边的两个顶点已经在一个连通块中(树)中,则继续枚举边的过程,直到集合中已经包含n-1条边,就得到最小生成树。
查询是两个点是否属于同一连通块以及将两个点合并到一个连通块中,我们可以利用并查集来维护,在O(1)的情况下完成操作。

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

using namespace std;

const int N = 2e5+100;

int n,m;
int p[N];

struct Edge{
    int a,b,w;
    //重载运算符<
    bool operator<(const Edge& W)const
    {
        return w<W.w;
    }
}edge[N];

//使用并查集来维护当前的连通块
int find(int x)
{
    if(p[x]!=x) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    //将所有的边按照权重从小到大排序
    sort(edge,edge+m);
    
    //初始化并查集
    for(int i = 1; i<=n; i++) p[i] = i;
    
    //ans维护最小生成树的权重之和,cnt维护连通块当中的边
    int ans = 0, cnt = 0;
    //枚举每一条边
    for(int i = 0; i<m;i ++)
    {
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;
        
        
        a = find(a);
        b = find(b);
        
        //如果a 与 b 不连通
        if(a!=b)
        {
            //将a这个点加入到集合中来
            p[a] = b;
            //维护ans
            ans += w;
            cnt++;
        }
    }
    //所有边都遍历完之后,如果cnt不等于n-1,则不存在最小生成树
    if(cnt != n-1)
        return 0x3f3f3f3f;
    return ans;
}

int main()
{
    //n:顶点数  m:边数
    cin>>n>>m;
    
    for(int i = 0; i<m; i++)
    {
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        //表示a与b之间有一条权重为w的边
        edge[i] = {a,b,w};
    }
    
    int ans = kruskal();
    
    if(ans == 0x3f3f3f3f)
        puts("impossible");
    else
        cout<<ans<<endl;
    
    return 0;
}

6.Prim算法求最小生成树(Acwing858)

给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。

输入格式

第一行包含两个整数n和m。(1≤n≤500,1≤m≤10^5)
接下来m行,每行包含三个整数u,v,w,表示点u和点v之间存在一条权值为w的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

我们用集合S维护已经加入到最小生成树当中的点。开始我们将所有顶点之间的距离初始化成+∞,然后迭代n次,找到集合S外距离集合最近的点(该点与集合中与该点相连的边中权重最小),用该点更新S外的点到S的距离,并将该点加入到集合中。

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 510,inf = 0x3f3f3f3f;

int n,m,ans;
int dist[N];
int g[N][N];
bool st[N];

int prim()
{
    memset(dist,0x3f,sizeof dist);
    
    //迭代n次,每次向集合中添加一个点
    for(int i = 0; i<n; i++)
    {
        int t = -1; 
        for(int j = 1; j<=n; j++)
            if(!st[j] && (t == -1||dist[t]>dist[j]))
                t = j;
        
        if(i && dist[t] == inf) return inf;
        
        if(i) ans+=dist[t];
        st[t] = true;
        //更新这个点到其他点的距离
        for(int j = 1; j<=n; j++)
            dist[j] = min(dist[j],g[t][j]);
      
    }
    return ans;
}
int main()
{
    cin>>n>>m;
    
    memset(g,0x3f,sizeof g);
    while(m --)
    {
        int a,b,w;
        cin>>a>>b>>w;
        
        g[a][b] = g[b][a] = min(g[a][b],w);
    }
    
    int ans = prim();
    
    if(ans == inf)
        puts("impossible");
    else
        cout<<ans<<endl;
    return 0;
}

之后会补充二分图以及有向图的强连通分量,无向图的双连通分量hh

长风破浪会有时,直挂云帆济沧海!共勉!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,774评论 0 13
  • 1)这本书为什么值得看: Python语言描述,如果学的Python用这本书学数据结构更合适 2016年出版,内容...
    孙怀阔阅读 12,465评论 0 15
  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,340评论 0 2
  • 计算机基础 三级存储系统的结构 计算机的三级存储系统是什么?答:计算机系统中存储层次可分为三级:高速缓冲存储器、主...
    臭墨鱼阅读 5,055评论 0 7
  • 我是简律,这是日更千字的第16天。 柳比歇夫时间记录 在使用番茄工作法之前,我一直使用柳比歇夫的方法记录时间。在我...
    简律公号简设坊阅读 215评论 0 2