1.定义
①树(Tree):n(n≥0)个结点的有限集。若 n = 0,称为空树;若n>0,则它满足如下两个条件:
(1)有且仅有一个特定的称为根 (Root) 的结点;
(2) 其余结点可分为 m (m≥0) 个互不相交的有限集 T1, T2,T3, …, Tm,其中每一个集合本身又是一棵树,并称为 根的子树(SubTree)。
②图 (Graph) :一种复杂的非线性数据结构,由顶点集合及顶点间的关系(也称弧或边)集合组成。可以表示为:G=(V, {VR})其中 V 是顶点的有穷非空集合; VR 是顶点之间关系 的有穷集合,也叫做弧或边集合。弧是顶点的有序对,边是顶点的无序对。
③度:无向图中顶点 v 的度是和 v 相关联的边的数目,记为TD(v)。
入度:有向图中以顶点 v 为终点的弧数目称为 v 的入度,记ID(v)
出度:有向图中以顶点 v 为起点的弧数目称为 v 的出度,记OD(v)。
其中:TD(v) = ID(v) + OD(v)
④连通分量:无向图的极大连通子图;任何连通图的连通分量只有一个,即其本身;非连通图有多个连通分量(非连通图的每一个连通部分)。
强连通图:有向图G中,若对于V(G)中任意两个不同的顶点vi和vj,都存在从vi到vj以及从vj到vi的路径,则称G是强连通图。
强连通分量:有向图的极大强连通子图;任何强连通图的强连通分量只有一个,即其本身;非强连通图有多个强连通分量。
2.图的存储方式
①图的存储结构之数组表示法(邻接矩阵表示法)
对于一个具有n个顶点的图,可用两个数组存储。其中一个一维数组存储数据元素(顶点)的信息,另一个二维数组(图的邻接矩阵)存储数据元素之间的关系(边或弧)信息。
邻接矩阵:设 G = (V, {VR}) 是具有 n 个顶点的图,顶点的顺序依次为 {v1, v2, …, vn},则 G 的邻接矩阵是具有如下性质的 n 阶方阵:
特点:无向图的邻接矩阵对称,可压缩存储;有n个顶点的无向图需存储空间为 n(n-1)/2。
有向图邻接矩阵不一定对称;有n个顶点的有向图需存储空间为n²,空间复杂度O(n2),用于稀疏图时空间浪费严重。
无向图中顶点vi的度 TD(vi) 是邻接矩阵中第i行1的个数。
有向图中顶点vi的出度是邻接矩阵中第i行1的个数。有向图中顶点vi的入度是邻接矩阵中第i列1的个数。
邻接矩阵使用场合:
数据规模不大n <= 1000,m越大越好
稠密图最好用邻接矩阵
图中不能有多重边出现
②邻接表(类似于树的孩子链表表示法)
特点:若无向图中有n个顶点、e条边,则其邻接表需n个顶点表结点和2e个边表结点。适宜存储稀疏图。无向图中顶点 vi 的度为第 i 个单链表中的结点数。
邻接表使用场合:
顶点数很多n>1000,边数却不多。
采用邻接表存储后,很多算法的复杂度也都是跟边数有关。
连通性的问题很多情况边数不多,多采用邻接表存储方式
3.并查集
英文:Disjoint Set,即“不相交集合”
将编号分别为1…N的N个对象划分为不相交集合,
在每个集合中,选择其中某个元素代表所在集合。
常见两种操作:
合并两个集合
查找某元素属于哪个集合
4.具体问题
①最小生成树问题
生成树:由G的n-1条边构成的无环的子图,这些边的集合成为生成树。
最小生成树:所有生成树中权值最小的一个边集T为最小生成树,确定树T的问题成为最小生成树问题。
Prim算法
设G=(V,E)是连通带权图,V={1,2,…,n}。
构造G的最小生成树的Prim算法的基本思想是:首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件iÎS,jÎV-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。
在这个过程中选取到的所有边恰好构成G的一棵最小生成树。
Kruskal算法:
将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。
最终得到的结果就是最小生成树。并查集
Kruskal算法步骤:
把原始图的N个节点看成N个独立子图
每次选取当前最短的边(前提操作是?),看两端是否属于不同的子图;若是,加入;否则,放弃;
循环操作该步骤二,直到有N-1条边
#include <stdio.h>
#include <iostream>
using namespace std;
#include <algorithm>
const int N=105;
int father[N];
int find(int x){
if(x!=father[x])
father[x]=find(father[x]);
return father[x];
}
struct edge{
int x,y,v;
}e[N*(N-1)/2];
int cmp(edge e1,edge e2){
return e1.v<e2.v;
}
int main(){
int n;
while(scanf("%d",&n)!=EOF&&n){
for(int i=0;i<=n;i++){
father[i]=i;
}
n=n*(n-1)/2;
for(int i=0;i<n;i++){
scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].v);
}
sort(e,e+n,cmp);
int res=0;
for(int i=0;i<n;i++){
int x=find(e[i].x);
int y=find(e[i].y)
if(x!=y) {
res+=e[i].v;
father[x]=y;
}
}
printf("%d\n",res);
}
return 0;
}
②单源点最短路径问题
问题描述:给定带权有向图G=(V, E)和源点v∈V,求从v到G中其余各顶点的最短路径。
Dijkstra算法
基本思想:设置一个集合S存放已经找到最短路径的顶点,S的初始状态只包含源点v,对vi∈V-S,假设从源点v到vi的有向边为最短路径。以后每求得一条最短路径v, …, vk,就将vk加入集合S中,并将路径v, …, vk , vi与原来的假设相比较,取路径长度较小者为最短路径。重复上述过程,直到集合V中全部顶点加入到集合S中。
Dijkstra算法——伪代码
1. 初始化数组dist、path和s;
2. while (s中的元素个数<n)
2.1 在dist[n]中求最小值,其下标为k;
2.2 输出dist[j]和path[j];
2.3 修改数组dist和path;
2.4 将顶点vk添加到数组s中;
计算单源最短路径问题的Dijkstra算法
#define NUM 100
#define maxint 10000
//顶点个数n,源点v,有向图的邻接矩阵为c
//数组dist保存从源点v到每个顶点的最短特殊路径长度
//数组prev保存每个顶点在最短特殊路径上的前一个结点
void dijkstra(int n, int v, int dist[], int prev[], int c[][NUM])
{
int i,j;
bool s[NUM]; //集合S
//初始化数组
for(i=1; i<=n; i++)
{
dist[i] = c[v][i];
s[i] = false;
if (dist[i]>maxint) prev[i] = 0;
else prev[i] = v;
}
//初始化源结点
dist[v] = 0;
s[v] = true;
//其余顶点
for(i=1; i<n; i++)
{
//在数组dist中寻找未处理结点的最小值
int tmp = maxint;
int u = v;
for(j=1; j<=n; j++)
if( !(s[j]) && (dist[j]<tmp))
{
u = j;
tmp = dist[j];
}
s[u] = 1; //结点u加入s中
//利用结点u更新数组dist
for(j=1; j<=n; j++)
if(!(s[j]) && c[u][j]<maxint)
{
//newdist为从源点到该点的最短特殊路径
int newdist = dist[u]+c[u][j];
if (newdist<dist[j])
{
//修正最短距离
dist[j] = newdist;
//记录j的前一个结点
prev[j] = u;
}
}
}
}
Bellman-Ford算法思想
Bellman-Ford算法构造一个最短路径长度数组序列dist 1 [u], dist 2 [u], …, dist n-1 [u]。其中:
dist 1 [u]为从源点v到终点u的只经过一条边的最短路径长度,并有dist 1 [u] =Edge[v][u];
dist 2 [u]为从源点v最多经过两条边到达终点u的最短路径长度;
dist 3 [u]为从源点v出发最多经过不构成负权值回路的三条边到达终点u的最短路径长度;
……
dist n-1 [u]为从源点v出发最多经过不构成负权值回路的n-1条边到达终点u的最短路径长度;
算法的最终目的是计算出dist n-1 [u],为源点v到顶点u的最短路径长度。
算法实现:
#define MAX_VER_NUM 10 //顶点个数最大值
#define MAX 1000000
int Edge[MAX_VER_NUM][MAX_VER_NUM]; //图的邻接矩阵
int vexnum; //顶点个数
void BellmanFord(int v) //假定图的邻接矩阵和顶点个数已经读进来了
{
int i, k, u;
for(i=0; i<vexnum; i++)
{
dist[i]=Edge[v][i]; //对dist[ ]初始化
if( i!=v && dis[i]<MAX ) path[i] = v; //对path[ ]初始化
else path[i] = -1;
}
for(k=2; k<vexnum; k++) //从dist1[u]递推出dist2[u], …,distn-1[u]
{
for(u=0; u< vexnum; u++)//修改每个顶点的dist[u]和path[u]
{
if( u != v )
{
for(i=0; i<vexnum; i++)//考虑其他每个顶点
{
if( Edge[i][u]<MAX &&
dist[u]>dist[i]+Edge[i][u] )
{
dist[u]=dist[i]+Edge[i][u];
path[u]=i;
}
}
}
}
}
}