1介绍
本节将记录两个问题,(1)并查集;(2)克鲁斯卡尔算法;。
这是贪心算法最后一节,可能不是所有的问题都与贪心算法有关,但是都是我认为有趣且比较重要的东西,有必要统一学习记录一下。可能我举例不太文雅,但绝对没有歧视和嘲讽任何群体的意思,只是为了让人印象深刻一些。
2并查集
2.1原理
2.1.1基础
我们在使用QQ或者其他社交软件的时候,会发现有一个好友推送的功能,也就是向你推荐你朋友的朋友。如果我们认定你朋友的朋友就是你的朋友,那么你的朋友圈就足够大了。倘若现在出现一个美女——如花,你非常想认识她/他,首先你不认识她,其次你朋友圈里也没有人认识如花和如花朋友圈里的人。
当然最直接的方法是直接去搭讪如花,这是最简单的方法。但是你想做的更高效一些,不能只考虑自己的幸福,因为你所处的是闷骚型屌丝群体,都是单身狗,而如花所处的朋友圈都是女神(原谅老郭),所以,有没有什么办法让这两个朋友圈都互相认识呢?
很简单,让你朋友圈的老大谦哥去跟对方朋友圈的老大老郭谈一谈,谈妥以后就可以联谊了,至于怎么联谊,从此以后,是让谦哥认老郭做朋友圈老大,还是让老郭认谦哥做朋友圈老大,这个也有讲究,稍后讨论。
可以总结一下这个过程:(1)你要认识如花;(2)你去找你的老大谦哥;(3)如花去找她的老大老郭;(4)发现你们两个的老大不是同一个人(谦哥和老郭),于是联合。
程序的一般结构,三个函数,一个数组:
(1) 一个数组:用来存贮当前数据的前驱结点,如:Arry[i]=j表示i的前驱是j,当Arry[i]=i时表示i是根结点。
(2) Find函数:寻找老大;
int Find(int num){
while(Arry[num]!=num)
num=Arry[num];
return num;
}
(3) Connect函数:判断两人是否认识,是返回true,否返回false
bool Connect(int a,int b){
if(Arry[a]==Arry[b])
return true ;
return false;
}
(4) Union函数:联合
void Union(int a ,int b){
int aRoot=Find(a);
int bRoot=Find(b);
if(aRoot!=bRoot)
Arry[aRoot]=bRoot;
}
2.1.2路径压缩
现在来优化一下,假如你的朋友圈是下面这种结构的,也就是说,你可能太宅了,朋友圈中很多牛逼的人物你接触不到。
如果是这样的话,你想促成这次联谊,就要一层一层往上去找老大,一个个问“咱们这个圈子谁比较有号召力啊?”,这样劳心费力,操碎了心,还不如直接去找谦哥来的快。
我们要想办法把路径压缩成上图这样比较好。我们在Find函数里压缩,修改前驱就可以了。当然还有很多高明的压缩手法此处就不拓展了。
路径压缩:
int Find(int num){
int root=num,temp;
while(Arry[root]!=root)
root=Arry[root];
//路径压缩
while(num!=root){
temp=Arry[num];
Arry[num]=root;
num=temp;
}
return num;
}
2.1.3畸形树
回到之前的一个问题,“从此以后,是让谦哥认老郭做朋友圈老大,还是让老郭认谦哥做朋友圈老大”。
之前默认的是Arry[aRoot]=bRoot,为什么不写成Arry[bRoot]=aRoot?这样一味跪舔女神有意义吗?毕竟不是所有的单身狗都是哈士奇,也有很多独狼,就像人生苦短,有人选择及时行乐,有人选择创造价值,无所谓对错,只是个人选择而已。
并查集告诉我们,一味地,盲目地妥协只能造成畸形,从树的角度而言,下列哪种树最合适一目了然。
给出一张正规的比较图。
优化代码,小树附在大树旁,要给每一个节点一个Size,那么所有结点的Size可以存进一个数组里。
Size数组的初始化,size[i]=j意思是以i为根结点的树中,有j个节点,初始化,每个结点是相互独立的,只包括自己:
for (int i = 0; i < max; i++)
size[i] = 1;
优化一下union函数:
void Union(int a ,int b){
int aRoot=Find(a);
int bRoot=Find(b);
if(aRoot<bRoot){
Arry[aRoot]=bRoot;
size[bRoot]+=size[aRoot];
}
else {
Arry[bRoot]=aRoot;
size[aRoot]+=size[bRoot];
}
}
给出一张正规的优化前(上)和优化后(下)的比较图,很明显优化后的查找速率更快。接下来,用并查集解决一个实际问题。
2.2问题
问题:今天是Ignatius的生日。他邀请了许多朋友。现在是吃晚饭的时间了。Ignatius想知道他至少需要多少桌。你必须注意到并不是所有的朋友都认识对方,而且所有的朋友都不想和陌生人呆在一起。
这个问题的一个重要规则是,如果我告诉你A知道B,B知道C,意思是A,B,C互相了解,所以他们可以呆在一桌中。
例如:如果我告诉你A知道B,B知道C,D知道E,那么A,B,C可以呆在一桌,D,E必须留在另一桌。所以伊格至少需要2桌。
2.3解析
详细的就不用解释了,只是最后输出的是树的数量,最初Count=M,每次合并(Union)Count就要减1,或者找根结点的个数,循环数组序号与值相等的即为根结点。
AC源代码:
#include<iostream>
using namespace std;
#define max 25
//数组
int Arry[1003];
int size[1003];
//find
int Find(int num){
int root=num,temp;
while(Arry[root]!=root)
root=Arry[root];
//路径压缩
while(num!=root){
temp=Arry[num];
Arry[num]=root;
num=temp;
}
return num;
}
//connect
bool Connect(int a,int b){
if(Arry[a]==Arry[b])
return true ;
return false;
}
//union
void Union(int a ,int b){
int aRoot=Find(a);
int bRoot=Find(b);
if(aRoot==bRoot)
return ;
if(aRoot<bRoot){
Arry[aRoot]=bRoot;
size[aRoot]+=size[bRoot];
}
else {
Arry[bRoot]=aRoot;
size[bRoot]+=size[aRoot];
}
}
int main (){
int n,m;//m为数据组数,n为结点个数
int i,j,k;//循环用
int x,y;//输入的两个参数
int count;
cin>>k;
while(k--){
cin>>n>>m;
count=0;
//初始化size数组
for (i = 0; i <n; i++)
size[i] = 1;
//初始化Arry数组
for(i=0;i<n;i++)
Arry[i]=i;
for(i=0;i<m;i++){
cin>>x>>y;
Union(x-1,y-1);
}
for(j=0;j<n;j++){
if(Arry[j]==j)
count++;
}
cout<<count<<endl;
}
return 0;
}
2.3结果
3克鲁斯卡尔算法
3.1问题
问题:求上图的最小生成树。
3.2分析
这个问题,我之前在写【动态规划(二)】时用普里姆算法解析过了,这次用克鲁斯卡尔算法详解一次。这跟并查集有很重要的联系。
(1)依据贪心算法思想,要求最小生成树,那么每次需要寻找最小权值的边,然后依据边确定相应的结点
(2)找最小权值的边很容易,只需要升序排序即可,问题在于一个结点对应着多个边,各种交错,很容易形成环路,那样求得的肯定就不是最小生成树了。
(3)我们回到并查集,去掉所有的边(其实是把他们放进一个数组里按升序排序),把他们看成一个个独立的结点,
(4)把每个结点看成一个独立的树,遍历边权值数组,每次寻找最小的,如果边两端的结点所在树的根节点不通,则说明二者为两棵树,这样合并之后就不会出现环路的情况了。
(5)那么我们来给出克鲁斯卡尔算法的框架(可能会有些细微不同,因为我在尽量把它跟并查集的框架保持一致,)
#define Maxvex 9//最多点数
#define Maxedge 81//最多边数
#define INFINITY 65536
typedef struct
{
int begin;
int end;
int weight;
}Edge;
int Parent[Maxvex];//记录每个根结点的前驱结点
//不影响树的结构只起记录作用:不考虑畸形树的问题
int Find(int num){
int root = num, temp;
while (Parent[root]!= root)
root = Parent[root];
return num;
}
bool Union(int s, int e){
int sRoot = Find(s);
int eRoot = Find(e);
if (sRoot != eRoot){
Parent[eRoot] = sRoot;//不需要考虑畸形树的问题
return true;
}
return false;
}
void KrusKal(int M[Maxvex][Maxvex]){
Edge e[Maxedge],temp;
int i, j,t=0;
//初始化Parent数组,在大话数据结构中此处全初始化为0,原理是一样的,本人此处是为确保和上述所讲并查集保持一致
for (i = 0; i < Maxvex;i++){
Parent[i] = i;
}
//开始选边
for (i = 0; i < t;i++){
if (Union(e[i].begin,e[i].end))
cout << "(" << e[i].begin<< "," << e[i].end << "),"<<e[i].weight << endl;
}
}
(6)《大话数据结构》关于克鲁斯卡尔算法,跟我在此处的代码是有许多不同的。首先,它把union并入了void KrusKal(int M[Maxvex][Maxvex]),其次, Parent[i]它记录的是下一个结点,我们记录的是前驱Parent[eRoot] = sRoot;,从而该数组初始化可能不同,if (sRoot != eRoot)不需变动。再次,路径压缩与畸形树的优化可有可无,因为Parent不影响树的结构只起记录作用,最后给出源代码。
源代码:
#include<iostream>
using namespace std;
#define Maxvex 9//最多点数
#define Maxedge 81//最多边数
#define INFINITY 65536
typedef struct
{
int begin;
int end;
int weight;
}Edge;
int Parent[Maxvex];//记录每个根结点的前驱结点
//不影响树的结构只起记录作用:不考虑畸形树的问题
int Find(int num){
int root = num, temp;
while (Parent[root]!= root)
root = Parent[root];
//路径压缩
while (num != root){
temp = Parent[num];
Parent[num] = root;
num = temp;
}
return num;
}
bool Union(int s, int e){
int sRoot = Find(s);
int eRoot = Find(e);
if (sRoot != eRoot){
Parent[eRoot] = sRoot;//不需要考虑畸形树的问题
return true;
}
return false;
}
void KrusKal(int M[Maxvex][Maxvex]){
Edge e[Maxedge],temp;
int i, j,t=0;
//初始化Parent数组,在大话数据结构中此处全初始化为0,原理是一样的,本人此处是为确保和上述所讲并查集保持一致
for (i = 0; i < Maxvex;i++){
Parent[i] = i;
}
//处理矩阵,化成Edge
for (i = 0; i < Maxvex;i++){
for (j = i+1; j < Maxvex; j++){
if (M[i][j] < INFINITY){
e[t].begin = i;
e[t].end = j;
e[t].weight = M[i][j];
t++;
}
}
}
//根据贪心思想,每次选取最小的边,先对Edge按升序排序
for (i = 0; i <t; i++){
for (j = i + 1; j <t; j++){
if (e[i].weight> e[j].weight){
temp = e[i];
e[i] = e[j];
e[j] = temp;
}
}
}
//开始选边
for (i = 0; i < t;i++){
if (Union(e[i].begin,e[i].end))
cout << "(" << e[i].begin<< "," << e[i].end << "),"<<e[i].weight << endl;
}
}
int main(){
//构建图矩阵
int MyGraph[Maxvex][Maxvex] = {
{ 0, 10, INFINITY, INFINITY, INFINITY, 11, INFINITY, INFINITY, INFINITY },
{ 10, 0, 18, INFINITY, INFINITY, INFINITY, 16, INFINITY, 12 },
{ INFINITY, INFINITY, 0, 22, INFINITY, INFINITY, INFINITY, INFINITY, 8 },
{ INFINITY, INFINITY, 22, 0, 20, INFINITY, 24, 16, 21 },
{ INFINITY, INFINITY, INFINITY, 20, 0, 26, INFINITY, 7, INFINITY },
{ 11, INFINITY, INFINITY, INFINITY, 20, 0, 17, INFINITY, INFINITY },
{ INFINITY, 16, INFINITY, INFINITY, INFINITY, 17, 0, 19, INFINITY },
{ INFINITY, INFINITY, INFINITY, 16, 7, INFINITY, 19, 0, INFINITY },
{ INFINITY, 12, 8, 21, INFINITY, INFINITY, INFINITY, INFINITY, 0 } };
KrusKal(MyGraph);
return 0;
}
3.3结果
蓝色框为边出现顺序。
八个问题的源代码项目(19):https://github.com/AngryCaveman/C-Struct.git