浙江科技学院ACM新生培训图论讲义1

图论讲义P1

1 图

在解决问题时,我们通常把一些问题通过建立图来得以更好的解决。
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在图中的数据元素,我们称之为顶点(Vertex),顶点集合有穷非空。在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。(解释来源网络)
图的边通常也称为弧,弧头是箭头指向方向。
图也大致分为有向图和无向图,它们的区别在于有向图的边是带方向的,区分弧头和弧尾(出边、入边)
对于每个点,有以下一些概念比较重要
度:有多少条边连向这个点
出度:该点有多少条出边。
入度:该点有多少条入边。
在这里插入图片描述
(图片来源网络,侵删)
图中e->a的边称为e的出边a的入边,其中弧头指向a。
图中a的入度和出度各1。

2 邻接矩阵

存图的方法有很多种,下面我们介绍其中一种方法,邻接矩阵。
邻接矩阵通常用一个二位数组表示一个矩阵。其中每一维度的下标表示节点,矩阵(二位数组)表示对应的边的信息。
一般来说,对于有向图,二位数组的第一维存的是弧尾指向的节点,第二维度是弧头指向的节点,数组中存的是边的相关信息。
对于无向图来说,二位数组中边的信息是关于对角线i=j对称的,其中在带权图中,自己到自己的距离标为0。
在这里插入图片描述
(图片来源网络,侵删)
如图就是一个有向图的邻接矩阵。
在这里插入图片描述
(图片来源网络,侵删)
如图就是一个无向图的邻接矩阵。

ElemType G[MAXN_SIZE][MAXN_SIZE];
memset(G,0,sizeof G)或者memset(G,inf,sizeof G);
void Add_egde(int s,int t,ElemType info){
	G[s][t]=info;
	//G[t][s]=infol
}
也可以用结构体存更多的信息;

一般情况下,邻接矩阵用于稠密图的存图,稠密图是指相对点的数量,边较为多的图,其中e=v*(v-1)/2 成为完全图。如果在点较多,而边叫少的情况下,往往会用另外一种方式存图。

3 邻接表

在算法竞赛的图论题目中,邻接表往往是一种更为常用的存图方法,相比于邻接矩阵,邻接表的链式存图方法更加节约空间,也更加方便图的访问。
在算法竞赛中,我们常用的邻接表有*链式前向心 和 使用c++中的向量vector来模拟邻接表来存图。而不是对每一个节点都建一个链表来表示顶点向量。
从顶点出发,指向其他节点的边以及相关信息,称为顶点向量上的分量。

3.a.链式前向心

链式前向心是对所有顶点,建立顶点向量(使用数组模拟),顶点向量的每一个分量,存当前点的出边,对每一条边都进行编号,再用一个数组来存每条边的信息(比如边权等,弧头指向的节点是必须要有的,否则没法对图进行遍历访问)。

D
|
|e1
|   e2
A------B
\
 \ e3
  \
  	C
 A  e1 -> e2 -> e3 ^
 B e2^
 C e3^
 D e1^

这就是一无向图的链式前向心结构图。对于点A来说e1就是A的顶点向量的第一个分量,也称为顶点向量A的头节点,每次访问A的顶点向量,必须从e1开始访问。一般情况下,我们在顶点向量中插入边时,通常都是往头节点的前面插入的,所以往往最后插入的边位于顶点向量的头节点。

struct Nood//定义顶点
{
	int head;//当前顶点顶点向量的头节点(第一个分量)
	ElemType info;
}v[MAXN_SIZE];
struct Edge//定义边
{
	ElemType info;
	int to,nxt;//当前边的必要信息,包括弧头指向节点和所在顶点向量上的下一个分量
}e[MAXN_SIZE];
int tot;统计边的编号
void Add_Edge(int s,int t,ElemType info)//在s,t间插入一条s到t的边
{
	tot++;//建一条新边
	e[tot].to=t;//当前边的弧头指向t
	e[tot].info=info;//增加边的额外信息,比如边权
	e[tot].nxt=v[s].head;//将s的头节点(顶点向量的第一个分量)作为边tot所在分量的下一个分量
	v[s].head=tot;//将tot设置为顶点向量的头节点(第一个分量)
}
void Gvisit(int u)
{
	for(int i=v[u].head;i!=0;i=e[i].nxt)
	{..........}//从向量的头节点开始,访问当前点的所有分量
}
3.b.vector模拟

使用vector模拟邻接表的存图方法,可以让代码更加精简。而且vector存图操作起来相对链式前向心更加方便。
在学习vector存图之前,必须先学会c++中数据结构vector的使用方法!
对于每个顶点,我们只要将它的顶点向量上所对应的边(弧)弧头所指向的节点以及相关信息加入到以该点为下标的vector容器中即可。与常规的临阶表有所不同的是,vector最先加入的分量为第一个分量。

struct Nood//顶点向量的分量结构体
{
	int to;
	ElemType info;
};
vector <Nood> G[MAXN_SIZE];
void Add_Edge(int s,int t,ElemType info)//在s,t间插入一条s到t的边
{
	Nood  Vector_Component;
	 Vector_Component.to = t;//当前分量弧头指向
	  Vector_Component.info = info;//当前边(弧)的相关信息
	 G[s].push_back( Vector_Component);//加入到s的顶点向量中
}
void Gvisit(int u)
{
	for(auto v:G[u])//迭代器自动访问u的顶点向量上的所有分量信息
	{............}
}

除了在某些特殊情况下(比如需要对反图进行操作),大部分题目更推荐使用vector模拟法存图。

4 简单图的连通性问题

图的连通性问题有许多,比如强连通分量(能互相到达的顶点集合)、最小生成树问题(选择最少的 v - 1 边,使得所有点连通,并且边权和最小)、双连通分量、割点和桥等等。
这次我们只解决最小生成树。

4.a.并查集

在 *最小生成树算法克鲁斯卡尔 之前,我们先补充一个前置知识:并查集。
并查集是一种对集合进行操作的操作方法,分为合并两个集合,以及查询两个元素是否在同一个集合中。
在所有元素没有被合并之前,每个元素都是一个集合,每个集合标记为该元素自己。
当合并两个元素时,我们只要像建树一样,将一个元素作为儿子节点,另一个元素作为父亲节点,把儿子节点连到父亲节点上,就算合并完成,并且将父亲节点标记为根节点以及将合并后的集合标记为该根节点。
同理,合并两个集合时,保证合并后的结构要是树,同时被合并的两个集合也一定是树。我们只要将其中一个集合的根节点作为儿子,连到另一集合的根节点上,再将合并后的集合标记为合并后的根节点。
查询两个元素是否处在同一个集合时,只要查询两个元素的根节点是否相同即可。
将两个元素合并成一个集合等同将两个元素所在的集合合并成一个集合。
以及在一个集合中的两个元素不可再合并。

Father[MAXSIZE] //存放父节点:根节点被标记为集合,所有每次我们只查根节点,也就是说,只要知道每个节点的父亲节点就可以了
/*所有元素最初都是根节点,所以所有的Father[i] = i , 代码略*/
void Find_Root(int x)//查找x的根节点
{
	if(Father[x] != x)//根据我们的初始化,根节点满足 Father[i] = i 。
	{
		return Father[x] = Find_Root(Father[x]);//递归寻找根节点,并且执行路径压缩,将所有路径上的节点连向根节点,来减少之后查询的时间复杂度。
	}
	return x;//返回根节点
}
int Merge_Set(int x,int y)//合并两个元素 x,y
{
	int Rootx=Find_Root(x);
	int Rooty=Find_Root(y);//找到x , y的根节点。
	if(Rootx != Rooty)//如果x,y所在的根节点不同,才能合并
	{
		Father[Rootx]=Rooty;//将x的集合Rootx作为儿子节点,连到y的集合Rooty上,新的集合标记为Rooty
		return 0;
	}
	return -1;//不满足条件,合并失败
}
4.b.克鲁斯卡尔最小生成树

4 简单图的连通性问题一开始,我们大概介绍了最小生成树,这里我们介绍如何构造一颗最小生成树。
在给定的一个连通图里,选最小的 n - 1 条边,使得图连通,首先,边权最小的那条边一定在最小生成树中(证明略),所以我们一定会选择最小的那条边作为最小生成树的边,并通过并查集将相连的两个连通集合合并,所生成的新的集合中的所有点可互相到达。(因为当前只选了一条边,所以两个连通集合实际上是两个点)。
然后依次再判断其次小的边,如果该条边所连的两个点,处于不同的连通集合中,那么这条边也是最小生成树的边,将相连的两个连通集合合并。否则说明这条边是多余的,不选这条边。
重复这些操作,就可以构造出最小生成树了。
时间复杂度为 O ( E l o g E ) O(Elog_E) O(ElogE)只和边数有关。
例题:最小生成树
本题给出了N个点和M条边,需要判断原图是否连通,如果不连通,则输出orz ,否则输出最小生成树的边权之和。
其实我们不需要对是否连通做特殊的判断,只要按照克鲁斯卡尔的过程,构造最小生成树,如果最后所有点都在一个集合中,说明是连通的,否则说明图不连通,输出orz .。

/*
		ProblemOrigin : Luogu P3366.
		Author : アイラ
		LastModify : 2022/1/12 
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX_SIZE = 2e5 + 10;
int n,m;
struct Edge//	定义边的结构,由边直接连接的两个点u , v 和边权 w 组成。
{
    int u,v,w;
}E[MAX_SIZE];
int Father[MAX_SIZE];//	并查集相关操作 , 在上一节中已经提及。
int Find_Root(int x)
{
    if(Father[x] != x)
    {
        return Father[x] = Find_Root(Father[x]);
    }
    return x;
}
int Set_Merge(int x,int y)
{
    int Rootx = Find_Root(x);
    int Rooty = Find_Root(y);
    if(Rootx != Rooty)
    {
        Father[Rootx] = Rooty;
        return 1;
    }
    return 0;
}
void Set_Init(int n)// 					初始化Father数组
{
    for(int i = 1;i <= n;i++)
    {
        Father[i] = i;
    }
}
void Min_Span_Tree(Edge E[],int &Ans)//	最小生成树核心代码,其中 Ans 存最小生成树的边权和
{
    sort(E + 1,E + m + 1,[](Edge X,Edge Y){return X.w < Y.w;});//   按照克鲁斯卡尔的策略,边的判断是按照边
    //权,从小往大的,所以我们先按照边权大小关系排序,其中'[]'后面的是构造函数,用来表明排序依据
    for(int i = 1;i <= m;i++)
    {
        if(Set_Merge(E[i].v,E[i].u))// 		如果合并成功,才表示这条边是在最小生成树中的
        {
            Ans += E[i].w;// 在最小生成树的总边权中加上这条边的边权
        }
    }
    for(int i = 2;i <= n;i++)//   所有点最终是否在一个集合中,如果不满足所有点都在一个集合中,说明图不连通,把 Ans 标为 -1 
    {
        if(Find_Root(i) != Find_Root(i-1))
        {
            Ans = -1;
            break;
        }
    }
}
int main()
{
    ios::sync_with_stdio(0);
    cin >> n >> m;
    Set_Init(n);
    for(int i = 1;i <= m;i++){
        cin >> E[i].u >> E[i].v >> E[i].w; // 按照题目要求输入边的信息.
    }
    int Ans;
    Min_Span_Tree(E,Ans);
    if(Ans == -1)cout << "orz" << '\n'; // Ans 为 -1 表示图不连通,题目要求输出 orz
    else cout << Ans << '\n';
}
4.c.普利姆算法

接下来介绍普利姆算法求最小生成树。
我们将所有点分成两个集合 U,V。
假设一开始,所有点都在集合U中,我们要通过选择所给边中的 v-1 条边,将点并入集合V中。
首先我们选择任意一个点加入V中,然后更新集合U中的每一个点,到集合V中任意点的权值最小的边(也就是说,对于U中每一个点,找一条能到达集合V中点的边,并且这条边是所有满足条件的边中权值最小的)没有直接连接到V中任意点的边,则标为INF。
在集合U中的所有点中(且不是V中的点),找出到V的任意点中有直接连接的边,并且边权最小的那个点,加入V中,并且这条边加入最小生成树的边中,重复步骤,直到所有点都被加入V中,此时,最小生成树也构建完成。
这个算法的时间复杂度之和点有关,所以在稠密图的最小生成树求解,往往会使用这个算法,不使用堆优化时,时间复杂度为 O ( N 2 ) O(N^2) O(N2)
同样我们使用普利姆算法解决**4.b.**中的例题。

/*
		ProblemOrigin : Luogu P3366.
		Author : アイラ
		LastModify : 2022/1/13
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX_SIZE = 5001;
const int INF = 10002;
int G[MAX_SIZE][MAX_SIZE];// 使用邻接表存图(本题中点数<5001)
int min_edge[MAX_SIZE];// 存 U 中每一个节点到 V中节点的最短直接连接边
int n,m;
void Prim(int &Ans)//  普利姆核心算法
{
    Ans = 0;
    min_edge[1] = -1;
    int NowV;
    NowV = 2;
    for(int i = 2;i <= n;++i)// 默认先把顶点1加入集合V,并更新min_edge.
    {
        min_edge[i] = min(min_edge[i],G[1][i]);
        if(min_edge[NowV] > min_edge[i])NowV = i;
    }
    Ans += min_edge[NowV];
    for(int i = 1;i < n - 1;i++)// 把U中的点加入V的过程
    {
        min_edge[NowV] = -1; //当前点已经加入V中,所以把min_edge标为-1
        for(int j = 1;j <= n;j++)// 更新所有U中的点到V的最短距离
        {
            if(min_edge[j] > G[NowV][j] && min_edge[j] != -1)min_edge[j] = G[NowV][j];
        }
        int mindis = 0x7fffffff;
        for(int j = 1;j <= n;j++) //找出一个离 V最近的点
        {
            if(min_edge[j] < mindis && min_edge[j] != -1 && min_edge[j] < INF)
            {
                mindis = min_edge[j];
                NowV = j;
            }
        }
        if(min_edge[NowV] != -1)Ans += min_edge[NowV];//	将最小的这条边加入最小生成树
        else Ans = -1;
    }
}
int main() 
{
    ios::sync_with_stdio(0);
    cin >> n >> m;
    memset(G,0x3f,sizeof G);
    memset(min_edge,0x3f,sizeof min_edge);
    for(int i = 1;i <= m;i++)
    {
        int u,v,w;
        cin >> u >> v >> w;
        if(u == v)continue;
        G[u][v] = min(G[u][v],w);//	只保留最短的边
        G[v][u] = min(G[v][u],w);
    }
    int Ans;
    Prim(Ans);
    if(Ans == -1)cout << "orz\n";
    else cout << Ans << '\n';
}
5 最短路问题

下接 浙江科技学院ACM新生培训图论讲义2 图论讲义P2

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值