前言:我们是冠军🎉🎉🎉
EDG定理:一旦EDG打破了8强的魔咒,那么EDG将勇往直前,直取冠军。
决赛第一场的时候博主还在回宿舍的路上,没来得及看,EDG碾压DK直接拿下。
第二场第三场博主是全程观看。看完第三场后博主的心情从一开始的激动变成了心肌梗塞。真不敢再看下去了,当年RNG打G2的时候博主就是全程观看的,结果落了个8强。要不是当时正在上晚自习,我恨不得把桌子给它掀了。当时我跟舍友说我不看了,EDG如果要赢了你们就喊我,我不管,这我肯定不敢看了。
结果第四场打了回来(李佐伊!)。
这我一看,我进场的时候到了,于是我又从床上坐了起来打算看最后一场。舍友当时及时地制止了我:"刘某你先别看,最后一场不能搞啊。"我一想也是,我注重的又不是比赛内容,劳资只是想看EDG夺冠罢了,于是博主又躺下了。第五场开局比较顺利。
14:00的时候Khan致命空枪送了,舍友大喊了起来,我当时就直接光速进入直播间,准备迎接EDG的胜利。没想到我进去不到30秒,DK三个人集合把EDG下路逼走,把下一塔给拆了,我默默地退出了直播间;
27:10的时候原神哥迷路被抓到机会,舍友再次大叫了起来,博主再次以单身19年的手速进入直播间,准备迎接EDG的胜利。结果大龙被偷了,我又默默退出了直播间;
38:04的时候李佐伊直接一个飞星砸死满血炸弹人,舍友沸腾了,我也沸腾了,于是第三次进入直播间。 结果李佐伊的20层杀人书掉了…
最后高地虐泉的时候舍友大喊着"刘某,快,进场了!",我tm直接从床上奋起,看着可怜的汉子哥被虐泉,大喊出『EDGNB!我们是冠军!』。
博主虽然早就脱坑了(并不是因为打游戏太菜了),但博主的心一直留有一个地方,用来存放这份回忆,这份对游戏的感情。你说它幼稚吗?博主并不这么觉得。
只希望在将来的某一天,博主无法支撑起对所爱之事的热情之时,博主能够重拾起当初那颗中二又纯粹的热血之心。又有哪个网瘾少年没有过一个电竞梦呢?
既然EDG都夺冠了,那博主就来兑现承诺。
废话少说,接下来让我们进入『最小生成树算法——克鲁斯卡尔算法』的学习之中吧。
Ⅵ.克鲁斯卡尔(Prim)算法:战国时期的“合纵”与“连横”
合纵连横(hé zòng lián héng)简称纵横,是战国时期纵横家所宣扬并推行的外交和军事政策。
公孙衍和苏秦曾经联合“天下之士合纵相聚于赵而欲攻秦”(《战国策·秦策》三),公孙衍首先发起,由苏秦游说六国推动六国最终完成联合抗秦。秦在西方,六国在东方,因此六国土地南北相连,故称“合纵”;与合纵相对。后秦国自西向东与各诸侯结交,自西向东为横向,故称“连横”。
现在你又穿越到了战国时期,并附身在了秦国丞相——张仪的身上。
此时由于苏秦游说六国定立合纵盟约,联合对抗秦国,秦国陷入了十分危险的情况。作为秦国的丞相,你需要发挥自己的智慧帮助秦国化解这次的危机。
上图是一张战略图。虽然看着和上一次看到的图是一样的,但并不是同一张战略图(并不是博主比较懒就不画新图直接复制旧图用)。
以下是对于战略图的解释:
1.两国之间若存在边,则代表你可以对两国进行游说,使得两国签订盟约。
2.边上的权值则代表你需要游说的时间(单位:天)。
3.若A、B两国之间不存在边,我们就认为如果我们想让A、B两国签订盟约,所需要花费的时间是不计其数的。
4.若A国与B国签订盟约,B国与C国之间又存在盟约,则我们认为A国与C国之间也存在同盟关系。
现在秦国危在旦夕,你需要想办法在最短的时间内让其余6国与秦国达成盟约,破解苏秦的合纵之势,拯救秦国。
和废柴日记6:迟到的『构造最小生成树算法』③中的例题完全相同,博主只是把题意稍微修改了一下,以便于大家理解我们接下来要讲的 『克鲁斯卡尔算法』 。
克鲁斯卡尔算法的思想
假设连通网 G = ( V , E ) G=(V,E) G=(V,E),令最小生成树的初始状态为只有 n n n个顶点而无边的非连通图 T = ( V , T=(V, T=(V,{} ) , ), ),概述图中每个顶点自成一个连通分量。
- 在 E E E中选择代价最小的边,若该边依附的顶点分别在 T T T中不同的连通分量上,则将此边加入到 T T T中;
- 否则,舍去此边而选择下一条代价最小的边。
依此类推,直至 T T T中所有顶点构成一个连通分量为止。
博主的理解(结合题目)
对比『普里姆算法』 与『克鲁斯卡尔算法』 ,我们会发现:
『普里姆算法』更加注重于选点,即每次行动时选择合适的点;而『克鲁斯卡尔算法』 在每一次行动中更加注重的是找一条代价最小的边。
这就使得使用『邻接矩阵』与『邻接表』实现『克鲁斯卡尔算法』会显得有些麻烦。
所以我们这里采用一个新的结构存储战略图,使用的是之前在废柴日记5:迟到的『构造最小生成树算法』②中所提到的『边集数组』。
边集数组结构设计
//边的固有属性:连接的两个点与边上的权值
typedef struct
{
int begin; //顶点其一
int end; //顶点其二
int weight; //权值
}Edge;
在求解的过程中,我们每次都需要选择 两个符合条件的需要游说时间最短的国家 进行游说。
- 条件:两个国家并不是同盟国。
那么这就引出了一个新问题:如何快速判定两个国家是否符合条件呢?
此时我们就需要借助一个新数组
v
s
e
t
vset
vset,数组
v
s
e
t
vset
vset用来记录每个国家的同盟国是谁。
一开始我们并没有开始游说,我们认为所有的国家的同盟国都是自己。
还没有结束,再考虑一个情况:韩国先与秦国结盟,然后再与魏国结盟。根据规则4有:秦国与魏国为盟友。那么我们该如何规定
v
s
e
t
vset
vset值的含义,才能方便我们判断秦国与魏国两个国家是否同盟呢?
我们规定:
当两个国家签订盟约时,以"秦 齐 楚 燕 韩 赵 魏"的顺序,顺序优先的国家作为同盟国的东道主。
所有国家的同盟国名称统一填写该国家所处的同盟国的东道主的国家名。
那么根据我们所指定的规则,若有韩、秦、魏三国结盟,则有vset[韩]==vset[秦]==vset[魏]==秦
。
准备就绪,开始解题。
首先我们先写出题目中的边集数组:
在『克鲁斯卡尔算法』中,我们每次都需要寻找权值最小的边进行判断。
所以我们先给边集数组根据权值进行排序,以减小后续过程中的时间复杂度。
关于排序有些人可能有疑问:
秦国与韩国这条边的权值是2,赵国与魏国这条边的权值也是2,为何前者排在前面?调换一下顺序可以吗?
我们每次选择的是权值最小的那条边,并不对边所连接的两个顶点做要求。
所以答案是完全可以,看你心情。
接下来我们就根据排好序的边集数组开始进行选择每一次要游说的国家。
第一次我们按照边集数组的顺序,选择了韩国与赵国
此时我们需要根据
v
s
e
t
vset
vset数组判断两国是否为同盟国。
显然两国并不是同盟国,所以此次行动就是让韩国与赵国定下盟约。
根据我们制定好的规则更改数组 v s e t vset vset:
第二次我们按照边集数组的顺序,选择了秦国与韩国
我们仍需要根据
v
s
e
t
vset
vset数组判断两国是否为同盟国.
显然两国并不是同盟国,所以此次行动就是让韩国与秦国定下盟约。
此时我们会注意到,由于规则4的存在,韩国与秦国结盟之后,作为韩国盟友的赵国也会变成秦国的盟友。
注意上述内容,根据我们制定好的规则更改数组 v s e t vset vset:
第三次我们按照边集数组的顺序,选择了赵国与魏国
根据
v
s
e
t
vset
vset数组判断两国是否为同盟国,结果是两国并不是同盟国。
所以此次行动就是让赵国与魏国定下盟约。
此时我们会注意到,赵国的同盟国是秦国。那么根据规则4与我们制定的规则,魏国的同盟国此时也应该改为优先级更高的秦国。
根据我们制定好的规则更改数组 v s e t vset vset:
第四次我们按照边集数组的顺序,选择了燕国与齐国
根据
v
s
e
t
vset
vset数组判断两国是否为同盟国,结果是两国并不是同盟国。
所以此次行动就是让燕国与齐国定下盟约。
根据我们制定好的规则更改数组 v s e t vset vset:
第五次我们按照边集数组的顺序,选择了魏国与楚国
根据
v
s
e
t
vset
vset数组判断两国是否为同盟国,结果是两国并不是同盟国。
所以此次行动就是让魏国与楚国定下盟约。
根据我们制定好的规则更改数组 v s e t vset vset:
第六次我们按照边集数组的顺序,选择了赵国与燕国
根据
v
s
e
t
vset
vset数组判断两国是否为同盟国,结果是两国并不是同盟国。
所以此次行动就是让赵国与燕国定下盟约。
由于齐国与燕国是同盟国,导致齐国也需要更改自己的
v
s
e
t
vset
vset值。
根据我们制定好的规则更改数组 v s e t vset vset:
此时我们发现所有国家的同盟国都是秦国,也就代表我们成功完成了任务,让其余6国与秦国达成盟约,化解了秦国的险境。
我们把没有选中的边全部删除,原图就会变为:
这个时候可能有人就会说了:"啊!懒狗你也太懒了吧!这不是第三篇最后得出来的图吗?你个懒狗,你给我去spa!"
冤枉啊,真不是我懒,它真的就是一样的,你自己试试啊┭┮﹏┭┮
有点碰巧了,感觉没法表示出一张无向图的最小生成树不止一个的观点。
不过懒狗就不再举出例子说明了,大家记住就好(✿◡‿◡)
不过按道理来说,『普里姆算法』 与『克鲁斯卡尔算法』得出的最小生成树大部分情况下都是不同的(个人感觉)。
中间过程中没有出现过一个问题,博主在这里提出:
如果按照边集数组的顺序,在第五次选择时需要游说的国家是韩国与魏国,该怎么办?
在第五次选择之前,盟友情况是这样的:
此时我们发现,韩国与魏国已经是盟友了。所以我们此时要做的就是跳过这条边,再去找一条符合条件的边进行选择。
代码部分
和之前一样,为了方便输入,我们用不同的编号代表不同的国家。
接下来,就是定义边集数组与 v s e t vset vset数组。
const int MAXVEX=100;
typedef struct
{
int begin;
int end;
int weight;
} Edge;
Edge edge[MAXVEX];
int vset[MAXVEX];
准备工作做完,开始正式的流程。
一.输入战略图
//我们并不需要存储每个顶点各自的信息
//Kruskal算法的关键是边
void CreateUMGraph()
{
int i,j,x,y,w;
printf("请输入无向图G的顶点个数:\n");
scanf("%d",&numNodes); //numNodes:顶点个数
printf("请输入无向图G的边数:\n");
scanf("%d",&numEdges); //numEdges:边的个数
for(i=1; i<=numEdges; i++)
{
printf("请输入边(i,j)上的国家i的编号,国家j的编号与权值w:\n");
scanf("%d%d%d",&x,&y,&w);
edge[i].begin=x;
edge[i].end=y;
edge[i].weight=w;
}
}
二.编写Kruskal()函数,实现『克鲁斯卡尔算法』
1.根据理论部分,我们首先要对边集数组进行排序。
对结构体进行排序的话,我们需要事先设计一个排序函数。
bool cmp(Edge a,Edge b)
{
return a.weight<b.weight;
}
sort(edge+1,edge+numEdges+1,cmp);
2.根据理论部分,接下来需要初始化vset数组。
一开始我们并没有开始游说,我们认为所有的国家的同盟国都是自己。
for(int i=1;i<=numNodes;i++)
vset[i]=i;
3.执行一个『循环』。
我们来细看每次『循环』的流程:
- 每次在边集数组中选择第一个没有使用过的边(由于排序,此时边集数组是有序的);
- 判断被选中的两个国家是否为同盟国;
- 若不是,则选中该边,并更新vset数组;
若是,则跳过该边,执行指令1;
这个循环要持续 6 ( 7 − 1 ) 6(7-1) 6(7−1)次,也就是 n − 1 n-1 n−1次。
int k=1;
int pos=1;
int day=0;
while(k<numNodes)
{
int u1=edge[pos].begin;
int u2=edge[pos].end;
int f1=vset[u1];
int f2=vset[u2];
if(f1!=f2)
{
k++;
day+=edge[pos].weight;
cout<<"此次结盟双方为: "<<country[u1]<<" 与 "<<country[u2]<<" ,所需时间为"<<edge[pos].weight<<"天."<<endl;
//按照我们制定的规则更新vset数组
for(int i=1; i<=numNodes; i++)
if(vset[i]==max(f1,f2))
vset[i]=min(f1,f2);
}
pos++;
}
cout<<"游说总时间为:"<<day<<"天."<<endl;
三.接下来写个主函数,把这些补一补就ok了
#include<bits/stdc++.h>
using namespace std;
const int MAXVEX=100;
typedef struct
{
int begin;
int end;
int weight;
} Edge;
bool cmp(Edge a,Edge b)
{
return a.weight<b.weight;
}
Edge edge[MAXVEX];
int vset[MAXVEX];
int numEdges,numNodes;
char country[10][35]= {"","秦国","齐国","楚国","燕国","韩国","赵国","魏国"};
void CreateUMGraph()
{
int i,j,x,y,w;
printf("请输入无向图G的顶点个数:\n");
scanf("%d",&numNodes);
printf("请输入无向图G的边数:\n");
scanf("%d",&numEdges);
for(i=1; i<=numEdges; i++)
{
printf("请输入边(i,j)上的国家i的编号,国家j的编号与权值w:\n");
scanf("%d%d%d",&x,&y,&w);
edge[i].begin=x;
edge[i].end=y;
edge[i].weight=w;
}
}
void Kruskal()
{
sort(edge+1,edge+numEdges+1,cmp);
for(int i=1;i<=numNodes;i++)
vset[i]=i;
int k=1;
int pos=1;
int day=0;
while(k<numNodes){
int u1=edge[pos].begin;
int u2=edge[pos].end;
int f1=vset[u1];
int f2=vset[u2];
if(f1!=f2){
k++;
day+=edge[pos].weight;
cout<<"此次结盟双方为: "<<country[u1]<<" 与 "<<country[u2]<<" ,所需时间为"<<edge[pos].weight<<"天."<<endl;
for(int i=1;i<=numNodes;i++)
if(vset[i]==max(f1,f2))
vset[i]=min(f1,f2);
}
pos++;
}
cout<<"游说总时间为:"<<day<<"天."<<endl;
}
int main()
{
CreateUMGraph();
Kruskal();
}
代码效果图
后话
『构造最小生成树算法』 也算是终于完结了。从9月14日写到11月7日,这也能看出博主真的是一个死懒狗啊。
不过结局总是好的,每件事都有了一个华丽的结束。
我们下个系列见 ~