图的概念
定义
- 表示多对多的关系
- 术语:
- 顶点:V
- 边:E
- 网络:带权值的图
- 邻接点:有边直接相连的顶点
- 出度:从某顶点发出的边数
- 入度:指向某顶点的边数
- 稀疏图:顶点很多而边很少的图
- 稠密图:顶点多边也多的图
- 完全图:对于给定的一组顶点,顶点间都存在边
- 分类:
- 有向图<v,w>
- 无向图(v,w)
- 抽象数据类型:
- 数据对象集:G(V,E),一个非空顶点集V和一个有限边集E组成
- 操作:Graph Create():建立并返回空图
Graph InsertVertex(Graph G, Vertex v)
:将 v 插入 GGraph InsertEdge(Graph G, Edge e)
:将 e 插入 Gvoid DFS(Graph G, Vertex v)
:从顶点 v 出发深度优先遍历图 Gvoid BFS(Graph G, Vertex v)
:从顶点 v 出发宽度优先遍历图 Gvoid ShortestPath(Graph G, Vertex v, int Dist[])
:计算图G中顶点到其他任何顶点的距离void MST(Graph G)
:计算图G的最小生成树
表示方法
邻接矩阵
- 定义:
- 特征:对角线元素全0;关于对角线对称
- 优点:
- 简单直观
- 方便检查任一对顶点之间是否存在边
- 方便查找任意顶点的所有邻接点
- 方便计算任意顶点的出入度(行对应元素是出度,列对应元素是入度)
- 缺点:
- 储存稀疏图浪费空间
- 统计稀疏图的边浪费时间
- 优化:对于无向图,这样储存可以节省一半空间
- 实现:
#include <bits/stdc++.h>
#define MaxVertexNum 100
typedef int weightType; // 权重
typedef int Vertex; // 顶点
typedef int DataType; // 顶点存的数据
// 图
typedef struct GNode *MGraph;
struct GNode
{ // 图
int Nv; // 顶点数
int Ne; // 边数
weightType G[MaxVertexNum][MaxVertexNum]; // 矩阵
DataType Data[MaxVertexNum]; // 顶点存的数据
};
// 边
typedef struct ENode *Edge;
struct Enode
{
Vertex V1, V2; // 有向边<V1,V2>
weightType Weight; // 权重
};
// 初始化一个n个顶点的图
MGraph Create(int VertexNum)
{
Vertex v, w; // 顶点的横竖坐标
MGraph Graph;
Graph = (MGraph)malloc(sizeof(struct GNode));
Graph->Nv = VertexNum;
Graph->Ne = 0;
for (v = 0; v < VertexNum; v++)
for (w = 0; w < VertexNum; w++)
Graph->G[v][w] = 0;
return Graph;
}
// 插入边
void InsertEdge(MGraph Graph, Edge E)
{
// 插入边<V1,V2>
Graph->G[E->V1][E->V2] = E->Weight;
// 若是无向图,还要插入边<V2,V1>
Graph->G[E->V2][E->V1] = E->Weight;
}
// 建图
MGraph BuildGraph(){
MGraph Graph;
Edge E;
Vertex V;
int Nv,i;
scanf("%d",&Nv); // 读入顶点数
Graph = Create(Nv);
scanf("%d",&(Graph->Ne)); // 读入边数
if(Graph->Ne != 0){
E = (Edge)malloc(sizeof(struct ENode));
for(i=0;i<Graph->Ne;i++){
scanf("%d %d %d",&E->V1,&E->V2,&E->Weight); // 读入每个边的数据
Insert(Graph,E);
}
}
return Graph;
}
// 遍历图
void print(MGraph Graph){
Vertex v,w;
for(v=0;v<Graph->Nv;v++){
for(w=0;w<Graph->Nv;w++)
printf("%d ",Graph->G[v][w]);
printf("\n");
}
}
//简洁版,将矩阵和顶点数边数设置成全局变量
int G[MAXN][MAXN],Nv,Ne;
void BuildGraph1(){
int i,j,v1,v2,w;
scanf("%d",&Nv);
// 初始化图
for(i=0;i<Nv;i++)
for(j=0;j<Nv;j++)
G[i][j] = 0;
scanf("%d",&Ne);
// 插入边
for(i=0;i<Ne;i++){
scanf("%d %d %d",&v1,&v2,&w);
G[v1][v2] = w;
G[v2][v1] = w;
}
}
// 遍历图
void print1(){
int i,j;
for(i=0;i<Nv;i++){
for(j=0;j<Nv;j++)
printf("%d ",G[i][j]);
printf("\n");
}
}
邻接表
-
定义:
-
优点:
- 方便找任意顶点所有的邻接点
- 节省稀疏图的空间(N个头指针+2E个节点)
- 方便计算任意顶点的度(有向图需要构造逆邻接表来计算入度)
-
缺点:不方便检查任意的一对顶点之间是否存在边
-
实现:
#include <stdio.h>
#include <stdlib.h>
#define MaxVertexNum 100
typedef int Vertex;
typedef int DataType;
typedef int weightType;
// 边
typedef struct ENode *ptrToENode;
typedef ptrToENode Edge;
struct ENode
{
Vertex V1, V2; // 有向边<V1,V2>
weightType Weight; // 权重
};
// 图
typedef struct GNode *PtrToGNode;
typedef PtrToGNode LGraph;
struct GNode
{
int Nv; // 顶点
int Ne; // 边
AdjList G; // 邻接表
};
// 邻接表类型
typedef Vonde AdjList[MaxVertexNum];
struct Vonde
{
PtrToAdjVNode FirstEdge; // 表头
DataType Data; // 存顶点的数据
};
// 邻接表内元素
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode
{
Vertex AdjV; // 邻接点下标
weightType Weight; // 权重
PtrToAdjVNode Next; // 下一个
};
// 初始化
LGraph create(int VertexNum)
{
Vertex v, w;
LGraph Graph;
Graph = (LGraph)malloc(sizeof(struct GNode));
Graph->Nv = VertexNum; // 初始顶点数
Graph->Ne = 0; // 初始化边数
// 每条边的 FirstEdge 指向 NULL
for (v = 0; v < Graph->Nv; v++)
Graph->G[v].FirstEdge = NULL;
return Graph;
}
// 插入边
void InsertEdge(LGraph Graph, Edge E)
{
PtrToAdjVNode newNode;
/**************** 插入边<V1,V2> ******************/
// 为 V2 建立新的结点
newNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
newNode->AdjV = E->V2;
newNode->Weight = E->Weight;
// 将 V2 插入到邻接表头
newNode->Next = Graph->G[E->V1].FirstEdge;
Graph->G[E->V1].FirstEdge = newNode;
/*************** 若为无向图,插入边<V2,V1> *************/
newNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
newNode->AdjV = E->V1;
newNode->Weight = E->Weight;
newNode->Next = Graph->G[E->V2].FirstEdge;
Graph->G[E->V2].FirstEdge = newNode;
}
// 建图
LGraph BuildGraph()
{
LGraph Graph;
Edge E;
Vertex V;
int Nv, i;
scanf("%d", &Nv);
Graph = create(Nv);
scanf("%d", &(Graph->Ne));
if (Graph->Ne != 0)
{
for (i = 0; i < Graph->Ne; i++)
{
E = (Edge)malloc(sizeof(struct ENode));
scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
InsertEdge(Graph, E);
}
}
return Graph;
}
// 打印
void print(LGraph Graph)
{
Vertex v;
PtrToAdjVNode tmp;
for (v = 0; v < Graph->Nv; v++)
{
tmp = Graph->G[v].FirstEdge;
printf("%d ", v);
while (tmp)
{
printf("%d ", tmp->AdjV);
tmp = tmp->Next;
}
printf("\n");
}
}
// 简洁版 将邻接链表和边数点数设置成全局变量
typedef struct AdjVNode1 *AdjList1;
struct AdjVNode1
{
int weight; // 权值
int adjv; // 下标
AdjList1 next; // 其后一个
};
AdjList1 Graph[MaxVertexNum];
int Ne, Nv;
// 建图
void BuildGraph1()
{
int i;
int v1, v2, w;
AdjList1 NewNode;
scanf("%d", &Nv);
for (i = 0; i < Nv; i++)
{
Graph[i] = (AdjList1)malloc(sizeof(struct AdjVNode));
Graph[i]->adjv = i;
Graph[i]->next = NULL;
}
scanf("%d", &Ne);
for (i = 0; i < Ne; i++)
{
scanf("%d %d %d", &v1, &v2, &w);
NewNode = (AdjList1)malloc(sizeof(struct AdjVNode));
NewNode->adjv = v1;
NewNode->weight = w;
NewNode->next = Graph[v2]->next;
Graph[v2]->next = NewNode;
NewNode = (AdjList1)malloc(sizeof(struct AdjVNode));
NewNode->adjv = v2;
NewNode->weight = w;
NewNode->next = Graph[v1]->next;
Graph[v1]->next = NewNode;
}
}
void print1()
{
AdjList1 tmp;
int i;
for (i = 0; i < Nv; i++)
{
tmp = Graph[i];
while (tmp)
{
printf("%d ", tmp->adjv);
tmp = tmp->next;
}
printf("\n");
}
}
int main()
{
BuildGraph1();
print1();
return 0;
}
遍历
每进行一次DFS或BFS,就相当于把图所有的连通分量遍历了一遍
深度优先搜索DFS
类似树的前序遍历
void DFS ( Vertex V ){
visited[ V ] = true;
for ( V 的每个邻接点 W )
if( !visited[ W ])
DFS( W );
}
有N个顶点,E条边,时间复杂度是:
- 用邻接表存储,O(N + E)
- 用邻接矩阵存储,O(N^2)
广度优先搜索BFS
相当于树的层序遍历
void BFS( Vertex V ){
queue<Vertex> q;
visited[V] = true;
q.push(V);
while(!q.empty()){
V = q.front(); q.pop();
for( V 的每个邻接点 W ){
if( !visited[ W ]){
visited[W] = true;
q.push(W);
}
}
}
}
有N个顶点,E条边,时间复杂度是:
- 用邻接表存储,O(N + E)
- 用邻接矩阵存储,O(N^2)
连通性
- 连通:从V到W存在一条路径,则称V和W是连通的
- 路径:V到W的路径是一系列顶点的集合,路径的长度是所有边的权重和,如果V到W所有顶点都不同,则称为简单路径
- 回路:起点等于重点的路径
- 连通图:图中任意两顶点之间都连通
- 连通分量:无向图的极大连通子图
- 强连通:有向图中顶点VW之间存在双向路径
- 强连通图:有向图中任意两顶点均强连通
- 强连通分量:有向图中的极大强连通子图
题目
拯救007
六度空间
最短路径
在网络(带权图)中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径,就是两点之间最短路径。
分类:
- 单源最短路径问题:从某固定源点出发,求其到所有其他顶点的最短路径
- (有向)无权图
- (有向)有权图
- 多源最短路径问题:求任意两顶点间的最短路径
无权图单源最短路径
按照递增(非递减)的顺序找出S到各个顶点的最短路
// 起点为S
// dist[W]=S到W的最短距离
// dist[S]=0
// path[W]=S到W路上经过的W前一个点
void Unweighted(Vertex S){
queue<Vertex> q;
q.push(S);
while (!q.empty()){
V = q.front();q.pop();
for (V 的每个临界点 W)
{
dist[W] = dist[V] + 1; // 当前距离上一距离 + 1
path[W] = V; // s 到 w 的必经顶点就是前一个顶点 v
q.push(W);
}
}
}
有权图单源最短路径
Dijkstra算法
- 令 S = {源点s + 已经确定了最短路径的顶点 vi}
- 对任一未收录的顶点v,定义dist[v]为s到v的最短路径长度,但该路径仅经过S中的顶点。即路径{s→(vi∈S)→v} 的最小长度
- 若路径是按照递增(非递减)的顺序生成的,则
- 真正的最短路必须只经过S中的顶点
- 每次从未收录的顶点中选一个dist最小的收录
- 增加一个v进入S,可能影响另外一个w的dist值,dist[w] = min{dist[w],dist[v] + <v,w>的权重}
- 不能解决有负边的情况
void Dijkstra( Vertex s ){
while(1){
V = 未收录顶点中dist最小值;
if( 这样的V不存在 )
break;
collected[V] = true;
for( V 的每个邻接点 W )
if( collected[W] == false )
if(dist[V] + E<V,W> < dist[W]){
dist[W] = dist[V] + E<V,W>;
path[W] = V;
}
}
}
取出未收录顶点中dist最小值和更新dist[W]的操作可以考虑两种方法:
- 直接扫描所有未收录顶点 ——O(|V|)
T = O(|V|^2 + |E|) ——稠密图效果更好 - 将dist存在最小堆中 ——O(log|V|)
更新dist[w]的值 —O(log|V|)
T = O(|E|log|V|) —— 稀疏图效果更好
#include <bits/stdc++.h>
using namespace std;
#define MaxVertex 100//最大顶点数
typedef int Vertex;
int G[MaxVertex][MaxVertex];//邻接矩阵
int dist[MaxVertex]; // 距离
int path[MaxVertex]; // 路径
bool collected[MaxVertex]; // 收录情况
int Nv; // 顶点数
int Ne; // 边数
//初始化图
void build(){
Vertex v1, v2;
int w;//边长
cin >> Nv;
//初始化邻接矩阵
for (int i = 1; i <= Nv;i++){
for(int j=1;j<=Nv;j++)
G[i][j] = 0;
}
//初始化路径
for(int i=1;i<=Nv;i++)
path[i] = -1;
// 初始化距离
for(int i=0;i<=Nv;i++)
dist[i] = 1000000;
// 初始化收录情况
for(int i=1;i<=Nv;i++)
collected[i] = false;
cin >> Ne;
// 初始化边长
for(int i=0;i<Ne;i++){
cin>>v1>>v2>>w;
G[v1][v2] = w; // 有向图
}
}
// 初始化源点的距离和路径信息
void crate(Vertex s){
dist[s] = 0;
collected[s] = true;
for(int i=1;i<=Nv;i++)
if(G[s][i]){
dist[i] = G[s][i];
path[i] = s;
}
}
// 遍历查找未收录顶点中dist最小者
Vertex FindMin(Vertex s){
int min = 0; // 之前特地把 dist[0] 初始化为正无穷
for(Vertex i=1;i<=Nv;i++)
if(i != s && dist[i] < dist[min] && !collected[i])
min = i;
return min;
}
//Dijkstra算法查找s到所有顶点的最短路径
void Dijkstra(Vertex s){
crate(s);
while(true){
Vertex V = FindMin(s); // 找到
if(!V)
break;
collected[V] = true; //收录
for(Vertex W=1;W<=Nv;W++)
if(!collected[W] && G[V][W]){ // 如果未被收录
if(dist[V] + G[V][W] < dist[W]){
dist[W] = G[V][W] + dist[V];
path[W] = V;
}
}
}
}
多源最短路径
- 直接将单源最短路算法调用|V|遍
T = O(|V|^3 + |E|×|V|) ——对于稀疏图效果好 - Floyd算法
T = O(|V|^3) ——对于稠密图效果好
Floyd算法
- D^k[i][j]=路径{ i → {l ≤ k} → j } 的最小长度
- 最初的D^-1是全0的邻接矩阵
- 如果i和j不直接相连,则初始化为无穷大
void Floyd(){
for( i = 0; i < N; i++ )
for( j = 0; j < N; j++ ){
D[i][j] = G[i][j];
path[i][j] = -1;
}
for( k = 0; k < N; k++ )
for( i = 0; i< N; i++)
for( j = 0; j < N; j++ )
if( D[i][k] + D[k][j] < D[i][j] ) {
D[i][j] = D[i][k] + D[k][j];
path[i][j] = k;
}
}
题目
哈利波特的考试 p94-p97
最小生成树
- 定义:是从连通图里挑出来的树,包含全部v个顶点和v-1条边,没有回路且不唯一,边的权值最小,而且加上任意一条边都构成回路
- 贪心:每一步都加上图里权重最小的边,且只能有v-1条边,不能有回路
Prim算法
每一步都增加一条权重最小的边,最后连接起所有的顶点,时间复杂度:T = O(|V|^2),稠密图合算
void Prim(){
MST = {s}; // parent[s] = -1
while(1){
V = 未收录顶点中dist最小者; // dist[V] = E<V,W> 或 正无穷
if ( 这样的V不存在 )
break;
dist[V] = 0; // 将V收录进MST
for ( V 的每个邻接点 W )
if ( dist[W]!= 0)
if ( E<V,W> < dist[w] ){
dist[W] = E<V,W>;
parent[W] = V;
}
}
if ( MST 中收的顶点不到|V|个)
Error ( "图不连通" );
}
Kruskal算法
每次把整张图中最小的边连在一起然后看成一个整体,继续排序,类似哈夫曼树,T = O(|E|log|E|),稀疏图合算
void Kruskal ( Graph G ){
MST = { };
while ( MST 中不到|V|-1条边 && E中还有边 ) {
从 E 中取一条权重最小的边 E<V,W>; // 最小堆
将 E<V,W> 从 E 中删除;
if ( E<V,W> 不在 MST 中构成回路 ) // 并查集
将 E<V,W> 加入MST;
else
彻底无视 E<V,W>;
}
if ( MST 中不到|V|-1条边 )
Error("图不连通");
}
拓扑排序
- 拓扑序:如果图中从V到W有一条有向路径,则V一定排在W之前。满足此条件的顶点序列称为一个拓扑序
- 拓扑排序:获得一个拓扑序的过程
- AOV网络:每个顶点是项目,如果有合理的拓扑序,则必定是有向无环图
关键路径
AOE网络