讲解这两个经典算法的博文已经汗牛充栋了,这里再记录一下自己学习的心得,欢迎指正。
这是一个求某一个端点到其他所有端点的最短距离的算法,有贪心加深搜算法的特点。即在每一层都选现已知距离最短的点,确定下来,然后以此为基础更新未确定距离的点,再进行下一轮搜索。
其中贪心的合理性是因为:如果某顶点到src(出发点)的距离是所有未确定的点中最小的,那么就可以将之确定下来了,后面不会出现更优解–以另一个顶点为中间点过来–因为那些尚未确定距离点将来的结果路线,一定是借助之前就确定了最优解的点完成的。
这里贪心的特点是不会产生状态回退的情况,有也是只有一步回退。
示例数据:
6 8 (六个顶点(从1开始计数),八个边)
1 3 10
1 5 30
1 6 100
2 3 5
3 4 50
4 6 10
5 4 20
5 6 60
结果
id: 1 closest distance: 0
id: 2 closest distance: 4095(INF)
id: 3 closest distance: 10
id: 4 closest distance: 50
id: 5 closest distance: 30
id: 6 closest distance: 60
实现过程中,关键点是三个:1.标记一个顶点的最短距离是否已经确定2.更新未确定点的距离3.更新后再排序
一开始写犯得错误是,为了便捷的实现功能3,用优先队列,结果给另外两个功能的实现造成了麻烦。这里采用两个一维数组来处理就比较合适,用struct比较麻烦。一个来标记是否确定了距离,一个记录现在的距离。
判断结束点时用了一个trick,融入到了找最小值的过程中,若所有的点的最小距离都已经确定,包括那些到达不了的。这样没有那个点的距离是大于INF(假定的最大值)了,便可依次为标志结束循环。
#include
#include
#include
#include
#define MAX 100
#define INF 0xfff
using namespace std;
int main(int argc, char const *argv[])
{
int V, E, a, b, len;
int atlas[MAX][MAX];
int distance[MAX];
bool designed[MAX];
cin>>V>>E;
fill(distance, distance+V+1, INF);
fill(designed, designed+V, false);
for (int i=0;i<MAX;i++){
for (int j=0;j<MAX;j++){
if (i == j) atlas[i][j] = 0;
else atlas[i][j] = INF;
}
}
for (int i = 0; i < E; ++i)
{
cin>>a>>b>>len;
atlas[a][b] = len;
}
int src = 0;//设出发点为端点1(从1开始计数)
distance[src] = 0;
int d;
while(true){
d = V;
for (int i = 0; i < V; ++i)
{
if (!designed[i] && distance[i]<distance[d])
d = i;
}
if (d == V) break;
for (int i = 0; i < V; ++i)
{
distance[i] = min(distance[i], distance[d]+atlas[d+1][i+1]);
}
designed[d] = true;
}
for (int i = 0; i < V; ++i)
{
cout<<"id: "<<i+1<<" closest distance: "<<distance[i]<<endl;
}
return 0;
}
Prime和Kruskal算法是最小连通图生成算法,其中Prime是通过不断扩展已接入连通图的顶点的集合来完成连通图的构建。Kruskal是通过不断寻找最短的不会导致形成环路的边来构建连通图。
测试数据:六个顶点,十个边,无向图。
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
Prime算法实现时的要点有1.实现已加入连通图顶点集合和未加入连通图顶点集合的增删2.遍历两个集合间所有边,找出最小值
这里还是用一个bool数组维护即可,标记是否加入联通图,现在看来用其他结构还是比较麻烦,待后来有什么新发现吧。
#include
#include
#define MAX 100
#define INF 0xff
using namespace std;
int main(int argc, char const *argv[])
{
int atlas[MAX][MAX] = {0};
bool join[MAX];
int E, V, a, b, dis;
cin>>V>>E;
for (int i = 1; i <= V; ++i)
{
for (int j = 1; j <= V; ++j)
{
if (i == j) atlas[i][j] = 0;
else atlas[i][j] = INF;
}
}
for (int i = 0; i < E; ++i)
{
cin>>a>>b>>atlas[a][b];//直接输入:P
atlas[b][a] = atlas[a][b];
}
fill(join, join+V, false);
join[1] = true;
int min, extand, cost=0, sum;
while(true){
min = INF;
sum = 0;
//两层循环遍历一下
for (int i = 1; i <= V; ++i)
{
if (join[i])
for (int j = 1; j <= V; ++j){
if (!join[j] && min > atlas[i][j]){
min = atlas[i][j];
extand = j;
}
}
}
cout<<"extanding: "<<extand<<" costing: "<<min<<endl;
join[extand] = true;
cost += min;
//检查是否全为true了,即是否都加入连通图了
for (int i = 1; i <= V; ++i){
sum += join[i];
}
if (sum == V)
break;
}
cout<<"Costing: "<<cost<<endl;
return 0;
}
Krustal算法
这里的贪心key值是边的长度,排序后从小到大来添加,添加时注意检查是否生成了环路。关键点就是检查是否产生了环路,这里用并查集实现。
并查集是一种树结构,同一个集合的元素都挂到一个树里面,有共同根节点的元素即为同一个集合中的。这里每连一条边,都更新一下集合,用pre表示该元素的父节点,其中父节点的pre值是其本身。并且做一下状态压缩,将树的深度降低为一,减少查询时间。
#include
#include
#include
#define MAX 100
#define INF 0xfff
using namespace std;
int pre[MAX] = {0};
struct edge{
int a, b, dis;
edge(){
a = b = 0;
dis = INF;
}
};
bool cmp(edge a, edge b){
return a.dis < b.dis;
}
int find(int x){
int r = x;
while (r != pre[r]) r = pre[r];
int i = x, j;
while (i != pre[i]){//下面这一块就是状态压缩,让子树中的每个节点都直接指向根节点
j = pre[i];
pre[i] = r;
i = j;
}
return r;
}
void join(int a, int b){
int fa = find(a);
int fb = find(b);
if (fa != fb)
pre[fa] = fb;//子集合并,注意是把子集的父节点进行合并,不能直接把自己给并过去了
}
int main(int argc, char const *argv[])
{
//definition
int V, E, cost=0;
edge edges[MAX*MAX];
cin>>V>>E;
//input
for (int i = 0; i < E; ++i)
{
cin>>edges[i].a>>edges[i].b>>edges[i].dis;//赋值给结构数组
}
sort(edges, edges+E, cmp);
for (int i = 1; i <= V; ++i)
{
pre[i] = i;
}
for (int i = 0; i < E; ++i)
{
edge e = edges[i];
if (find(e.a) != find(e.b)){
join(e.b, e.a);
cost += e.dis;
}
}
printf("Costing: %d\n", cost);
return 0;
}