第四讲 图
图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E ),其中G就表示一个图,V是图G中顶点的集合,E是图G中边的集合。简单的说就是多个前驱多个后继。
图的相关定义
无向边:从一个顶点a到另一个顶点b之间的边没有方向,则称这条边为无向边。
有向边:从一个顶点a到另一个顶点b之间的边有方向,则称这条边为有向边,也称为弧。
无向图:每条边都是无向边。
有向图:每条边都有方向。
完全图:任意两个点都有一条边相连。
无向完全图的点有n个,则有n*(n-1)/2。
有向完全图每两个点之间有两条边有n个点,n*(n-1)条边。
网:边带权值的图。
邻接:有边相连的两个顶点之间的关系。
关联:边与顶点之间的关系。
顶点的度:相关联的边的数目在有向图中又分入度和出度。
路径:接续的边构成的顶点序列。
路径长度:路径上边或弧的数目。
回路(环):第一个顶点和最后一个顶点相同的路径。
极小联通子图:在子图中删去任何一条子图就不连通了。
生成树:包含图g所有顶点的极小联通子图。
图的储存结构
顺序存储
可以使用二维数组来存储,称为邻接矩阵表示法。
创建的步骤
1,输入总顶点数和边数
2,依次输入点的信息存入顶点表中
3,初始化邻接矩阵
无向图的邻接矩阵是对称的,顶点的度等于第i行中1的个数
完全图的邻接矩阵中,对角元素为0,其余为1
有向图的邻接矩阵第i行是出度边,第i列是入度边
下图就是邻接矩阵
可以对照我列出来的特点分别对照着看看。
邻接表
处理方法是这样的,先用一个一维数组把图中顶点存起来,因为数组可以容易的读取到顶点的信息,更方便,对于数组中还要开辟一个指针域,指向邻接的顶点,然后后面用单链表来构成。
邻接矩阵和邻接表的优缺点:邻接矩阵可以很方便的查找入度出度,而且也方便查找,但是面对一些特殊情况的时候,有可能会浪费很多的内存空间,而邻接表虽然很好的避免这种问题,但是邻接表,如果要查找两个顶点间有没有邻接,就要逐个遍历,会比较复杂,而且统计入度也很繁杂。
图的遍历
遍历的定义:从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且每个顶点仅被访问一次,就叫做图的遍历,他是图的基本运算。
遍历的实质:找每个顶点的邻接点的过程。
图的特点:图中可能存在回路,且图的任何一个顶点都可能与其它顶点想通,所以在遍历的时候我们可能又会重复的访问已经访问过的顶点,所以为了避免重复访问,我们就需要定义额外一个数组,来做标记。
深度优先遍历(DFS)
方法:在图中某一起始点出发,然后访问邻接的顶点,再从邻接的顶点出发,继续访问未访问过的顶点,一直进行类似的访问,直到到达所有邻接点都被访问过的那个点的时候,就退回到上一个访问的顶点,看看还有没有没访问过的,有得话就继续访问,没有的话就继续退回去,一直进行这样的动作,直到所有的顶点都被访问过。
就是按上图的顺序来得。
邻接矩阵的深度优先遍历,就是寻找每一行,如果碰到1,那么就看看有没有遍历过,如果没有就把这个点标记一下,然后继续寻找这个点所在的那一行,寻找那一行有没有1,碰到就先看看有没有遍历过,然后继续,一直到某一行,都是遍历过了,就返回。
void DFS(int G[][],int v)
{
printf("%d ",v);
book[v]=1;//标记,避免重复访问
for(int i=0;i<n;i++)//n个数
{
if(G[v][i]!=0&&book[i]==0)//看看是不是遍历过了
{
DFS(G,i);
}
}
}
广度优先搜索(BFS)
方法:从图的某一结点出发,首先依次访问该结点的所有邻接顶点,再按这些顶点被访问的先后次序访问与它们相邻接的所有未被访问的顶点,重复这个过程直到所有点,都被访问过了。
有点类似于树的层次遍历。
广度优先搜索的实现是依靠队列来实现的,比如上面这张图,我们先把v1点入队,然后出队,把v1点的两个邻接点v2,v3入队,然后v2出队,把v2点的邻接点v4,v5入队,一直重复这个步骤,直到队空,就完成了遍历。
void BFS(int G[][],int v)
{
queue<int> q;
q.push(v);
while(!=q.empty)
{
int t=q.top();
q.pop();
printf("%d",t);
for(int i=0;i<=n;i++)
{
if(G[t][i]==1&&!book[t][i])
{
q.push(i)
}
}
}
}
最小生成树
相关概念
1,生成树:所有顶点均由边连接到一起,但不存在回路的图。
2,一个图可以有许多课不同的生成树。
3,所有生成树具有以下特点:
生成树的顶点个数与图的顶点个数相同。
生成树是图的极小连通子图,去掉一条边就不联通了。
一个有n个顶点的生成树有n-1条边
在生成树中再加一条边必然形成回路。
生成树中任意两个顶点间的路径是唯一的。
最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵树称为该网的最小生成树,也叫最小代价生成树。
构造最小生成树,我们需要知道一个MST性质,这个性质的概念是,设N(V,E)是一个连通网,U是顶点集V的一个非空子集,若边(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,则必存在一棵包含这条边的最小生成树。
这个性质就是上图的这个意思。在已落在生成树上的顶点集和未落在生成树上的顶点集,在这两个集合的所有连通边中取权值最小的边,要注意,看看有没有成环。
普里姆算法
算法思想:像刚刚那个mst性质一样,分为两个集合,然后找一条代价最小的边,然后将那个顶点,也加入已落在最小生成树上的顶点的集合,直到所有顶点都落在生成树上。
步骤就像上图那样,后面我就没写下去了,都是一样的。
克鲁斯卡尔
算法思想:令最小生成树初始状态为只有n个顶点而无边的非连通图,然后在所有边的集合中取一个最小的边,加进去,还要判断有没有成环,如果成环了就下一条边,直到所有点都连通。所以用这个方法的时候,我们要先将所有边排序然后再选择
#include<stdio.h>
int f[10000];
struct way
{
int a;
int b;
int cost;
};
int getf(int v)
{
if(f[v]==v)
return v;
else
{
int tmp = getf(f[v]);
f[v] = tmp;
return tmp;
}
}
struct way q[10000];
int main()
{
struct way t;
int n,m;
while(scanf("%d%d",&m,&n)!=EOF)
{
if(n==0)
break;
int i,sum=0,j;
int num=0;
for(i=0;i<=m;i++)
{
f[i]=i;
}
for(i=1;i<=n;i++)
{
scanf("%d%d%d",&q[i].a,&q[i].b,&q[i].cost);
}
for(i=1;i<=n-1;i++)
{
for(j=1;j<=n-i-1;j++)
{
if(q[j].cost>q[j+1].cost)
{
t=q[j];
q[j]=q[j+1];
q[j+1]=t;
}
}
}
int x,y;
for(i=1;i<=n;i++)
{
x=getf(q[i].a);
y=getf(q[i].b);
if(x!=y)
{
f[x]=y;
sum+=q[i].cost;
}
}
for(i=1;i<=m;i++)
{
if(f[i]==i)
num++;
}
if(num==1)
printf("%d\n",sum);
if(num>1)
printf("Impossible\n");
}
}
这个代码就是克鲁斯卡尔的,先进行排序,然后判断两个点之间是否有关系,如果两个点都连接于同一个点,那么再把他们相连就会成环,所以,我们可以用并查集来进行判断,如果没关系,就相连。最后判断是否所有的点都连通了,这一步很重要,通过看每个点的主人是不是自己,也是并查集的思想。
两种算法的差别,普里姆算法是选择点的思想时间复杂度为O(n^2),更适用于稠密图,克鲁斯卡尔是选择边的思想,时间复杂度为O(eloge),适用于稀疏图。
最短路径
最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边。
最短路径问题分为两种类型:
一,单源最短路径,用迪杰斯特拉算法。
二,所有顶点间的最短路径用弗洛伊德算法。
迪杰斯特拉算法
算法步骤:
1,初始化,先找出源点v0到各终点vk的直达路径,即通过一条弧就达到的路径,如果没有则记为无穷大。
2,选择,选择出一条最短的路径。
3,更新,看看通过这条新加入的边,会不会到别的点的距离缩短,
一直重复选择和更新这两个步骤。
把顶点分为两组,一组是已求出最短路径的顶点集合,一组是未确定最短路径的,然后找最短的边,将点一个个加入第一个集合。
就是按图中这样来的,不断更新再找最小。每当选一个当前路径最短的顶点加入,就更新v0其它顶点的距离,下面是核心代码
int dik(int v0,int vn)
{
int i,j;
int mint;
int t;
int book[1000];
int quan[1000];
for(i=0;i<n;i++)
{
quan[i]=G[v0][i];
book[i]=0;
}
quan[v0]=0;
book[v0]=1;
for(i=0;i<n-1;i++)
{
t=v0;
mint=INF;
for(j=0;j<n;j++)//寻找当前最小的路径
{
if(book[j]==0&&quan[j]<mint)
{
mint=quan[j];
t=j;
}
}
book[t]=1;
for(j=0;j<n;j++)
{
if(book[j]==0&&G[t][j]+quan[t]<quan[j])//更新各路径
{
quan[j]=G[t][j]+quan[t];
}
}
}
return quan[vn];
}
弗洛伊德算法
算法思想:逐个顶点试探,从vi到vj中所有可能存在的路径中选出一条长度最短的路径。
步骤就是按上图所示
void floyd(int G[n][n],int s[n][n],n)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
s[i][j]=G[i][j];
}
}
for(int i=1;i<=n;i++)//逐个加入中间点
{
for(int j=1;j<=n;j++)
{
for(int k=1;k<=n;k++)
{
s[i][j]=min(s[i][j],s[i][k]+s[k][j])
}
}
}
}
拓扑排序
首先引入一个概念,AOV网,用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网。就比如学习,你要学数据结构,那肯定就要先有c语言的基础,这样才可以学习数据结构,aov网讲述就是这种先后顺序的关系。所谓的拓扑的排序就是将所有活动排成一个线性序列。
拓扑排序的步骤,就是在图中选一个没有前驱的结点且输出,从图中删去该顶点和以他为尾的弧,重复上面两个步骤,直到所有顶点输出。
void tuo()
{
int i,j,t;
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
{
if(G[i][j]==1)
{
rudu[j]++;//统计入度
}
}
}
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
{
if(rudu[j]==0)
{
t=j;
break;
}
}
rudu[t]=-1;
tuopu[i]=t;
for(j=1;j<=n;j++)
{
if(G[t][j]==1)
{
rudu[j]--;//删去弧
}
}
}
}