【数据结构】图
定义 图$G=(V,E)$无序对,$V(G)$记为顶点集,$E(G)$为边集 网 带权图称做网
存储结构 1.邻接矩阵:顶点$i,j$之间有边 则矩阵的$i,j$元为该边权大小,无边设置为inf,环设置为0 具体实现如下:
const int MAXN = 1000; //最大顶点数
int matrix[MAXN][MAXN];
int main()
{
int u,v,w;
int n,m;
cin>>n>>m;
for(int i = 1; i <= n; i++)
{
cin>>u>>v>>w; //u v 连边 权值w
matrix[u][v] = w;
// 下一行在无向图时使用
// matrix[v][u] = w; // 无向图的对称性
}
}
2.邻接表:每个顶点是一个动态数组,存放其指向的另一个顶点边。 逆邻接表:存放指向该顶点的边。 具体实现如下:
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1000; //最大顶点数
struct Edge{
int u;
int v;
int w;
Edge(int u,int v,int w):u(u),v(v),w(w){}
}; // 此结构并非必要 在无权图的情况下可以直接存储其到达的其他顶点
vector<vector<Edge>> adject;
int main()
{
int u,v,w;
int n,m;
cin>>n>>m;
adject.resize(n+1); //n个顶点
for(int i = 1; i <= n; i++)
{
cin>>u>>v>>w; //u v 连边 权值w
adject[u].push_back(Edge(u,v,w)); // u -> v
// v -> u 取决于有向图还是无向图
}
}
基本运算
1.创建图:见上文 2.输出图 3.销毁图
其他结构
1.十字链表:顺邻接表和逆邻接表结合 2.邻接多重表
遍历 1.深度优先遍历 递归 2.广度优先遍历 用队列 每次取出一个进行扩展 3.非连通图遍历 即对每个顶点进行搜索 (待填坑...)
最小生成树
生成树:含有该图的所有顶点的该图的极小连通子图,同时是一个树的结构(即不能产生回路) 生成树的性质:含有$n$个顶点的生成树一定只有$n-1$条边 最小生成树:生成树集合中权值和最小的树。
求最小生成树的算法 1.Prim算法:
描述:考虑2个顶点集U和V,初始时$U={{v_{0}}}$作为初始点,引入一个变量$d$存储$V-U$集合中每个顶点到当前集合$U$的所有顶点的距离(权值)的最小值 (即对$U$中所有顶点$i$,以及$V-U$中所有顶点$j$取$d[j]=min_{i:{1->n}}(matrix[i][j])$) 每次在$V-U$集合中加入一个距离$U$最近的顶点,加入该顶点后对两个集合之间的最小距离进行更新,直到所有顶点都已经加入(即统计总共更新次数为$n-1$次) 实现:
/* Prim 2024.5.22 */
#include <iostream>
#include <cstring>
#define INF 9999
using namespace std;
const int MAXN = 1000; //最大顶点数
int matrix[MAXN][MAXN];
struct Edge{
int nodeto;
int cost;
Edge(int nodeto,int cost):nodeto(nodeto),cost(cost){}
Edge():nodeto(INF),cost(INF){}
}min_edge[MAXN]; // 记录V-U中顶点到U中顶点的最小距离。形式上是一个单向边
int main()
{
int u,v,w;
int n,m;
cin>>n>>m;
memset(matrix,INF,sizeof(matrix));
for(int i = 1; i <= n ; i++)
{
matrix[i][i] = 0;
}
for(int i = 1; i <= m; i++)
{
cin>>u>>v>>w;
matrix[u][v] = w;
matrix[v][u] = w; // 本例使用无向图
}
int v0;
cin>>v0; //作为最小生成树的根节点
min_edge[v0].cost = 0; //意味着该顶点加入了U
for(int i = 1;i<=n;i++)
{ // 即初始化 相当于第0步更新
if(matrix[i][v0]) {
// 有向图时为v0 i
min_edge[i] = Edge{v0,matrix[i][v0]};
}
}
for(int i = 1; i < n; i++)
{
int mincost = INF;
int min_node = 0;
for(int i = 1; i <= n; i++)
{ // 找到当前 V-U 中 距离 U 最小的顶点 注意顶点i的cost不等于0才表示该顶点没有加入U
if(min_edge[i].cost > 0 and min_edge[i].cost<mincost)
{
mincost=min_edge[i].cost;
min_node = i;
}
}
min_edge[min_node].cost = 0; // 标记该新找出的顶点新加入了U
cout<<min_edge[min_node].nodeto<<"->"<<min_node<<endl; // 输出该边 或者做其他事情
/* 通过该新加入的顶点更新V-U中所有顶点与U中顶点的最小权值 */
for(int i = 1; i <= n; i++)
{
if(min_edge[i].cost != 0 and matrix[min_node][i] < min_edge[i].cost)
{
min_edge[i].cost = matrix[min_node][i];
min_edge[i].nodeto = min_node;
}
}
}
}
2.Kruskal算法: 描述: 对边从小到大进行建树,具体描述:设$T=(U,TE)$是$G$的最小生成树,初始时将$U$设置为$V$,即包含$G$中所有顶点,而$TE$设置为空,即等待加边,然后从边集中挑选最小的一个边,如果这个边加上之后,$T$不构成回路,则加入该边,直到$TE$有$n-1$条边为止。 性质: 此算法与Prim算法生成的最小生成树可能是不同的。 实现: 由于每次加边都需要判断回路,使用搜索判断回路效率过低。因此考虑采用并查集,初始$vset[i]=i$代表顶点$i$属于$i$集合,每次迭代加边时,判断当前边连接的两个顶点是否有相同的集合标号,若没有,则加入该边,并合并并查集。 Kruskal实现如下:
/* Kruskal 2024.5.24 */
#include <vector>
#include <iostream>
#include <algorithm>
#include "union_set.h"
#define INF 9999
#define MAXN 1000
using namespace std;
struct Edge{
int u,v,w;
Edge():u(0),v(0),w(0){}
Edge(int u,int v,int w):u(u),v(v),w(w){}
};
vector<Edge> adj; /* 存该图的边 */
bool cmp(Edge A,Edge B)
{
return A.w<B.w; /* 根据边权值递增 */
}
int main()
{
int n,m;
cin>>n>>m;
int i,j,w;
for(int ite = 1;ite<=m;ite++)
{
cin>>i>>j>>w;
adj.push_back(Edge(i,j,w));
adj.push_back(Edge(j,i,w)); /* 无向图存双向边 */
}
MAKE_SET(n);
sort(adj.begin(),adj.end(),cmp);
i = 0; /* 当前加了多少条边到树中 */
j = 0; /* 当前指向的待加边 */
while(i<n-1)
{
Edge tmp = adj[j];
if(FIND_SET(tmp.u)!=FIND_SET(tmp.v))
{
/* 不构成回路 加边 */
cout<<tmp.u<<"-"<<tmp.v<<" "<<tmp.w<<endl;
UNION(tmp.u,tmp.v);
i++;
}
j++;
}
}
并查集定义与实现如下: union_set.h
#include <iostream>
#define MAXN 1000
using namespace std;
/* 在这里假定每个元素的编号为它数组的下标 */
int vset_[MAXN]; /* 作为集合的数组 存储每个元素的代表元素 */
int rank_[MAXN]; /* 按秩合并的辅助数组 记录该结点到代表元素的高度 */
/* 初始化并查集,每个元素属于自己代表的集合 */
void MAKE_SET(int n);
/* 查找x元素属于的集合的代表 */
int FIND_SET(int x);
/* 合并2个集合 */
void UNION(int x,int y);
union_set.cpp
#include "union_set.h"
void MAKE_SET(int n)
{
for (int i = 1; i <= n; i++)
{
vset_[i] = i; /* 每个动态集合仅包含自己作为代表 */
rank_[i] = 0;
}
}
int FIND_SET(int x)
{
return (x==vset_[x]?x:vset_[x]=FIND_SET(vset_[x]));
/* 若双亲是自己则返回,否则向上查找并缩短路径 */
}
void UNION(int x,int y)
{
x = FIND_SET(x);
y = FIND_SET(y);
if(rank_[x]>rank_[y]) /* 此时将y合并到x树中 */
{
vset_[y] = x;
}else{
vset_[x] = y;
if(rank_[x]==rank_[y])
{
rank_[y]++;
}
}
}
无向图的连通分量和生成树: 对无向图进行一次遍历即可形成生成树 1.深度优先生成树 2.广度优先生成树 3.生成树森林(非联通图的各连通分量的生成树组成的森林)