就是给有向带权图中指定一个特殊的点root,求一棵以root为根的有向生成树T,并且T中所有边的总权值最小。解决这种问题要用到朱刘算法。朱刘算法有以下五步:
1、找到除了root以为其他点的权值最小的入边。用In[i]记录
2、如果出现除了root以为存在其他孤立的点,则不存在最小树形图。
3、找到图中所有的环,并对环进行缩点,重新编号。
4、更新其他点到环上的点的距离,如:(in【j】表示到j结点的入度中最小的权值)
环中的点有(Vk1,Vk2,… ,Vki)总共i个,用缩成的点叫Vk替代,则在压缩后的图中,其他所有不在环中点v到Vk的距离定义如下:
gh[v][Vk]=min { gh[v][Vkj]-in[Vkj] } (1<=j<=i)
而Vk到v的距离为
gh[Vk][v]=min { gh[Vkj][v] } (1<=j<=i)
5、重复3,4知道没有环为止。
为什么出边的权不变,入边的权要减去in [u]?
对于新图中的最小树形图T,设指向人工节点的边为e。将人工节点展开以后,e指向了一个环。假设原先e是指向u的,这个时候我们将环上指向u的边 in[u]删除,这样就得到了原图中的一个树形图。我们会发现,如果新图中e的权w'(e)是原图中e的权w(e)减去in[u]权的话,那么在我们删除掉in[u],并且将e恢复为原图状态的时候,这个树形图的权仍然是新图树形图的权加环的权,而这个权值正是最小树形图的权值。所以在展开节点之后,我们得到的仍然是最小树形图。逐步展开所有的人工节点,就会得到初始图的最小树形图了。
流程如下图:
#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <string>
typedef long long LL;
using namespace std;
const int MAXV = 100;
const int MAXE = 10000;
const int INF = 0x3f3f3f3f;
//切记初始化还有判自环
int V; //n个点
int map[MAXV + 7][MAXV + 7];
bool visited[MAXV + 7];
bool flag[MAXV + 7];//缩点标记为true,否则仍然存在
int pre[MAXV + 7];//点i的父节点为pre[i]
//以root为根节点的最小树形图
int zhuliu(int root){
int sum = 0;//最小树形图的权值
int i, j, k;
pre[root] = root;
while(true){
for(i = 1; i <= V; i++){//求最短弧集合E0
if(flag[i] || i == root) continue;
pre[i] = i;
for(j = 1; j <= V; j++)
if(!flag[j] && map[j][i] < map[pre[i]][i])
pre[i] = j;
if(pre[i] == i) return -1;
}
for(i = 1; i <= V; i++){//检查E0
if(flag[i] || i == root) continue;
for(j = 1; j <= V; j++) visited[j] = false;
visited[root] = true;
j = i;//从当前点开始找环
do{
visited[j] = true;
j = pre[j];
}while(!visited[j]);
if(j == root)continue;//没找到环
i = j;//收缩G中的有向环
do{//将整个环的取值保存,累计计入原图的最小树形图
sum += map[pre[j]][j];
j = pre[j];
}while(j != i);
j = i;
do{//对于环上的点有关的边,修改其权值
for(k = 1; k <= V; k++)
if(!flag[k] && map[k][j] < INF && k != pre[j])
map[k][j] -= map[pre[j]][j];
j = pre[j];
}while(j != i);
for(j = 1; j <= V; j++){//缩点,将整个环缩成i号点,所有与环上的点有关的边转移到点i
if(j == i) continue;
for(k = pre[i]; k != i; k = pre[k]){
if(map[k][j] < map[i][j]) map[i][j] = map[k][j];
if(map[j][k] < map[j][i]) map[j][i] = map[j][k];
}
}
for(j = pre[i]; j != i; j = pre[j]) flag[j] = true;//标记环上其他点为被缩掉
break;//当前环缩点结束,形成新的图G',跳出继续求G'的最小树形图
}
if(i > V){//如果所有的点都被检查且没有环存在,现在的最短弧集合E0就是最小树形图.累计计入sum,算法结束
for(i = 1; i <= V; i++)
if(!flag[i] && i != root) sum += map[pre[i]][i];
break;
}
}
return sum;
}
int main()
{
}
虽然不想承认,但是邻接矩阵存储真的clumsy,再写个结构体存储的。
const int inf = 2e9;
const int maxn = 105;
const int maxm = 100050;
int n, m;//顶点数,边数
int in[maxn];//in[u]记录当前图中指向u结点的所有边权中最小的那条边权
int pre[maxn];//pre[u]记录最小边权对应的父亲结点
int used[maxn], id[maxn];//used是访问标记数组,id[u]是计算出u在下一次的新图中的编号
struct Edge {
int from, to;
int dist;
Edge(int f = 0, int t = 0, int d = 0) :from(f), to(t), dist(d) {}
}edges[maxm];//边集
int direct_mst(int root, int V, int E) {//三个参数分别是根结点,顶点数量,边数量
int ans = 0;
while (1) {
//为每个非根结点选出最小入边
for (int i = 0; i < V; ++i) in[i] = inf;
for (int i = 0; i < E; ++i) {
int u = edges[i].from;
int v = edges[i].to;
if (in[v] > edges[i].dist && u != v) {
in[v] = edges[i].dist;
pre[v] = u;
}
}
//判断连通性,如有不可达结点说明无解
for (int i = 0; i < V; ++i) {
if (i == root) continue;
if (inf == in[i]) return -1;
}
//判断有向环是否存在,存在有向环就缩圈
int cnt = 0;//生成新图的结点编号
memset(id, -1, sizeof(id));//id[u]==-1表示结点u还不属于任何一个自环
memset(used, -1, sizeof(used));
in[root] = 0;
for (int i = 0; i < V; ++i) {
ans += in[i];
int v = i;
while (used[v] != i && id[v] == -1 && v != root) {//每个结点不断向上寻找父亲结点,要么找到根结点,要么形成一个自环
used[v] = i;
v = pre[v];
}
if (v != root && id[v] == -1) {//找到了自环,进行缩点,更新id数组
for (int u = pre[v]; u != v; u = pre[u]) id[u] = cnt;
id[v] = cnt++;
}
}
if (0 == cnt) break;//没有自环说明已经求出最终结果
//建立新图
for (int i = 0; i < V; i++)
if (id[i] == -1) id[i] = cnt++;//先把不在自环中的点的编号更新
for (int i = 0; i < E; i++) {
int u = edges[i].from;
int v = edges[i].to;
edges[i].from = id[u];
edges[i].to = id[v];
if (id[u] != id[v]) edges[i].dist -= in[v];
//这里id[u] != id[v]说明edges[i]这条边原来不在有向环中,
//如果这条边指向了有向环,那么它的边权就要减少in[v]等价于整个环的边权减去in[v]
//而如果没有指向有向环,说明它与这个有向环毫无关系,那么在之前的寻找自环缩点过
//程中已经把这条边的权值加上了,所以这里避免重复计算让这条边的权值减小in[v]变为0
}
V = cnt;
root = id[root];
}
return ans;
}