图论的小总结

图论是一个很大的范围,如果要学精的话肯定是深度钻研以及学习更多的高级算法,高级数据结构等。因为我也学习了图论的入门了,所以想要总结一下图论入门的知识,如果有什么不对或不完整的部分希望各位大神帮我指出,谢谢。

这篇随笔就只写一下图的最短路和最小生成树以及一些基础的知识。

一.图的定义:由顶点集合V和一个顶点间关系的集合也就是边的集合E组成的,记做G=(V,E)。

二.图可以根据边的特性分为两种,有向图和无向图。

无向图:如果两个点a,b,在满足(a,b)∈E的同时也一定满足(b,a)∈E,则称这图为无向图,再画图时用不带箭头的边表示顶点之间的关系。

有向图:对于(a,b)∈E成立而(b,a)∈E不一定成立成为有向图,在画图时用带箭头的边表示顶点之间的关系。

三.图的储存方式

1.邻接矩阵。

2.邻接表。

3.边集数组。

4.前向星(可以被邻接表代替)。

(1)邻接矩阵(比较费空间):

邻接矩阵用二维数组模拟结点之间的关系:

a[i][j]=1:(或别的权值n)表示结点i与j之间联通(或从结点i到j的权值是n),如果是无向图,则a[j][i]也一定是这个值。

a[i][j]=0:结点i与j之间没有边联通。

至于读入的形式根据输入形式而定。

(2)邻接表:

struct edge
{
    int x,y,v;  //y表示这条边的终点编号
    int next; //next表示同期点的下一条边的编号是多少。
};
edge e[maxn*2+100]; //存储边的关系
int lin[maxn+100]; //起点表,lin[i]表示从i出去的第一条边的下标是多少。
int len=0; //表示有多少条边

void insert(int xx,int yy,int vv)
{
    e[++len].next=lin[xx]; lin[xx]=len;
    e[len].y=yy; e[len].v=vv; e[len].x=xx;
}

void init()
{
    int n,m;
    sacnf("%d %d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int xx,yy,vv;
        scanf("%d %d %d",&xx,&yy,&vv);
        insert(xx,yy,vv);
        insert(yy,xx,vv); //因为是无向图,所以要反向在存储一遍
    }
}

邻接矩阵如果要处理重边的情况要判断,而邻接表只需要直接存就行了。

四.图的遍历

深搜dfs和广搜bfs。

需要一个判断是否已经到达过了的数组 bool

vis[maxn+100];

深度优先搜索:

邻接矩阵的dfs

void dfs(int k)
{
    for(int i=1;i<=n;i++)
    {
        if(a[k][i] && !vis[i])
        {
            vis[i]=1;     //只要到达了这个点就标记为true
            dfs(i);     //以i为开始点继续搜索
        }
    }
}

邻接表的dfs

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

主程序

int main()
{
    memset(vis,0,sizeof(vis));
    for(int i=1;i<=n;i++)
    {
        if(!vis[i]) dfs(i);
    }
    return 0;
}

广度优先搜索:

需要再dfs的基础上再加一个队列q来辅助

邻接矩阵的bfs:

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

邻接表的bfs:

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

主程序和dfs的一样。

五.图的最小最短路

(1).神奇而又强大的Floyd。

(2).我到现在一道题都没有敲的dijkstra QAQ。

(3).Bellman—Ford

(4).SPFA(是Bellman—ford算法的迭代的改进)

前两个是要用邻接矩阵存储,后两个用邻接表存储。

Floyd:

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

运行完以后a[i][j]就代表从i到j 的最短路了,而且它的强大在于可以直接求出所有最短路。

Floyd还有一个特性:传递背包,其实我认为就是能够单纯的判断图是不是联通的,用遍历的算法依旧可以。

dijkstra在这里就不敲了,自己不会用,也估计敲不出来,我觉得SPFA比他强大的多,所以一直在练SPFA。

bellman—Ford:

int dis[maxn+100] //表示起始点到每个点的最小路径。            
int Bellman_ford(int k)
{
    memset(dis,10,sizeof(dis));
    dis[k]=0;
    bool flag=0;
    for(int i=1;i<=n;i++) //最多迭代n次
    {
        flag=0;
        for(int j=1;j<=len;j++) //取图的每一条边
        {
            if(dis[e[j].x]+e[i].v<dis[e[j].y])
            {
                dis[e[j].y]=dis[e[j].x]+e[j].v;
                flag=1;
            }
        }
        if(!flag) return 0;//已经松弛不了了
    }
    return 1;  //迭代了n次,表示有负圈。
}

Bellman—Ford的独特功能就是能够判断是不是有负圈。当然作为他的改进版的SPFA也可以。但是如果是Floyd和dijkstra的话碰到负圈就不行了,他们会无限的迭代。

SPFA:

int lon[maxn+100];
bool vis[maxn+100];
int dis[maxn+100];
int SPFA(int k)
{
    memset(dis,10,sizeof(dis));
    memset(vis,0,sizeof(vis));
    memset(lon,0,sizeof(lon));
    int head=0,tail=1;
    q[1]=k; vis[k]=1;
    dis[k]=0;
    while(head<tail)
    {
        int ty=q[++head];
        vis[ty]=0;
        for(int i=lin[ty];i;i=e[i].next)
        {
            int u=e[i].y;
            lon[u]++;
            if(lon[u]==n)
            {
                return 1;    //如果这个点被迭代了n次了,证明有负圈。
            }
            if(dis[ty]+e[i].v<dis[u])
            {
                dis[u]=dis[ty]+e[i].v;
                if(!vis[u])
                {
                    q[++tail]=u;
                    vis[u]=1;
                }
            }
        }
    }
}

六.图的最小生成树

(1)Prim算法

(2)kruskal算法

1)重复取最小权的交叉边,直到生成树中已经存在了n-1条边了。

int dis[maxn+100],vis[maxn+100];
int sum=0;  //最小生成树的总值
void prim(int s)
{
    memset(dis,10,sizeof(dis));
    memset(vis,0,sizeof(vis));
    vis[s]=1;
    for(int i=1;i<=n;i++) dis[i]=a[s][i];
    for(int i=1;i<n;i++) //做n-1次最短路径
    {
        int minn=9999999,c=0;
        for(int j=1;j<=n;j++)
        {
            if(!vis[j] && dis[j]<minn)
            {
                minn=dis[j];
                c=j;
            }
        }
        vis[c]=1;
        sum+=minn;
        for(int j=1;j<=n;j++)           //用最小权值的边更新最小生成树
        {
            if(a[c][j]<dis[j] && !vis[j])
                dis[j]=a[c][j];
        }
    }
}

用邻接矩阵,可以用堆优化。

2)kruskal算法

用到了并查集这个神奇的东西。

int getfather(int k)
{
    if(father[k]==k) return k;
    father[k]=getfather(father[k]);
    return father[k];
}

void merge(int x,int y)
{
    int fx=getfather(x);
    int fy=getfather(y);
    father[fx]=fy;
}

bool mycmp(edge a,edge b)
{
    return a.v<b.v;
}

void kruskal()
{
    for(int i=1;i<=n;i++) father[i]=i;
    sort(e+1,e+len+1,mycmp);
    int cal=0;
    for(int i=1;i<=len;i++)
    {
        int v=getfather(e[i].x);
        int u=getfather(e[i].y);
        if(v!=u)
        {
            merge(v,u);
            if(++cal==n-1)
            {
                return;
            }
        }
    }
}

最近在赶学习进度,没有写的太细,有些东西的概念和我自己敲代码时发现的一些没有来得及总结写上去,就写了个最基本的总结,等进度赶得差不多了估计会再回来加上一些具体的东西和我自己写代码时的体会和小细节。也是最近学到这里了,怕自己最近有些赶,掌握的东西不是很扎实,所以写了个小总结。

原文地址:https://www.cnblogs.com/assassinyyd/p/6409429.html