ch4 图

14 阅读11分钟

图的概念

定义

  • 表示多对多的关系
  • 术语:
    • 顶点:V
    • 边:E
    • 网络:带权值的图
    • 邻接点:有边直接相连的顶点
    • 出度:从某顶点发出的边数
    • 入度:指向某顶点的边数
    • 稀疏图:顶点很多而边很少的图
    • 稠密图:顶点多边也多的图
    • 完全图:对于给定的一组顶点,顶点间都存在边
  • 分类:
    • 有向图<v,w>
    • 无向图(v,w)
  • 抽象数据类型:
    • 数据对象集:G(V,E),一个非空顶点集V和一个有限边集E组成
    • 操作:Graph Create():建立并返回空图
      • Graph InsertVertex(Graph G, Vertex v):将 v 插入 G
      • Graph InsertEdge(Graph G, Edge e):将 e 插入 G
      • void DFS(Graph G, Vertex v):从顶点 v 出发深度优先遍历图 G
      • void BFS(Graph G, Vertex v):从顶点 v 出发宽度优先遍历图 G
      • void ShortestPath(Graph G, Vertex v, int Dist[]):计算图G中顶点到其他任何顶点的距离
      • void MST(Graph G):计算图G的最小生成树

表示方法

邻接矩阵

  • 定义: image.png
  • 特征:对角线元素全0;关于对角线对称
  • 优点:
    • 简单直观
    • 方便检查任一对顶点之间是否存在边
    • 方便查找任意顶点的所有邻接点
    • 方便计算任意顶点的出入度(行对应元素是出度,列对应元素是入度)
  • 缺点:
    • 储存稀疏图浪费空间
    • 统计稀疏图的边浪费时间
  • 优化:对于无向图,这样储存可以节省一半空间 image.png
  • 实现:
#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");
	}
} 

邻接表

  • 定义: image.png

  • 优点:

    • 方便找任意顶点所有的邻接点
    • 节省稀疏图的空间(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所有顶点都不同,则称为简单路径
  • 回路:起点等于重点的路径
  • 连通图:图中任意两顶点之间都连通
  • 连通分量:无向图的极大连通子图
    image.png
  • 强连通:有向图中顶点VW之间存在双向路径
  • 强连通图:有向图中任意两顶点均强连通
  • 强连通分量:有向图中的极大强连通子图
    image.png

题目

拯救007

image.png image.png

image.png

六度空间

image.png

image.png

image.png

image.png

最短路径

在网络(带权图)中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径,就是两点之间最短路径。
分类:

  • 单源最短路径问题:从某固定源点出发,求其到所有其他顶点的最短路径
    • (有向)无权图
    • (有向)有权图
  • 多源最短路径问题:求任意两顶点间的最短路径

无权图单源最短路径

按照递增(非递减)的顺序找出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不直接相连,则初始化为无穷大 image.png
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网络:每个顶点是项目,如果有合理的拓扑序,则必定是有向无环图

image.png

image.png

关键路径

AOE网络 image.png

image.png