1 图的最小生成树
1.1 Kruskal算法(C语言)
①Kruskal是基于贪心策略的求加权连通图的最小生成树的算法。
②思路:首先按照边的权值进行从小到大排序,每次从剩余边中选择权值较小的且边的两个顶点不在同一个集合内的边(就是不会产生回路的边),加入到生成树中,知道加入了n-1条边为止。
③时间复杂度:快速排序+m条边找n-1条 且 (M比N大很多) ==> O(MlogM+MlogN)=O(MlogM)
#include<stdio.h>
struct edge
{
int u;
int v;
int w;
}; //为了方便排序这里创建了一个结构体用来存储边的关系
struct edge e[10]; //数组的大小根据实际情况来设置,要比边数m的最大值大1
int n, m;
int f[7] = { 0 }, sum = 0, count = 0; //并查集需要用到一些变量
//f数组的大小根据实际情况来设置,要比点数n的最大值大1
void quicksort(int left, int right)
{
int i, j;
struct edge t;
if (left > right)
return;
i = left;
j = right;
while (i != j)
{
//顺序很重要,从右边开始找
while (e[j].w > e[left].w && i < j)
j--;
//再从左边开始找
while (e[i].w <= e[left].w && i < j)
i++;
//交换
if (i < j)
{
t = e[i];
e[i] = e[j];
e[j] = t;
}
}
//最终将基数归位,即将left和i互换
t = e[left];
e[left] = e[i];
e[i] = t;
quicksort(left, i - 1); //继续处理左边的,这里是一个递归的过程
quicksort(i + 1, right); //继续处理右边的,这里是一个递归的过程
return;
}
//并查集寻找祖先的函数
int getf(int v)
{
if (f[v] == v)
return v;
else
{
//这里是路径压缩
f[v] = getf(f[v]);
return f[v];
}
}
//并查集合并两子集合的函数
int merge(int v, int u)
{
int t1, t2;
t1 = getf(v);
t2 = getf(u);
if (t1 != t2) //判断两个点是否在同一个集合中
{
f[t2] = t1;
return 1;
}
return 0;
}
int main()
{
int i;
printf("%s", "请输入:顶点数 边数\n");
scanf("%d %d", &n, &m);
//读入边,这里用一个结构体来存储边的关系
printf("%s", "请输入:起点 终点 权值\n");
for (i = 1; i <= m; i++)
scanf("%d %d %d", &e[i].u, &e[i].v, &e[i].w);
quicksort(1, m); //按照权值从小到大对边进行快速排序
//并查集初始化
for (i = 1; i <= n; i++)
f[i] = i;
//Kruskal算法核心部分:求加权连通图的最小生成树的算法
for (i = 1; i <= m; i++) //开始从小到大枚举每一条边
{
//判断一条边的两个顶点是否已经连通,即判断是否已在同一个集合中
if (merge(e[i].u, e[i].v)) //如果目前尚不连通,则选用这条边
{
count++;
sum = sum + e[i].w;
}
if (count == n - 1) //直到选用了n-1条边之后退出循环
break;
}
printf("最小生成树权值:%d", sum); //打印结果
getchar();
getchar();
return 0;
}
1.2 Prim算法(C语言)(最基础)
①贪心算法
②思路:首先任选一个顶点加入生成树,接下来从非树顶点中找到一条离生成树最近的顶点添加到生成树。照此方法操作n-1次,知道所有顶点都加入生成树中。
③
Dijkstra算法:dis记录的最短距离是每个顶点到1号顶点最短距离。若dis[k] > dis[j]+e[j][k] (1<=k<=n)则更新dis[k] = dis[j]+e[j][k] (1<=k<=n)。
Prim算法:dis记录的是每个顶点到任一个生成树的顶点的最短距离(到生成树的最短距离)。若dis[k] > e[j][k] (1<=k<=n)则更新dis[k] = e[j][k] (1<=k<=n)。
④时间复杂度
O(N2)–邻接矩阵、搜索
O(Mlog(N))–二叉堆、邻接表(堆用来选边,临接表用来存储数组)(1.3介绍)
#include<stdio.h>
int main()
{
int n, m, i, j, k, min, t1, t2, t3;
int e[7][7], dis[7], book[7] = { 0 }; //这里对book数组进行了初始化
//book数组标记哪些顶点加入了生成树,dis数组记录生成树到各个顶点距离
int inf = 99999999; //正无穷值
int count = 0, sum = 0; //count用来记录生成树中的顶点的个数,sum用来存储路径之和
printf("%s", "请输入:顶点数 边数\n");
scanf("%d %d", &n, &m);
//初始化
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
if (i == j) e[i][j] = 0;
else e[i][j] = inf;
//读入边
printf("%s", "请输入:起点 终点 权值\n");
for (i = 1; i <= m; i++)
{
scanf("%d %d %d", &t1, &t2, &t3);
//注意这里是无向图,所以需要将边反向再存储一遍
e[t1][t2] = t3;
e[t2][t1] = t3;
}
//初始化dis数组,这里是1号顶点到其余各顶点的初始路程,因为当前生成树只有1号顶点
for (i = 1; i <= n; i++)
dis[i] = e[1][i];
//Prim算法核心语句
//将1号顶点加入生成树
book[1] = 1; //这里用book来标记一个顶点是否已经加入生成树
count++;
while(count<n)
{
min = inf;
for (i = 1; i <= n; i++)
{
if (book[i] == 0 && dis[i] < min)
{
min = dis[i];
j = i;
}
}
book[j] = 1;
count++;
sum = sum + dis[j];
//扫描当前顶点j所有的边,再以j为中间点,更新生成树到每一个非树顶点的距离
for (k = 1; k <= n; k++)
{
if (book[k] == 0 && dis[k] > e[j][k])
dis[k] = e[j][k];
}
}
//输出
printf("%s", "最小生成树的权值和是:\n");
printf("%d ", sum);
getchar();
getchar();
return 0;
}
1.3 Prim算法(C语言)(优化)
①用堆选边,用临接表存储图。
②h建最小堆的时候,并非按照顶点编号大小建,而是按照顶点在数组dis中所对应的值来建最小堆。
#include<stdio.h>
int dis[7], book[7] = { 0 }; //这里对book数组进行了初始化
//book数组标记哪些顶点加入了生成树,dis数组记录生成树到各个顶点距离
int h[7], pos[7], size; //h用来保存堆,pos用来存储每个顶点在堆中的位置,size为堆的大小
//交换函数,用来交换堆中的两个元素的值
void swap(int x, int y)
{
int t;
t = h[x];
h[x] = h[y];
h[y] = t;
//同步更新pos
t = pos[h[x]];
pos[h[x]] = pos[h[y]];
pos[h[y]] = t;
return;
}
//向下调整函数
void siftdown(int i) //传入一个需要向下调整的结点编号
{
int t, flag = 0; //flag用来标记是否需要继续向下调整
while (i * 2 <= size && flag == 0)
{
//比较i和它左儿子i*2在dis中的值,并用t纪录值较小的结点编号
if (dis[h[i]] > dis[h[i * 2]])
t = i * 2;
else
t = i;
//如果它有右儿子,再对右儿子进行讨论
if (i * 2 + 1 <= size)
{
//如果右儿子的值更小,更新较小的结点编号
if (dis[h[t]] > dis[h[i * 2 + 1]])
t = i * 2 + 1;
}
//如果发现最小的结点编号不是自己,说明子结点中有比父结点更小的
if (t != i)
{
swap(t, i); //交换它们,注意swap函数需要自己来写
i = t; //更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整
}
else
flag = 1; //否则说明当前的父结点已经比两个子结点都要小了,不需要再进行调整了
}
return;
}
void siftup(int i) //传入一个需要向上调整的结点编号i
{
int flag = 0; //用来标记是否需要继续向上调整
if (i == 1) return; //如果是堆顶,就返回,不需要调整了
//不在堆顶,并且当前结点i的值比父结点小的时候继续向上调整
if (i != 1 && flag == 0)
{
//判断是否比父结点的小
if (dis[h[i]] < dis[h[i / 2]])
swap(i, i / 2); //交换它和它爸爸的位置
else
flag = 1; //表示已经不需要再进行调整了,当前结点的值比父结点的值要大
i = i / 2; //这句话很重要,更新编号i为它父结点的编号,从而便于下一次继续向上调整
}
return;
}
//从堆顶取出一个元素(取第一个元素)
int pop()
{
int t;
t = h[1]; //用一个临时变量巨鹿堆顶点的值
pos[t] = 0; //其实这句话要不要无所谓
h[1] = h[size]; //将堆的最后一个点赋值到堆顶
pos[h[1]] = 1;
size--; //堆的元素减少1
siftdown(1); //向下调整
return t; //返回之前记录的堆顶点
}
int main()
{
int n, m, i, j, k;
//u,v,w和next的数组大小要根据实际情况来设置,此图是无向图,要比2*m的最大值要大1
//first要比n的最大值要大1,要比2*m的最大值要大1
int u[19], v[19], w[19], first[7], next[19];
int inf = 99999999; //正无穷值
int count = 0, sum = 0; //count用来记录生成树中的顶点的个数,sum用来存储路径之和
printf("%s", "请输入:顶点数 边数\n");
scanf("%d %d", &n, &m);
//读入边
printf("%s", "请输入:起点 终点 权值\n");
for (i = 1; i <= m; i++)
scanf("%d %d %d", &u[i], &v[i], &w[i]);
//注意这里是无向图,所以需要将边反向再存储一遍
//这里使用三个数组来存储边的关系,和以前用临接矩阵存储不同
for (i = m + 1; i <= 2 * m; i++)
{
u[i] = v[i - m];
v[i] = u[i - m];
w[i] = w[i - m];
}
//开始使用临接表存储边
for (i = 1; i <= n; i++) first[i] = -1;
for (i = 1; i <= 2 * m; i++)
{
next[i] = first[u[i]];
first[u[i]] = i;
}
//Prim算法核心语句
//将1号顶点加入生成树
book[1] = 1; //这里用book来标记一个顶点是否已经加入生成树
count++;
//初始化dis数组,这里是1号顶点到其余各顶点的初始路程,因为当前生成树只有1号顶点
dis[1] = 0;
for (i = 2; i <= n; i++) dis[i] = inf;
k = first[1];
while (k != -1)
{
dis[v[k]] = w[k];
k = next[k];
}
//初始化堆
size = n;
for (i = 1; i <= size; i++)
{
h[i] = i;
pos[i] = i;
}
for (i = size / 2; i >= 1; i--)
{
siftdown(i);
}
pop(); //先弹出一个堆顶元素(1号点),此时堆顶是2号顶点
while (count<n)
{
j = pop();
book[j] = 1;
count++;
sum = sum + dis[j];
//扫描当前顶点j所有的边,再以j为中间点,更新生成树到每一个非树顶点的距离
k = first[j];
while (k != -1)
{
if (book[v[k]] == 0 && dis[v[k]] > w[k])
{
dis[v[k]] = w[k]; //更新距离
siftup(pos[v[k]]); //对该点在堆中进行向上调整
//pos[v[k]]存储的是顶点v[k]在堆中的位置
}
k = next[k];
}
}
//输出
printf("%s", "最小生成树的权值和是:\n");
printf("%d ", sum);
getchar();
getchar();
return 0;
}
1.4 小结
①Kruskal算法是一步步将森林中的树进行合并,而Prim算法则是通过每次增加一条边来建立一棵树。
②Kruskal算法更适用于稀疏图,没有使用堆优化的Prim算法适用于稠密图,使用了堆优化的Prim算法则更适用于稀疏图。
③如果所有边权都不想等,那么最小生成树是唯一的。
④Prim又被称为DJP算法、Jarnik算法、Prim-Jarnik算法。