图论基础(图&最短路)

树也是一种特殊的图,图上每一个点有出度和入度,对于有向图来说,出度表示顶点的出边,入度表示顶点的入边,图可根据是否有边权分为有权图和无权图,一般来说,对于无权图求最短路时,可以将其赋值为1。可以证明,有向无环图一定存在拓扑序列,因此有向无环图又被成为拓扑图。具体看如下。

建图

邻接矩阵:邻接矩阵通过一个二维数组a[i][j]来存,首先通过memset(a,0x3f,sizeof(a))来给其赋初值为无穷大,每读入一条边就给其赋值为边权值,表示一条边从i指向j,权值为a[i][j],若无向图存两遍即可,时间复杂度和空间复杂度都为O(n^2),不难发现,邻接矩阵存图对时间和空间的消耗都较大,适用于存稠密图,但若是存储稀疏图,会造成大量的空间浪费。故当输入边集较多时,一般不采用邻接矩阵

邻接表:

数组实现

(模板来自于y总,个人感觉这种写法更像链式前向星...?)

// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);

vector实现

#include<iostream>
#include<queue>
#include<vector>
using namespace std;

const int N = 1e3;

int n, m;//n为点数,m为边数
vector<pair<int, int>>h[N]; //N为边的起始点,first为边的入点,second为边的权值
struct node {
	int id; int d;//编号和距离
	node(){}
	node(int id,int d):id(id),d(d){}
	//重新建立优先级,实现小根堆
	bool operator<(const node& A) const {
		return d > A.d;
	}
};

int main()
{
	cin >> n >> m;
	while (m--)
	{
		int x, y, z;
		cin >> x >> y >> z;
		h[x].push_back(make_pair(y, z));//有向图的存法
	}
}

需要注意的是,因为求最短路时(如dijkstra)需要用到优先队列,但是优先队列默认大根堆,因此需要在结构体中修改他的优先级,将大根堆改为小根堆,但这并不是邻接表存图的主要内容。

链式前向星:

int head[maxn];//表示以i为起点的最后一个点的编号,初始为-1
int cnt = 1;//cnt表示边的编号
struct node {
	int v, w, next;//next表示与这条边起点相同的上一条边的编号
}edge[maxn];
void addedge(int u, int v, int w)
{
	edge[cnt].v = v;
	edge[cnt].w = w;
	edge[cnt].next = head[u];
	head[u] = cnt++;
}
//初始化
	memset(head, -1, sizeof(head));

/*若要遍历图, i表示点,j首先为以i为起点的最后一个点的编号,j不为 - 1
则证明还有连通的边,j又为与这条边起点相同的上一条边,若遇见-1,则表示
已经没有以i为起点的边了,结束循环,遍历下一个点
*/
for(int i = 0;i < n;i++)
	for(int j = head[i];j != -1;j = edge[j].next)

图的遍历

图的深度优先遍历(基于链式前向星建图):
 

int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = head[u]; i != -1; i = edge[i].next)
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}

图的宽度优先遍历:

queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (!q.empty())
{
    int t = q.front();
    q.pop();

    for (int i = head[t]; i != -1; i = edge[i].next)
    {
        int j = edge[i].next;
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

最短路是图论里一个重要的问题,给定一个图,求一条从起点到终点的路径,使得边权和最小。

最短路分为单源最短路径(从某个源点到其他各个顶点的最短距离)和多源最短路径(任意两个顶点之间的最短距离),一般情况来说,对于只有正权边的图,一般采用dijkstra算法,朴素版的dijkstra算法时间复杂度为O(n^2),堆优化版的dijkstra时间复杂度为O(mlogn),若是存在负权边,则可以选择Bellman-Ford算法或者SPFA算法,前者时间复杂度为O(nm),而后者时间复杂度一般为O(m),最坏为O(nm),一般来说,SPFA算法各方面都要优于Bellman-Ford算法,但是Bellman-Ford算法可以处理一些指定情况,比如限制边数,这是SPFA算法不能做到的,需要注意的是,若是图中存在负环,则最短路又可能不存在,而Bellman-Ford算法和SPFA算法都可以判断负环(一般使用SPFA),并且大多数情况,SPFA算法也是可以替代dijkstra算法的(也可能会被卡),对于求多源汇最短路,我们一般采用基于动态规划思想的Floyd算法,时间复杂度为O(n^3)(可以发现与边的数量无关,只与点的数量有关),代码如下。

dijkstra(朴素版)

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

using namespace std;

const int maxn=510;
int g[maxn][maxn],dis[maxn];
bool vis[maxn];
int n,m;

int dijkstra()
{
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    for(int i=0;i<n;i++)
    {
        int t=-1;
        for(int j=1;j<=n;j++)
        {
            if(!vis[j]&&(t==-1||dis[j]<dis[t])) t=j;
        }
        vis[t]=1;
        for(int j=1;j<=n;j++)
        {
            dis[j]=min(dis[j],dis[t]+g[t][j]);
        }
    }
    if(dis[n]==0x3f3f3f3f) return -1;
    return dis[n];
}

int main()
{
    memset(g,0x3f,sizeof g);
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b]=min(g[a][b],c);//有向图
    }
    int res=dijkstra();
    printf("%d",res);
    return 0;
}

dijkstra(堆优化版)

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

using namespace std;

int n,m;
const int maxn=500,maxm=1e5+5;
int dis[maxn];
bool vis[maxn];
int h[maxm],e[maxm],ne[mexm],w[mexm],idx;

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

int dijkstra()
{
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    priority_queue<pii,vector<pii>,greater<pii>>q;
    q.push(mp(0,1));
    while(!q.empty())
    {
        auto t=q.top();
        q.pop();
        int ver=t.second,dist=t.first;
        if(vis[ver]) continue;
        vis[ver]=1;
        for(int i=h[ver];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist+w[i]<dis[j])
            {
                dis[j]=dist+w[i];
                q.push(mp(dis[j],j));
            }
        }
    }
    if(dis[n]==0x3f3f3f3f) return -1;
    return dis[n];
}

int main()
{
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    int res=dijkstra();
    printf("%d",res);
    return 0;
}

Bellman-Ford

(最多只能经过k条边)

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

using namespace std;

const int maxn=510,maxm=1e4+5;
int dis[maxn],backup[maxn];
struct Edge{
    int a,b,w;
}edges[maxm];
int m,n,k;

void Bellman_Ford()
{
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    for(int i=0;i<k;i++) //有边数限制,最多只能经过k条边的最短路
    {
        memcpy(backup,dis,sizeof dis); //备份一下dis数组,防止发生串联,若没有边数限制则不需要备份
        for(int j=0;j<m;j++)
        {
            int a=edges[j].a,b=edges[j].b,w=edges[j].w;
            dis[b]=min(dis[b],backup[a]+w);
        }
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&k);
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        edges[i]={a,b,c};
    }
    Bellman_Ford();
    if(dis[n]>0x3f3f3f3f/2) puts("impossible");
    else printf("%d",dis[n]);
    return 0;
}

SPFA

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

using namespace std;

const int maxn=1e5+5;
int h[maxn],ne[maxn],w[maxn],e[maxn],idx;
int dis[maxn];
bool vis[maxn];
int n,m;

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

void spfa()
{
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    queue<int>q;
    q.push(1);
    vis[1]=1;
    while(q.size())
    {
        int t=q.front();
        q.pop();
        vis[t]=0;
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dis[j]>dis[t]+w[i])
            {
                dis[j]=dis[t]+w[i];
                if(!vis[j])
                {
                    q.push(j);//如果不在队列中就加入队列
                    vis[j]=1;
                }
            }
        }
    }
}

int main()
{
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    spfa();
    if(dis[n]==0x3f3f3f3f) puts("impossible");
    else printf("%d\n",dis[n]);
    return 0;
}

SPFA判断负环

bool spfa()
{
    queue<int>q;
    for(int i=1;i<=n;i++)
    {
        q.push(i);
        vis[i]=1;
    }
    while(!q.empty())
    {
        auto t=q.front();
        q.pop();
        vis[t]=0;
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dis[j]>dis[t]+w[i])
            {
                dis[j]=dis[t]+w[i];
                cnt[j]=cnt[t]+1;
                if(cnt[j]>=n) return true;
                if(!vis[j]){
                    q.push(j);
                    vis[j]=1;
                }
            }
        }
    }
    return false;
}

Floyd

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

using namespace std;

const int maxn=210,inf=0x3f3f3f3f;
int d[maxn][maxn];
int n,m,q;

void init()
{
    for(int i=1;i<=n;i++)
       for(int j=1;j<=n;j++)
           if(i==j) d[i][j]=0;
           else d[i][j]=inf;
}

void floyd()
{
    for(int k=1;k<=n;k++)
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
            }
        }
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&q);
    init();
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        d[a][b]=min(d[a][b],c);
    }
    floyd();
    while(q--)//q次询问
    {
        int a,b;
        scanf("%d%d",&a,&b);
        if(d[a][b]>inf/2) puts("impossible");
        else printf("%d\n",d[a][b]);
    }
    return 0;
}

具体详情请见http://acwing.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值