『图论专题上篇』


<更新提示>

<第一次更新>


<正文>

图论

图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。

本文主要讲在OI竞赛中的一些图论问题及图论算法。
目录

·图的基本概念<上篇>
·图的储存结构<上篇>
·图的遍历<上篇>
·最短路<上篇>
·最小生成树<下篇>
·拓扑排序<下篇>
·差分约束系统<下篇>

BEGIN!

图的基本概念

图是由一个顶点的集合V和一个顶点间关系的集合E组成:
记 G=(V,E)
V:顶点的有限非空集合。
E:顶点间关系的有限集合(边集)。
存在一个结点v,可能含有多个前驱结点和后继结点。
无向图:
在图G=(V,E)中,如果对于任意的顶点a,b∈V,当(a,b)∈E时,必有(b,a)∈E(即关系R对称),此图称为无向图。即一个图的边都没有确定的方向,能双向联通。
有向图:
如果对于任意的顶点a,b∈V,当(a,b)∈E时 ,(b,a)∈E未必成立,则称此图为有向图。
在有向图中,通常用带箭头的边连接两个有关联的结点。刚好和无向图相反,每一条边像单行道,不能逆向行驶。
这里写图片描述
(无向图)
这里写图片描述
(有向图)
在有向图中:
入度——以该顶点为终点的边的数目和 。
出度——以该顶点为起点的边的数目和 。
度数为奇数的顶点叫做奇点,度数为偶数的点叫做偶点。
度:等于该顶点的入度与出度之和。
结论:图中所有顶点的度=边数的两倍
连通:如果存在一条从顶点u到v有路径,则称u和v是连通的。
连通图:图中任意的两个顶点u和v都是连通的,称为连通图。否则称为非连通图。
带权图:图中的边可以加上表示某种含义的数值,数值称为边的权,此图称为带权图。这里写图片描述
(带权图)

图的储存结构

图是数据结构中的一种,但因为里面涉及了大量算法和较困难的问题,所以单独分开。因为图是数据结构的一种,所以我们首先要将图进行存储。
常用的储存结构有三种
1 邻接矩阵
2 邻接表
3 边表

邻接矩阵

邻接矩阵就是用一个二维数组来存储,a[i][j]代表的意义就是从节点i到节点j是否有边相连,在无向图中,1代表i,j互相连接,有向图在代表从i到j有单向边相连,反之0就代表没边,如果输一个大于1的数值,就代表这条边的权值是这个数。这里写图片描述
(如图示)

数据的读入方式看具体的题目,一般的读入方式有两种:
1 直接给出邻接矩阵:直接读入(不附代码)
2 给出两个相邻的顶点(及权值):对邻接矩阵用具体的下表进行赋值。

cin>>n>>m;
for(int i=1;i<=n;i++)
{
    int x,y,v;
    cin>>x>>y>>v;//没权值时赋为1
    a[x][y]=v;
    //a[y][x]=v;无向图
}
邻接表

邻接表就是使用链表来存储图。
结构如下:
无权图:| 节点 | 邻节点指针 |
有权图:| 节点 | 边权值 | 邻节点指针 |
起点表:link[],相当于链表中的储存链表头的数组,表示由i点第一条边出去的下标。

struct edge{
    int y,v,next;       //y表示这条边的终点编号,v是权值;
};                  //next表示同起点下条边的编号是多少
edge e[maxm+10];  //边表。
int linkk[maxn+10];  //起点表 link[i]表示由i出去的第一条边的下标是多少

void insert(int ss,int ee,int vv)//ss为起点,ee为终点,vv为权值。
{  
    e[++t].y=ee; e[t].v=vv;     //t表示有t条边,是个全局变量。
    e[t].next=linkk[ss]; linkk[ss]=t;
}

void init(){
    scanf("%d %d %d",&n,&p,&m);
    for (int i=0;i<m;i++)    {
        int xx,yy,zz;
        scanf("%d%d%d",&xx,&yy,&zz);
        insert(xx,yy,zz);
        insert(yy,xx,zz);  //这里插入的是无向图,所以两条边都要插入。
    }    
}    

邻接矩阵与邻接表的区别:
邻接矩阵:代码书写简单,找邻接点慢
采用二维数组的静态存储结构
一般点数|v|小于 等于5000的时候,用邻接矩阵。
邻接表:代码书写较复杂,找邻接点快
采用动态存储结构(指针或用数组模拟)
一般点数|v|大于等于5000,并且边得个数不是很多的时候,用邻接表,并且现在一般都是用数组来模拟。

稀疏图:边表
struct edge
{
    int x,y,v;//两个点以及边权
}e[MAXN]={};

图的遍历

图的遍历就是对一个图的每一个节点进行一次有且仅有一次的访问。和搜索遍历的顺序类似,图的遍历也分两种:dfs遍历和bfs遍历。

DFS遍历

遍历算法:
1 从某一起始节点开始,对图进行访问,并对该节点标记:已访问。
2 再从上一个节点的一个邻节点继续作为始节点进行访问,并标记。
3 当始节点的的所有邻节点访问完时,返回到始节点的父节点继续访问,直至所有节点访问完毕

邻接矩阵遍历代码:

void dfs(int k)
{
    vis[k]=true;
    for(int i=1;i<=n;i++)
    {
        if((!vis[i])&&(a[k][i]))
        dfs(i);
    }
}

邻接表遍历代码:

void dfs(int k)
{
    for(int i=linkk[k];i;i=e[i].next)
    {
        if(!vis[e[i].y])
        {
            vis[e[i].y]=1;
            dfs(e[i].y);
        }
    }
}
BFS遍历

BFS按层次遍历:
1 从图中某节点出发,访问该节点并标记。
2 按广度优先搜索的顺序遍历该节点的邻节点,标记。
3 直至访问图中的每一个节点。

邻接矩阵遍历代码:

void bfs(int 1)
{
    memset(q,0,sizeof(q));
    int head=0,tail=1;
    q[i]=k;vis[i]=true;
    while(head<tail)
    {
        int k=q[++head];
        for(int j=1;j<=n;j++)
        {
            if(a[k][j]&&!vis[j])
            {
                q[++head]=j;
                vis[j]=true;
            }
        }
    }
}

邻接表的遍历代码:

void bfs(int i)
{
    int head=0,tail=1;
    q[1]=i;
    while(head++<=tail)
    {
        for(int j=linkk[q[head]];j;j=e[j].next)
        {
            if(!vis[e[j].y])
            {
                vis[e[j].y]=1;
                q[++tail]=e[j].y;
            }
        }
    }
}

最短路

这里写图片描述

引子:若有图G(若图所示),怎样从节点1到节点11使所走路径总和最短?
即求图论中的最短路径问题。

最短路径的属性
三角形性质

设源点s到x,y的最短路径的长度为dis[x],dis[y],x,y有直接路径且长度为len[x][y],则有以下三角形定理:dis[x]+len[x][y]>=dis[y]。
松弛:若处理过程中,有两点x,y不符合三角形定理,则可以利用三角形定理对嘴短路进行更新,称该操作为松弛:


if(dis[x]+len[x][y]<dis[y])
{
    dis[y]=dis[x]+len[x][y];
}
对于任意两点之间的最短路径——floyd算法

目标:求出图中任意两点的最短路径dis[i][j]。
原理:图的传递闭包思想:

if(dis[i][k]+dis[k][j]<dis[i][j])
{
    dis[i][j]=dis[i][k]+dis[k][j];
}

时间复杂度:O(n3)
算法代码:

for(int k=1;k<=n;k++)
{
    for(int j=1;j<=n;j++)
    {
        for(int i=1;i<=n;i++)
        {
            if(dis[i][k]+dis[k][j]<dis[i][j])
            {
                dis[i][j]=dis[i][k]+dis[k][j];
            }
        }
    }
}

初始化条件:

dis[i][i]=0;//自己到自己的距离为0
dis[i][j]=边权;//i到j有直接相连的边
dis[i][j]=INF;//i到j没有直接相连的边,将其设为正无穷
对于一个顶点到其他顶点的最短路径——dijkstra算法

目标:一个源点到其他顶点的最短路径,不允许负权边——单源,非负。
原理:贪心思想
思路:
将点分为两个集合,一开始集合1中只有一个源点,其他点均在集合2中。
1 在集合2中找到一个距离源点最近的顶点,加入集合1。
2 对加入后集合2中的每一个顶点进行松弛操作,即修改在加入顶点k后在集合2中的剩余顶点j经过k后是否变短。
如果变短,修改dis[j]。
3 重复1,直至集合2为空。
时间复杂度:O(n2)
算法代码如下

void dijkstra(int st)
{
    for(int i=1;i<=n;i++)dis[i]=a[start][i];
    memset(vis,0,sizeof(vis));
    vis[start]=1;dis[start]=0;
    for(int i=1;i<=n;i++)
    {
        int min=INF;
        int k=0;
        for(int j=1;j<=n;j++)
        {
            if(!vis[j]&&dis[j]<minn)
            {
                minn=dis[j];
                k=j;
            }
        }
        if(k==0)return;
        vis[k]=1;
        for(int j=1;j<=n;j++)
        {
            if(!vis[j]&&dis[k]+a[k][j]<dis[j])
            {
                dis[j]=dis[k]+a[k][j];
            }
        }
    }
}

初始化条件:

dis[start]=0;//起始点为0
dis[j]=a[start][j];//有边相连,直接赋为边权无边设为INF
对于单源点最短路,可以判断负环的算法——Bellman—ford算法

引子:容易得知,当图中有负权边构成的回路时,dijkstra算法求的最短路是错的,只会求出负无穷的答案,所以我们需要一种判断负环的算法。
目标:判断负环。
原理:通过求最短路的松弛次数判断
思路:
1 初始化每一个到源点s的距离为正无穷
2 取所有边(x,y),看x能否对y松弛(由于需要用到取边操作,所以我们用边表存图)
3 没有松弛则return,若有松弛,且松弛次数小于n转 2
4 如果松弛了n次还能松弛,则存在负环
时间复杂度:O(N*E)
算法代码如下:

void bellman_ford(int st)
{
    memset(dis,10,sizeof(dis));
    dis[st]=0;
    bool re=0;
    for(int i=1;i<=n;i++)
    {
        re=0;
        for(int j=1;j<=m;j++)
        {
            if(dis[a[j].x]+a[j].v<dis[a[j].y])//松弛 
            {
                dis[a[j].y]=dis[a[j].x]+a[j].v;
                re=1; 
            } 
        } 
        if(!re)return 0;//不存在负环 
    } 
    return 1;//超过n次松弛,存在负环 
}
对于Bellman—ford算法迭代的优化——SPFA算法

引子:Bellman—ford算法中,如果边(x,y)在上一次dis[x]没有改变,那么显然下一次检查也是多余的。
改进思路:我们只要在迭代时只检查刚松弛过的点x能不能在松弛其他点即可。就可以利用BFS的队列来实现了,每一次将松弛的点加入队列,在不断从队头取出点松弛其他的点即可。(用邻接表实现)
时间复杂度:O(K*E)
算法代码如下:

void spfa(int st)
{
    memset(dis,10,sizeof(dis));
    memset(vis,0,sizeof(vis));
    dis[st]=0;vis[st]=1;q[1]=st;
    //vis数组标记的是是否在队列中 
    int head=0,tail=1;
    while(head<tail)
    {
        int temp=q[++head];
        vis[temp]=0;
        for(int i=linkk[temp];i;i=e[i].next)
        {
            int Next=e[i].y;
            if(dis[temp]+e[i].v<dis[Next])
            {
                dis[Next]=dis[temp]+e[i].v;
                if(!vis[Next])
                {
                    q[++tail]=Next;
                    vis[Next]=1;
                }
            }
        }
    }
}

<后记>图论大工程上篇到此结束啦


<废话>

猜你喜欢

转载自blog.csdn.net/Prasnip_/article/details/80909457