0基础学图论!——图论精讲/详细/新手理解概念必看!

并不会有更好的阅读体验

特别特别感谢:

l m p p lmpp lmpp大佬牺牲自己宝贵时间,为我没有脾气的耐心讲解。

B e y o n d H e a v e n BeyondHeaven BeyondHeaven大佬,无偿帮我康博客,甚至和我这个陌生的蒟蒻分享自己的故事,带给了我知识和温暖。真的、真的非常谢谢宁! q w q qwq qwq

F l y Fly Fly_ F u n n y Funny Funny大佬,理会了一个无关紧要的人的丧气话,并且带给了她把这篇博文更完的动力

真的很感谢我的教练 d i d did did,他的宽容友爱和超一流的教学水准不仅让我迈上了 O I OI OI的正轨,更让我感受到了朋友的温暖。

还有很多很多好朋友帮助了我(详见友链),真的非常感谢你们!

没有你们就没有这篇博客,我也会比现在蒻上 i n f inf inf倍。


提示:

本博客还并没有做到完全是自己总结的概念,但是我已经正在努力填坑了 Q A Q QAQ QAQ

本文将以尽量朴(全)实(口)无(水)华(话)的语言和自己的一些奇奇怪怪的想法,为您呈现一个我眼中的图论世界。

在我自己掌握特别不好的题目/知识点旁会加以 ! \color{Red}\colorbox{Yellow}{!} 的标记。

所有题号以 B S O J BSOJ BSOJ为准。所有题目已经附上了题干和样例数据。题目链接我也正在尽量引用公开的大 O J OJ OJ上的题目,方便大家做题学习。注意!有些题的代码可能和附上的链接题目不能吻合,但题目的思路一定都是一致的,只是细节上的不同,具体代码对应的题干还是看我附在代码前的题干!

有什么错误纰漏的直接 Q Q QQ QQ+洛谷私信+讨论区留言,我真的超级需要您的反馈的 q w q qwq qwq

希望能您能从这份清单中找到您的一些知识漏洞并把它们补起来!


更新信息

2020.3.29 2020.3.29 2020.3.29 1.0 1.0 1.0版本,开始新的篇章。知识点记录至拓扑排序模板题。

2020.3.30 2020.3.30 2020.3.30 && 2020.3.31 2020.3.31 2020.3.31 2.0 2.0 2.0版本,爆肝完所有知识点。

2020.5.6 2020.5.6 2020.5.6 2.5.0 2.5.0 2.5.0版本,在自己的反复体会和阅读学习后进行了对文本效果知识内容方面修复部分问题。拓扑排序部分已删去死板概念,换为自己理解。

2020.5.7 2020.5.7 2020.5.7 2.5.1 2.5.1 2.5.1版本,书面上已经总结完了 K r u s k a l Kruskal Kruskal算法,博客上还没整完。

2020.5.8 2020.5.8 2020.5.8 2.5.2 2.5.2 2.5.2版本,书面上已经总结完了 P r i m Prim Prim算法,博客上还没整完。

2020.5.10 2020.5.10 2020.5.10 2.5.3 2.5.3 2.5.3版本,书面上已经总结完了 F l o y d Floyd Floyd算法,博客上还没整完。(发现一直以来 F l o y d Floyd Floyd大神的名字都写错了,真的对不起了呜呜呜呜)

2020.5.11 2020.5.11 2020.5.11 2.5.4 2.5.4 2.5.4版本,书面上已总结了一丢丢 S P F A SPFA SPFA,未完待续。

2020.5.12 2020.5.12 2020.5.12 2.5.4.5 2.5.4.5 2.5.4.5版本,乌龟速度更进 S P F A SPFA SPFA,废话一堆堆,未(我)完(想)待(烂)续(尾)。

2020.6.8 2020.6.8 2020.6.8 2.5.5 2.5.5 2.5.5版本,把之前的数字和字母做了优化, T a r j a n Tarjan Tarjan开始更新。不要问我为什么拖了这么久才又开始更。别问,问就是半期考试。不要问我半期考得怎么样。别问,问就是全班垫底(bushi)

2020.6.9 2020.6.9 2020.6.9 2.5.6 2.5.6 2.5.6版本,书面上已经总结完了 T a r j a n Tarjan Tarjan的基本概念,开始更新割点之类的概念。总结上 T a r j a n Tarjan Tarjan还没更完,发给了 d i d did did康康,我怀疑他不会回我。

2020.7.11 2020.7.11 2020.7.11 2.6.0 2.6.0 2.6.0版本,直接到 2.6 2.6 2.6版本是因为之前陆陆续续有在做一些修改和删减,大概可以算是 2.6.0 2.6.0 2.6.0版本了叭?……(小声)

2020.7.12 2020.7.12 2020.7.12 2.6.0 2.6.0 2.6.0版本,准备放弃了,大概是和 O I OI OI没有缘分了。在 l u o g u luogu luogu上发表了一篇丧丧的文章吧,应该没有人会理我的,希望负能量没有影响到大家 o r z orz orz

2020.7.14 2020.7.14 2020.7.14 2.6.0.1 2.6.0.1 2.6.0.1版本,在 l u o g u luogu luogu上丧气的文章, t a o x u a n y u taoxuanyu taoxuanyu大佬鼓励了我,挺好,所以还是决定在这篇文章不要烂尾,至少题目的链接啊,题干啥的还是要放一放,知识点看了一下,应该也是没有问题的。想了想还是给丧气话加上了渲染(((算是在 O I OI OI留下自己一个完整的脚印吧。


备忘录- T O TO TO D O DO DO L i s t List List

  • 把原题目链接换成大 O J OJ OJ的题目链接

  • 附上题目的题干和样例数据

  • (缓慢任务)删掉一些毫无营养的口水话,但是不能影响详细、搞笑(?)的风格


拓扑排序:

什么是拓扑排序?

拓扑排序,通过名字里的“排序”,我们自然知道了它的功能——排序。
那么,它怎样排序的?又是为谁排序的?

拓扑排序的简单规则

“排序”,一个多么简单的概念。在你的脑海里,“排序”是否具有以下特征呢?

  • 一堆数据
  • 一些规则

正如大多数排序一样,拓扑排序也具有以上的简单特征。

从“一堆数据”开始思考

不妨想想,作为一个工作的排序,怎样才能构造出“一堆”数据呢?
答案是明显的,我们应该将图中的每一个节点取出,形成我们排序所需的一堆数据
可是我们怎么选择取出的节点呢?真就毫无目标 r a n d ( ) rand() rand()函数随机整一个?

如果想到了这点,我们就可以开始考虑拓扑排序的规则

怎样用“一些规则”排序?

所谓规则,就是一个算法进行操作的方法。
对于拓扑排序来说,它的操作方法便是如下 3 3 3步:

  1. 寻找一个入度为 0 0 0的点(为什么?这就是“规则”,定死了的,不然你咋不问我为啥1+1=2人为规定的,不然怎么开始排序???)
  2. 取出这个点,随之被删除的还有和这个点所有直接相连的边
  3. 在剩下的节点中重复第 1 1 1 2 2 2步,直至所有节点被取出
我们对谁使用拓扑排序?/ 谁身上才能使用拓扑排序?

我们对 D A G DAG DAG——即:有向无环图使用拓扑排序。

为什么我们只能对 D A G DAG DAG进行拓扑排序?

通过刚刚的过程分析,我们可以轻易地发现一个问题:如果当前的节点没有全部被取出,但却已经发现不了入度为 0 0 0的节点,那么我们就不能愉快的继续拓扑排序了

不妨思考一下为什么会出现这种 b u g bug bug
一切的证据都指向有向有环图。(无向就不说了,咋整都是个环)

为啥?我们先来看看下图——一个有向有环图,来模拟一下拓扑排序的过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jky5eqRY-1594772179294)(https://mrcontinue.github.io/post-images/1588862342176.png)]
第一步,我们找出了入度为 0 0 0的节点①,并且将它和它相关联的边取出。
于是乎,现在的图是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nefSZH5I-1594772179295)(https://mrcontinue.github.io/post-images/1588862416195.png)]
这时我们可以发现一个有趣的事情。由于②、③。④三个点形成了一个环,所以现在图中没有入度为 0 0 0的点
img
所以此时就会出现无法继续进行拓扑排序的问题。而这一切正是印证了拓扑排序只能在 D A G DAG DAG图里进行操作的事实。

关于拓扑排序的应用

拓扑排序一般很少有单独针对该知识点的题,但是在关键路径和平常的其他例题的辅助操作中却发挥着重要作用。所以掌握好拓扑排序是很重要的事情。

1462 1462 1462 拓扑排序(这题实在太水了,大 O J OJ OJ上根本找不到,只好把题干和样例附在这里,提交就随缘吧((()

D e s c r i p t i o n Description Description
对一个有向图(Directed Acyclic Graph简称DAG) G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若 ∈E(G),则u在线性序列中出现在v之前。
若图中存在有向环,则不可能使顶点满足拓扑次序。

I n p u t Input Input
第1行:2个空格分开的整数n和m,分别表示图的顶点数和边数。
第2..m+1行:每行2个空格分开的整数i,j,i表示一条边的起点,j表示终点。
O u t p u t Output Output
拓扑序,顶点从1开始编号,如果有多个拓扑序,则顶点编号小的优先输出。
有环输出:no solution
S a m p l e Sample Sample I n p u t Input Input

【样例输入1】

4 4
1 3
1 4
2 3
2 4

【样例输入2】

4 4
1 2
2 3
3 4
4 1
S a m p l e Sample Sample O u t p u t Output Output

【样例输出1】

1 2 3 4

【样例输出2】

no solution
H i n t Hint Hint

【数据范围】

1≤n≤200
1≤m≤20000

本代码按字典序输出的部分:

int j=1; //从第一个点开始查找
while(j<=n&&bein[j])
	j++; //统计入度为零的节点
//由于-1的bool值也视作真,所以可以标记为-1
if(j>n) return 0; //如果统计的节点超出了范围n,说明这个图有环
sum[++top]=j; //拓扑序列答案数组统计新答案
//本代码由邻接矩阵实现,按字典序输出,复杂度O(n^2)

#include <iostream>
#include <cstdio>

#define maxn 205
using namespace std;

int bein[maxn]; //bein[i]表示节点i的入度
int a[maxn][maxn]; //邻接矩阵存图
int sum[maxn],top; //拓扑序列答案数组
int n,m;

int TS()
{
   
	for(int i=1;i<=n;i++)
	{
   
		int j=1; //从第一个点开始查找
		while(j<=n&&bein[j])
			j++; //统计入度为零的节点
        //由于-1的bool值也视作真,所以可以标记为-1
		if(j>n) return 0; //如果统计的节点超出了范围n,说明这个图有环
		sum[++top]=j; //拓扑序列答案数组统计新答案,储存已取出的点
		bein[j]=-1; //标记此点已经遍历
		for(int k=1;k<=n;k++)
			if(a[j][k]) //如果j和k之间有边相连
				bein[k]--; //和j相关联的节点删除与j相连的边,即入度--
	}
	return 1; //如果遍历过程中没有返回过假值,则有解,返回真值
}

int main()
{
   
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
   
		int x,y;
		cin>>x>>y;
		a[x][y]=1; //邻接矩阵储存单向边
		bein[y]++; //输入是从x到y的连线,所以是y的入度增加
	}
	if(TS()) //如果有解
		for(int i=1;i<=n;i++)
			cout<<sum[i]<<" "; //则输出节点的答案数组
	else
		cout<<"no solution"<<endl; //无解输出"no solution"
	return 0;
}

最小生成树(英文缩写 M S T MST MST我忘了全称叫啥了反正知道了也没啥用

  • 最小生成树:在一张带权的无向连通图中,各边权和为最小的一颗生成树即为最小生成树。
    简单讲:找出连接所有点的最低成本路线
    举个栗子,下面这个图中的最小生成树就是用红线标出的:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E61AGA65-1594772179300)(https://mrcontinue.github.io/post-images/1588863097566.png)]

  • 最小边原则:图中权值最小的边(如果唯一的话)一定在MST上。
    为啥?试想,既然 M S T MST MST的路径一定会连接至每个节点,那么我们一定会尽量选择最小的边以到达目标节点。(不然合着有最短的路不走走绕路??)

K r u s k a l Kruskal Kruskal算法

K r u d k a l Krudkal Krudkal算法的基本知识

K r u s k a l Kruskal Kruskal算法是一种贪心算法。为什么是贪心?知道了过程你就知道了。

K r u s k a l Kruskal Kruskal的操作方法:
  1. 按图中的边按权值从小到大快排(为啥?详见楼上【最小边原则】,您品,您细品)。
  2. 按照权值从小到大依次选边,若当前选取的边加入后使生成树形成环,则舍弃当前边(原因后面会讲的);否则标记当前边已遍历并计数。
  3. 重复2的操作,直到生成树中包含 ( n − 1 ) (n-1) n1条边为止;否则当遍历完所有的边后,都不能选取 ( n − 1 ) (n-1) n1条边,表示最小生成树不存在。

从上面 K r u s k a l Kruskal Kruskal直接快排无脑选边,当前不符合条件就直接舍弃,完全不考虑未来的步骤中,可以隐(强)约(烈)地感受到“鼠目寸光”的贪心内味。(有内味了. j p g jpg jpg

接下来咱简要的抠一抠算法里的细节。

怎么判断当前边加入后, M S T MST MST会不会形成环?

我们先来欣赏一个美丽的环,请您尽量把它想象成一棵变异了的二叉树——因为它有环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OTXGZ2Fw-1594772179301)(https://mrcontinue.github.io/post-images/1588864164761.png)]
可以看见,这棵奇怪的树分外耀眼的 p o i n t point point节点4同时拥有了两个父亲——即节点 2 2 2和节点 3 3 3
如果它与任意一个父亲断掉连接,那么它还是一棵健康茁壮的二叉树:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dMpdmpXV-1594772179302)(https://mrcontinue.github.io/post-images/1588948569959.png)]

所以我们可以推出,判断一棵树上有没有环,可以直接检查一个儿子是否拥有多个父亲。放在无向图里,我们可以变相理解为:多个节点是否同时联向同一个节点,即,有没有多个节点属于同一个集合

欸?集合?判断?
有没有很熟悉的感觉?(别告诉我你没有,你就是有(大雾))

没错,你想起了并查集!就是那个可以储存所属集合、可以直接合并两个集合的神奇数据结构。

H o w e v e r How ever However,我们就是用并查集来储存所有的节点,也正好利用它的特性——可以非常简单的判断所属集合,这点,来判断新加入的节点是否会让 M S T MST MST产生环

并查集的基本操作:

1. 1. 1. 找根节点:

int GetFather(int x)
{
   
	if(prt[x]==x) return x;
    return prt[x]=GetFather(prt[x]); //路径压缩
}

2. 2. 2. 合并操作(一般可以直接写在主函数里):

void Add(int x,int y)
{
   
    int f1=GetFather(x);
    int f2=GetFather(y);
    if(f1!=f2) prt[f1]=f2;
}

1449 1449 1449 最小生成树

D e s c r i p t i o n Description Description
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz。
I n p u t Input Input
第一行包含两个整数N,M,表示该图共有N个结点和M条无向边。

接下来M行每行包含三个整数Xi,Yi,Zi,表示有一条长度为Zi的无向边连接结点 Xi,Yi。
O u t p u t Output Output
如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz。
S a m p l e I n p u t Sample Input SampleInput
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
S a m p l e O u t p u t Sample Output SampleOutput
7
H i n t Hint Hint
对于20%的数据,N≤5,M≤20。
对于40%的数据,N≤50,M≤2500。
对于70%的数据,N≤500,M≤2×10^5。
对于100%的数据,1≤N≤5000,1≤M≤2×10^5。

样例解释:

所以最小生成树的总边权为 2 + 2 + 3 = 7 2+2+3=7 2+2+3=7

#include <iostream>
#include <cstdio>
#include <algorithm>

#define maxn 40005
using namespace std;

int n,m;
int prt[maxn]; //prt[i]表示节点i的根节点
int ans=0; //最小生成树的总权值和
bool flag; //标记是否有解

struct Edge
{
   
	int x,y,v;
}a[maxn]; 
//x[i]代表a[i]这条边的起点
//y[i]代表a[i]这条边的终点
//v[i]代表a[i]这条边的权值

bool cmp(Edge x,Edge y) //结构体数组排序函数
{
   
	return x.v<y.v;
}

int GetFather(int x) //并查集查找根节点,这里使用了状态压缩
{
   
	if(prt[x]==x) return x;
	prt[x]=GetFather(prt[x]); //压缩
	return prt[x];
}

void K()
{
   
	int f1,f2;
	int k=0;
	for(int i=1;i<=m;i++)
	{
   
		f1=GetFather(a[i].x);
		f2=GetFather(a[i].y); //分别找出x,y所属的集合,即根节点
		if(f1!=f2) //如果不属于同一集合,即不会形成环,则合并两集合
		{
   
			ans+=a[i].v; //累加最小生成树的权和总值值
			prt[f1]=f2; //两集合合并
			k++; //已遍历的节点累加
			if(k==n-1) //如果遍历到了所有点,则停止遍历
            //至于为什么是n-1,因为k的初始值是1啊qwq
				break;
		}
	}
	if(k<n-1) //如果当遍历完所有的边后 都不能选取(n-1)条边 则表示最小生成树不存在
	{
   
		cout<<"orz"<<endl;
		flag=1; //标记没有最小生成树
		return ;
	}
}

int main()
{
   
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>a[i].x>>a[i].y>>a[i].v;
	for(int i=1;i<=n;i++)
		prt[i]=i; //并查集初始化
	sort(a+1,a+m+1,cmp); //按边权从小到大排好,以便贪心操作
	K();
	if(!flag) //如果有最小生成树,则输出最小权值
		cout<<ans<<endl; 
	return 0;
}

P r i m Prim Prim算法

为啥有了 K r u s k a l Kruskal Kruskal算法还要学 P r i m Prim Prim呢?
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值