图
图相关概念
一、思维导图
二、概念解释
图的结构定义:是由一个顶点集V和一个顶点间的关系集合组成的数据结构
Graph=(V,VR)
其中,V={x|x∈某个数据对象},是顶点的又穷非空集合;
VR={(X,Y)|X,Y∈V}或VR={<X,Y>|X,Y∈V&&Path(X,Y)}是顶点间关系的有穷集合,也叫做边(edge)或弧(Arc)集合。Path(X,Y)表示从x到y的一条单向通路,它是有方向的。
弧是有方向的,因此称由顶点集和弧集构成的图为有向图。无向图则由顶点集和边集构成。
抽象数据类型定义:
ADT Graph{
数据对象V:V是顶点集。
数据关系R:R={VR}, VR={<v, w>| v, w∈V且P(v, w),
<v, w>表示从v到w的弧,谓词P(v, w)定义了弧 <v, w>的意义或信息}
基本操作P:
结构的建立和销毁
对顶点的访问操作
插入或删除顶点
插入和删除
对邻接点的操作
遍历
}ADT Graph
相关概念:
假设图中有n个顶点,e条边。则:
含有e=n(n-1)/2条边的无向图称作完全图;
含有e=n(n-1)条弧的有向图称作有向完全图;
若边或弧的个数e<nlogn,则称作稀疏图,否则称作稠密图。
若顶点v和顶点w之间存在一条边,则称顶点v和w互为邻接点;
和顶点v关联的边的数目定义为边的度。
对有向图来说:以顶点v为弧尾的弧的数目为顶点的出度。以顶点v为弧头的弧的度数为入度。
顶点的度(TD)=出度(OD)+入度(ID)
有n个顶点,e条边或弧的图,满足:e=1/2Σ(i=1,n)TD(Vi)
若图G中任意两个顶点之间都有路径相通,则称此图为连通图;
若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量;
对有向图来说,若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图,否则其各个强连通子图称作它的强连通分量;
假设一个连通图有n 个顶点和e 条边,其中n-1 条边和n 个顶点构成一个极小连通子图,称该 极小连通子图为此连通图的生成树;
对非连通图,则称由各个连通分量的生成树的集 合为此非连通图的生成森林;
图的存储结构:
邻接矩阵:
无向图的邻接矩阵是对称的,有向图的邻接矩阵 可能是不对称的
定义代码:
#define MaxInt 32767 //表示极大值
#define MVNum 100 //最大顶点数
typedef char VerTexType; //假设定点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
typedef struct { // 图的定义
VerTexType vexs[MVNum]; // 顶点表
ArcType arcs[MVNum][MVNum]; // 弧的信息(邻接矩阵)
int vexnum, arcnum; // 图当前的顶点数和边数
} AMGraph; //邻接矩阵
*借助于邻接矩阵容易求得顶点的度:
在无向图中,统计第i行(列)“1”的个数可求得顶点i的度。
在有向图中,统计第i行“1”的个数可得顶点i的出度;
统计第j列“1”的个数可得顶点j的入度;
邻接矩阵的缺点:
1、浪费空间:
存稀疏图(点很多而边很少)有大量无效元素
对稠密图(特别是完全图)还是很合算的
2、浪费时间:统计稀疏图中一共有多少条边
3、不适用于结构频繁修改,重新构建
图的邻接表的存储
单链表中每个结点由三个域组成
邻接点域(adjvex):指示与顶点vi邻接的点在图中的位置;
链域(nextarc):指示下一条边或弧的结点;
数据域(info):存储和边或弧相关的信息,如权值等。
定义代码:
//表(弧/边)结点: |adjvex|nextarc|info|
typedef struct ArcNode {
int adjvex; // 该弧所指向的顶点的位置
struct ArcNode *nextarc;//指向下一条弧的指针
InfoType *info; // 该弧相关信息的指针
} ArcNode;
//图的邻接表头结点:|data|firstarc|
typedef struct VNode {
VerTexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
} VNode, AdjList[MAX_VERTEX_NUM];
//typedef VNode AdjList[MAXV];//AdjList是邻接表类型
//图的结构定义:
typedef struct {
AdjList vertices;
int vexnum, arcnum;
} ALGraph;
图的遍历:从图中某个顶点出发游历图,访遍图中其余顶点, 并且使图中的每个顶点仅被访问一次的过程。
深度优先搜索(DFS):
连通图的DFS:从图中某个顶点v0出发,访问此顶点,然后依次 从v0的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v0有路径相通的顶点都被访问到。
深度优先搜索遍历连通图的过程类似于树的先根遍历;
为每个顶点设立一个“访问标志 visited[w]”来判断V的邻接点是否被访问;
代码:
void DFS(Graph G, int v) {
//从顶点v出发,深度优先搜索遍历连通图G
访问v; visited[v] = TRUE;
//FirstAdjVex为获得v的第一个邻接点, NextAdjVe为获得下一 个邻接点
for(w=FirstAdjVex(G, v); w>=0; w=NextAdjVex(G,v,w))
if (!visited[w]) DFS(G, w);
// 对v的尚未访问的邻接顶点w递归调用DFS } // DFS,这里只是伪代码,不涉及具体存储结构
非连通图的深度优先搜索遍历: 首先将图中每个顶点的访问标志设为FALSE, 之后搜 索图中每个顶点,如果未被访问,则以该顶点为起始 点,进行深度优先搜索遍历,否则继续检查下一顶点。
代码:
void DFSTraverse(Graph G) {
// 对非连通图 G 作深度优先遍历。
for (v=0; v<G.vexnum; ++v)
visited[v] = FALSE; //访问标志数组初始化
for (v=0; v<G.vexnum; ++v)
if (!visited[v]) DFS(G);//对尚未访问的顶点调用 DFS
}
深度优先遍历图的实质:对每个顶点查找其邻接点的过程。
广度优先搜索(BFS) :
从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0 的所有未被访问过的邻接点,之后按这些顶点被访问的先后 次序依次访问它们的邻接点,直至图中所有和V0有路径相通 的顶点都被访问到。
广度优先遍历图的实质:通过边或弧找邻接点的过程。
代码:
void BFSTraverse(Graph G){
for (v=0; v<G.vexnum; ++v)
visited[v] = FALSE; //初始化访问标志
InitQueue(Q); // 置空的辅助队列Q
for ( v=0; v<G.vexnum; ++v )
if ( !visited[v]) {// v 尚未访问
......
}
} // BFSTraverse
//说明:使“先被访问的顶点的邻接点”先于“后被访 问的顶点的邻接点”被访问,因此,采用队列来存放 已被访问过的顶点!
访问v; visited[v] = TRUE; // 访问v
EnQueue(Q, v); // v入队列
while (!QueueEmpty(Q)) {
DeQueue(Q, u); // 队头元素出队并置为u
for(w=FirstAdjVex(G, u); w!=0; w=NextAdjVex(G,u,w))
if ( ! visited[w]) {
访问v; visited[w]=TRUE;
EnQueue(Q, w); // 访问的顶点w入队列
} // if
} // while
DFS与BFS算法效率比较:
空间复杂度相同,都是O(n)(借用了堆栈或队列)
时间复杂度只与存储结构(邻接矩阵或邻接表)有关, 而与搜索路径(遍历的顺序)无关
搜索路径是由存储结构与算法共同决定的
无向图的连通分量和生成树:
对非连通图的遍历,需从多个顶点出发进行搜索。而每一 次从一个新顶点出发搜索得到的顶点序列就是图的各个连通分量中的顶点集。
对连通图进行搜索时,搜索过程中经历的所有边 和图中所有的顶点构成了连通图的一个极小连通 子图,即连通图的生成树。
生成树的特点:
n个顶点的连通子图的生成树是一个极小连 通子图,它包含图中所有顶点和n-1条边(但 有n-1条边的图不一定是生成树)。 生成树中任意两个顶点间的路径是唯一的。
注:边数>n-1时,则形成环;边数<n-1时则不 连通。
对于非连通图,其中的每一个连通分量都可以通 过遍历得到一棵生成树,所有这些连通分量的生 成树就构成了非连通图的生成森林。
图的应用:
1)最小生成树:
普里姆算法:取图中任意一个顶点 v 作为生成树的根; 往生成树上添加新的顶点 w。在添加的顶点 w 和已经在生成树上的顶点v 之间必定存在一条边, 并且该边的权值在所有连通顶点 v 和 w 之间的边 中取值最小;继续往生成树上添加顶点,直至生成树上 含有 n 个顶点为止。
构造的最小生成树不一定唯一, 但最小生成树的权值之和一定是相同的。
伪代码:
struct {
VertexType adjvex; //U集中的顶点序号
VRType lowcost; //边的权值
} closedge[MAX_VERTEX_NUM];
void MiniSpanTree_P(MGraph G, VertexType u) {
//用普里姆算法从顶点u出发构造网G的最小生成树
k = LocateVex ( G, u );
for (j=0; j<G.vexnum; ++j )//辅助数组初始化
if (j!=k)
closedge[j] = {u,G.arcs[k][j].adj };
closedge[k].lowcost = 0; //初始,U={u}
for (i=0; i<G.vexnum; ++i) {
继续向生成树上添加顶点;
}
k = Min(closedge);//求出加入生成树的下一个顶点k
printf(closedge[k].adjvex, G.vexs[k]);
//输出生成树上一条边
closedge[k].lowcost = 0;//第k顶点并入U集
for(j = 0; j < G.vexnum; ++j)
//修改其它顶点的最小边
if (G.arcs[k][j].adj < closedge[j].lowcost)
closedge[j]={G.vexs[k], G.arcs[k][j].adj };
克鲁斯卡尔算法:构造一个只含 n 个顶点的子图 SG;从权值最小的边开始,若它的添加不使SG 中产生回路,则在 SG 上加上这条边; 如此重复,直至加上 n-1 条边为止。
伪代码:
构造非连通图 ST=( V,{ } );
k = i = 0; // k 计选中的边数
while (k<n-1) {
++i;
检查边集 E 中第 i 条权值最小的边(u,v);
若(u,v)加入ST后不使ST中产生回路,则输出边(u,v);
且k++;
}
算法比较:
P法(点)——O(n2)、适用于稠密图
K法(边)——O(eloge)、适用于稀疏图
2)最短路径:
从某个源点到其余各顶点的最短路径 ——Dijkstra算法
每一对顶点之间的最短路径——Floyd算法
3)拓扑排序:
对有向图进行如下操作: 按照有向图给出的次序关系,将图中顶点排成一个 线性序列,对于有向图中没有限定次序关系的顶点, 则可以人为加上任意的次序关系。 由此所得顶点的线性序列称之为拓扑有序序列。
操作:从有向图中选取一个没有前驱的顶点,并输出之; 从有向图中删去此顶点以及所有以它 为尾 的弧; 重复上述两步,直至图空,或者图不空但找不到无 前驱的顶点为止。
算法实现(利用栈):
取入度为零的顶点v;
while (v<>0) {
printf(v); ++m;
w:=FirstAdj(v);
while (w<>0) {
inDegree[w]--;
w:=nextAdj(v,w);
}
取下一个入度为零的顶点v;
}
if m<n printf(“图中有回路”);
//为避免每次都要搜索入度为零的顶点,在算法中设置 一个“栈”,以保存“入度为零”的顶点。 CountInDegree(G,indegree); //对各顶点求入度
InitStack(S);
for ( i=0; i<G.vexnum; ++i)
if (!indegree[i]) Push(S, i); //入度为零的顶点入栈
count=0; //对输出顶点计数
while (!EmptyStack(S)) {
Pop(S, v); ++count; printf(v);
for (w=FirstAdj(v); w; w=NextAdj(G,v,w)){
--indegree(w); // 弧头顶点的入度减一
if(!indegree[w])Push(S,w);//新产生的入度为零的顶点入栈
}
}
if (count<G.vexnum) printf(“图中有回路”);
4)关键路径:
“关键活动”指的是:该弧上的权值增加将使有向图上的 最长路径的长度增加
事件vi的最早发生时间:从开始点v1到vi的最长路径长度。用ve(i)表示
事件vi的最迟发生时间:在不推迟整个工期的前提下,事件vi允许的最晚发生时间。用vl(i)表示。
ve(j) = Max{ve(i) + dut(<i,j>)}
vl(i) = Min{vl(j) - dut(<i,j>)}
关键活动:l(i) = e(i)的活动。
三、疑难解决
PTA-7-2 六度空间
“六度空间”理论虽然得到广泛的认同,并且正在得到越来越多的应用。但是数十年来,试图验证这个理论始终是许多社会学家努力追求的目标。然而由于历史的原因,这样的研究具有太大的局限性和困难。随着当代人的联络主要依赖于电话、短信、微信以及因特网上即时通信等工具,能够体现社交网络关系的一手数据已经逐渐使得“六度空间”理论的验证成为可能。
假如给你一个社交网络图,请你对每个节点计算符合“六度空间”理论的结点占结点总数的百分比。
解析:
完整代码:
#include <iostream>
#include <fstream>
#include <cstdio>
#include <queue>
#include <vector>
using namespace std;
const int maxn=10005;
int vertices, edges;
vector<int> G[maxn]; //每个vector结构的元素表示:与数组下标代表的结点有边的结点
bool vis[maxn]; //是否访问过
int BFS(int v)
{
for(int i=0; i<maxn; i++)
vis[i]=false;
int tail; //用于记录每层压入时的结点
int last=v; //记录每层的最后一个元素:该层压入之后弹出之前更新:last=temp;
int count=1;
int level=0;
vis[v]=true;
queue<int> q;
q.push(v);
while(!q.empty())
{
int x=q.front(); //弹出x
q.pop();
for(int j=0; j<G[x].size(); j++) //x的一圈压入队列
{
if(!vis[G[x][j]]) //若未被访问过:访问并压入队列
{
vis[G[x][j]]=true;
q.push(G[x][j]);
tail=G[x][j]; //记录压入的结点
count++;
}
}
if(x==last) //一层全部弹出,准备开始弹下一层:弹出的(x)=当前层最后一个元素(last)
{
last=tail; //一层全都压入完后,更新last
level++;
}
if(level==6)
break;
}
return count;
}
int main(int argc, char** argv) {
int x,y;
cin >> vertices >> edges;
//ifstream fin("test.txt");
//fin >> vertices >> edges;
for(int i=1; i<=edges; i++)
{
cin >> x >> y;
//fin >> x >> y;
G[x].push_back(y);
G[y].push_back(x);
}
for(int j=1; j<=vertices; j++)
{
//cout << BFS(j) << endl;
printf("%d: %.2lf%%\n", j, BFS(j)*1.0/vertices*100.0); //格式输出
}
return 0;
}