建图,就是将一个图放进内存的算法。
常用的建图方式有三种:邻接矩阵,邻接链表(vector建表),链式前向星。做题时我个人常用是邻接矩阵和链式前向星,链式前向星是最灵巧和节约空间的,深受ACMER喜爱。但同时也是三种算法里最难理解的。一旦理解,就是如鱼得水,所以我会着重讲链式前向星。后续图论的题,也推荐大家尽量熟悉和使用链式前向星。
用二维数组模拟建图,作为最经典和最简单的方式,这种算法最为直观,也深受新手喜爱。它最大的缺点是占用空间太大!在稀疏图中,浪费空间极其严重。(稀疏图即点与点之间联系不多的图)
现有以下无向加权图:
(注:以上图像非原创,仅用于个人复习和教学,如有侵权立删。那个水印它自己加的)
通过邻接矩阵存储:
先看看结果:
对照着上图自己看一看。
*图中第一个值的地址为map[1] [1] 。
简单说明一下邻接矩阵建加权图的特点:
1.一个顶点到它自己的距离为0,所以对角线上的值都是0.
2.如果两个顶点之间没有连线,则用-1或者INF(无限大,通常用0x3f3f3f3f配合memset使用,图中为1061109567)表示。
3.如果两个顶点之间有连线,则用权值表示,如上图所示。map[1] [2]表示顶点1到顶点2的距离是30.
代码,详细说明都在代码里:
#include
#include
#include
#include
using namespace std;
const int MAX_N = 200;//最大节点个数
const int INF = 0x3f3f3f3f;//无限大标志,必须配合memset使用才能表示无限大!
//邻接矩阵
int map[200][200];//这里的map只是一个变量名字,不是C++stl里那个Map!!
//初始化函数
void init(){
memset(map,INF,sizeof(map));//这句话的意思是把map二维数组里的值全置为无限大
}
//建图
void get_map(int n,int m)
{
//每个顶点到它自己的距离是0
for(int i = 1;i<=n;i++)
{
map[i][i] = 0;
}
printf("请输入起点、终点,边权:\n");
printf("顶点编号不得大于顶点个数\n");
for(int i = 0; i
无权图建图方式和有权图大同小异,无权图就是没有边权的图,那么有边值为1,没有边值为0就好。
代码里每一个注释都看看,都是细节。C++和C区别不大,这里尽量都用的C的格式,能看懂。
实际上vector建图不是严格意义上的邻接链表建图,因为真正的邻接链表建图要向链表那样写,用vector是在模拟链表。说他是邻接链表其实不够准确。这种建图方式我个人用的少,看个人喜好吧。
vector是C++ STL里一个容器,可变数组,当现数组满了的时候,它会重新分配更大的数组,然后将原数组的值一次拷贝到新数组(实际上是一种比较蠢的动态方式)。现阶段只需要会用它建图就好。
简单来说,一个顶点连接的终点连成一个链表,然后有几个顶点就有几个链表。
图示:
邻接链表的形式:
(终于还是动手画图了呜呜,总用别人的也不太好)
我们发现这样的话只能保存连接到哪个顶点,但是不能保存边权值。所以我们需要一个结构体,既保存终点的编号,同时保存边权值。
typedef struct Node{
int v;//终点编号
int w;//起点到终点的边权
}Node;
所以:
vector建图的优点:
1.写起来方便,动态分配。比起邻接矩阵,节省了很多无效空间。
缺点:不太了解,我觉得重分配数组很蠢。(个人看法)
采用vector模拟邻接链表建图:
还是一样,先上结果。
对照着上图应该很好看懂。
代码:
关于vector的用法可以自己去学学。
#include
#include
#include
#include
#include
using namespace std;
const int MAX_N = 200;//最大节点个数
typedef struct Node{
int v;//终点编号
int w;//起点到终点的边权
}Node;
vector map[MAX_N];//MAX_N为最大顶点个数
//建图
void get_map(int m)
{
printf("请输入边的起点 终点 权值:\n");
for(int i = 1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
Node tmp;
tmp.v = v;tmp.w = w;
map[u].push_back(tmp);//将终点节点放进链表末尾
tmp.v = u;tmp.w = w;//如果是有向图,这句和下面一句不用加
map[v].push_back(tmp);
}
}
//输出图
void print_map(int n){
for(int i = 1;i<=n;i++){
printf("起点%d->",i);
for(int j = 0;j
稍微有点难讲,我尽量用自己的理解说清楚。如果没学懂,有时间一定要去参考各种视频和文章弄懂这个,真的超级有用。现在图论的题解很多都是用的链式前向星,不学题解都看不明白。
上述邻接矩阵和邻接链表的形式,我们都是存的点与点的信息,在邻接矩阵中,如果两个点i,j之间有边,则map[i][j]有不为0且不为无穷大INF的值。在邻接链表中,我们每个链表的表头存的是起点,链表中每个节点存与起点直接连接的终点以及到终点的边权的信息。而在链式前向星中,我们将换成存边的形式来存图。
现有以下图:
链式前向星所用结构体和相关变量、数组:
const int MAX_N = 200; //最大顶点数
const int MAX_M = 2e5; //最大边数
//___________链式前向星___________
typedef struct E{
int v;//这条边指向的终点
int nxt;//跳转到相关的下一条边(下面会说)
int w;//这条边的权值
}E;//存边的结构体
E edge[MAX_M];//存边数组
int head[MAX_N];//下面会说
int cnt = 0;//边的编号,下面会说
还是根据结果来倒推链式前向星到底是什么东西。
上述图以链式前向星形式存放的最终形式:
v和w就不多说了,应该很好看出来是什么,这个nxt,它的规律其实是:某一条边的起点连接的上一个编号比它小的边。(这也是为什么它叫前向星的原因),比如我们看顶点A,A有四条边,分别是1,2,6,8.表格中8的nxt是6,6的nxt是2,2的nxt是1,1没有下一条边了,所以1的nxt是-1.
这样我们就有规律遍历出同一个起点的所有边了。但是还有一个问题,我们怎么知道一个顶点的第一条边是什么呢?所以我们需要一个head数组来储存每一个顶点的第一条边。
每个下标为顶点编号,对应的值为以该顶点为起点的编号最大的一条边。
注意这是个有向图,待会儿的代码里会说在哪一步存无向图。
上述最终存放表格看懂了就理解了链式前向星的原理。接下来一步一步放代码并解析。
首先我们应该初始化数组和相关变量:
//初始化函数
void init()
{
memset(head,-1,sizeof(head));//将head数组的值全置为-1.
cnt = 0;//初始化边的编号
}
加一条边的函数:
void add(int u,int v,int w)//u起点,v终点,w权值
{
edge[++cnt].v = v;
edge[cnt].w = w;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
这个代码看不懂最好自己调试时看看egde数组和head数组是怎么变化的。
关于怎么遍历会在代码里讲。在输出图那个函数里,一定要看看,不然你只会存图不会遍历。
#include
#include
#include
#include
#include
using namespace std;
const int MAX_N = 200;//最大节点个数
const int MAX_M = 2e5;//完全图的边数为n*(n-1)/2,即n^2级别
typedef struct Node{
int v;//终点编号
int w;//起点到终点的边权
}Node;
//___________链式前向星___________
typedef struct TT{int v,nxt,w;}E;
E edge[MAX_M];
int head[MAX_N];
int cnt = 0;
//初始化函数
void init(){
memset(head,-1,sizeof(head));
cnt = 0;
memset(edge,0,sizeof(edge));//这一句写不写都无所谓
}
//加边
inline void add(int u,int v,int w)
{
edge[++cnt].v = v;//注意这里是++cnt
edge[cnt].w = w;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
//建图
void get_map(int m)
{
printf("请输入边的起点 终点 权值:\n");
for(int i = 1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
//add(v,u,w); //如果是无向图,加上这一句,即反过去加一条边
}
}
//输出图
void print_map(int n){
//以顶点形式遍历边
for(int i = 1;i<=n;i++){
printf("起点%c:",i+'A'-1);
//以下是遍历某一个顶点的所有边,e代表边的编号,~e等同于e!=-1
for(int e = head[i];~e;e = edge[e].nxt){
int v = edge[e].v;
int w = edge[e].w;
printf("\t边编号:%d,终点:%c,权值:%d",e,v+'A'-1,w);
}
printf("\n");
}
}
int main(){
int n,m;
init();//记得初始化
printf("请输入顶点个数,边个数:\n");
scanf("%d %d",&n,&m);
//建图
get_map(m);
//输出图
print_map(n);
return 0;
}
//测试数据
/*
5 9
1 2 10
1 3 20
3 5 30
4 5 90
2 4 80
1 5 40
2 3 50
1 4 60
2 5 70
*/
运行结果:
这个结果就是之前那个图,加边的时候顶点A即1,B即2...E即5.
邻接矩阵优点:
直接,对于新手较为友好,上手简单。但是限制太大,最多只能开20000*20000左右,就是最多20000个顶点。大于这个数就会爆栈。而且有较多内存浪费。
邻接链表(vector)建图:
个人没怎么用过,这种建图步骤较为简单,也是推荐建图方式吧。只是说vector采用可变数组的方式,动态变化时都会耗费额外的时间复制数组,再加上本身就是STL,速度较慢。
链式前向星:
优点:太多,直接说缺点吧,新手不易上手。其他我觉得没什么缺点了。