上机实践报告
一、目的
1.熟悉算法设计的基本思想
2.熟悉并查集
二、内容与设计思想
解决OJ上的练习题。
简单描述优化复杂度的方案,比如按秩合并,路径压缩。
*注1:练习题只有正确性测试点,请至少列出不同实现的代码的时间(多次求平均)。
*注2:可以自己设计测试方式和数据进行比较。
*注3:可以自己额外找编程问题来进行比较。
有$N$个城镇,现有城镇道路统计表,表中列出了每条道路直接连通的城镇。目标是使任何两个城镇间都可以连通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
每个测试点都有多组输入,对于每组输入:
第一行输入两个正整数$N(1 <= N<= 20000)$和$M(1<= M <= 20000)$, $N$表示城镇个数,$M$表示道路个数。
接下来$M$行,每行两个正整数$a,b ( 1<= a,b <= N)$,用两个城镇表示一条道路。
在所有输入的结尾,有一个0单独成行,表示输入文件的结束。
保证每个测试点的输入组数不超过50组。
三、使用环境
推荐使用C/C++集成编译环境。
练习地址:**[oj | 《算法设计与分析》第七次实验课](http://47.100.233.213/contest/96/problems)**(密码40088860) |
截止时间:2021年11月28日 23:30
提交地址:算法设计与分析-实践报告7 (jianguoyun.com)
不建议随意改变本文档以上部分的结构,以下部分可以按需扩充。但不要缺少“实验过程”和“总结”。
四、实验过程
1.并查集最基础算法实现
源代码
#include <bits/stdc++.h>
using namespace std;
int bin[20000];
//构造并查集——一开始,我们先将它们的父节点设为自己。
void make_set(int n)
{
for (int i = 1; i <= n; i++)
{
bin[i] = i;
}
}
//寻找连接的城市——也就是查询树根——我们用递归的写法实现对代表元素的查询:一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。
int Find(int x)
{
if (x == bin[x])
{
return x;
}
else
{
return Find(bin[x]);
}
}
//将两点(城市)连接在一起——合并操作也是很简单的,先找到两个集合的代表元素,然后将前者的父节点设为后者即可。当然也可以将后者的父节点设为前者
void UNION(int x, int y)
{
bin[Find(x)] = Find(y);
}
int main()
{
int n, m;
//int bin[n];
cin >> n;
while (n > 0)
{
cin >> m;
make_set(n);
int x, y;
for (int i = 1; i <= m; i++)
{
cin >> x >> y;
UNION(x, y);
}
int counts = 0;
for (int i = 1; i <= n; i++)
{
if (Find(i) == i)
counts++;
}
cout << counts - 1 << endl;
cin >> n;
}
return 0;
}
2.用路径压缩进行优化
在基础算法的基础上需要修改的只有Find函数
//改进就是在找的过程中把每一个节点的父节点都设置为根节点,这样下一次再查询的时候会省去很多事情
int Find(int x)
{
if(x ==bin[x])
{
return x;
}
else
{
return bin[x]=Find(bin[x]);
}
}
3.用按秩合并进行优化
在基础算法的基础上需要修改的有make_set函数和union函数
void make_set(int n)
{
for (int i = 1; i <= n; i++)
{
bin[i] = i;
Rank[i]=1;//一开始将每个点的rank设为1
}
}
void UNION(int i, int j)
{
int x = Find(i), y = Find(j); //先找到两个根节点
if (Rank[x] <= Rank[y])
bin[x] = y;
else
bin[y] = x;
if (Rank[x] == Rank[y] && x != y)
Rank[y]++; //如果深度相同且根节点不同,则新的根节点的深度+1
}
4.同时使用路径压缩和按秩合并
就是find, make_set,union函数都修改,修改如上就不再赘述了
5.画图比较几种算法的运行时间
ID | 普通算法 | 路径压缩优化 | 按秩合并优化 | 路径压缩+按秩合并 |
---|---|---|---|---|
数据1 | 19ms | 20ms | 22ms | 9ms |
数据2 | 1455ms | 30ms | 31ms | 20ms |
6.对并查集的进一步运用
运用1-亲戚问题
这道题就可以运用并查集来解,我们可以把每个人分到不相交的集合,每个集合里的人彼此是亲戚,判断两个人是否是亲戚,即看他们是否属于一个集合即可
与道路问题不同的地方在于只用改其主函数即可
int main()
{
int n, m, p;
int x,y;
cin>>n>>m>>p;
make_set(n);
for (int i = 0; i < m; ++i)
{
cin>>x>>y;
union(x, y);
}
for (int i = 0; i < p; ++i)
{
cin>>x>>y;
if(Find(x)==Find(y))
cout<<"Yes";
else
cout<<"No";
}
return 0;
}
运用二-奶酪问题
思路::经过思考后我发现其实这道题也可以用并查集来实现,把所有空洞划分为若干个集合,当两个空洞相交或相切,就把它们放到同一个集合中,也就是union一下。然后再划出2个特殊元素,分别表示底部和顶部,如果一个空洞与底部接触,则把它与表示底部的元素放在同一个集合中,顶部同理。最后,只需要看顶部和底部是不是在同一个集合中即可。
五、总结
关于本次实验,进一步加深了对并查集的理解,我们可以看到当对算法进行优化时,数据二的运行时间发生了明显的缩短。寻找根节点时,基本算法就是采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find(x)都是O(n)的复杂度。路径压缩就是说把沿途的每个节点的父节点都设为根节点即可,这样以后再次Find(x)时复杂度就变成O(1)了。按秩合并就是说使具有较少结点的树的根指向较多结点的树的根。路径压缩和按秩合并如果一起使用,对n个元素上的m个不相交集合操作,联合使用按秩合并与路径压缩启发式策略后的运行时间是O(mAlpha(n)),这里Alpha是Ackerman函数的某个反函数,在很大的范围内,这个函数的值可以看成是不大于4的,所以并查集的操作可以看作是线性的。
文档信息
- 本文作者:Liu Jiafan
- 本文链接:https://jiapang777777.github.io//2023/04/01/%E5%B9%B6%E6%9F%A5%E9%9B%86/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)