【算法精讲】次小生成树 配套例题(HDU 4081 POJ 1679)

概述

最小生成树算法, 即在一个有N个点的有权无向图中选中N-1条边, 使得各个顶点直接相互连通且权值和最小, 这样的一个算法称为最小生成树算法(MST), 通过经典的Prim或Kruskal算法都可以有效求解

可在此问题的基础之上, 如果我们要求求出次小生成树, 或第k小生成树, 这样的算法又该如何实现呢 ?

算法思想

次小生成树算法是建立在最小生成树算法之上的, 首先我们求出最小生成树, 接下来我们枚举每条不在MST上的边, 并尝试把这条边加入到MST中; 根据MST的原理可以想到就一定会形成环, 这样的话我们再在环路中取出一条最长的路(新加入的边除外). 最终得到的结果就一定是次小生成树

大致思路就是: 枚举加边 -> 一定成环 -> 去掉环中最长边 -> 得到次小生成树

算法实现

既然是建立在MST的基础之上, 通过Prim或Kruskal算法自然都可以实现次小生成树, 下边我们分别进行介绍

Prim实现

对Prim进行改写的过程中, 需要定义几个数组

mst[i][j] : 表示该边是否已在MST中
pre[i] : 表示 i 的父节点
maxd[i][j] : 表示MST中从i -> j 的最大值

这里的重点是如何求出maxd, 这里用到了动态规划的思想
首先要明确, maxd是在MST中的边, maxd[u][v]的一定是一条边上的两点
这样的话只会有两种情况, 即取和不取该边

例如如下图中我们求maxd[u][v]的值, 我们就需要取<4, 5>该边
【算法精讲】次小生成树 配套例题(HDU 4081 POJ 1679)_第1张图片
再例如如下情况, 我们就不取<4, 5>该边
【算法精讲】次小生成树 配套例题(HDU 4081 POJ 1679)_第2张图片

可得状态转移方程 maxD[u][v] = max(maxD[pre[u]][v], d[u])
tips: 不太理解的同学把4看做u, 5看做v, 结合两种情况, 仔细在图上思考一下. 注意更新方程的两个条件, 一定是一条边上的两个点, u, v一定都已经在MST中

int N, M, w[maxn][maxn];
int d[maxn];
bool used[maxn];
int maxD[maxn][maxn];   //MST中从i->j的最大权值
int pre[maxn];          //某一点父节点
bool mst[maxn][maxn];   //该点是否已经在MST中
typedef pair<int, int> P;
int Prim(int s) {
    fill(d, d + maxn, inf);
    ms(maxD, 0);
    ms(used, 0);
    ms(mst, 0);
    fill(pre, pre + maxn, s);
    priority_queue<P, vector<P>, greater<P> > q;
    q.push(P(d[s] = 0, s));
    int res = 0;
    while(!q.empty()) {
        P cur = q.top();
        q.pop();
        int u = cur.second;
        if(used[u])
            continue;
        used[u] = true, res += d[u];
        mst[u][pre[u]] = mst[pre[u]][u] = true; //加入到MST中
        for(int v = 1; v <= N; ++v) {
            if(used[v] && w[u][v] < inf)        //只更新MST中的
                maxD[u][v] = maxD[v][u] = max(maxD[pre[u]][v], d[u]);
            if(w[u][v] < d[v]) {
                d[v] = w[u][v];
                pre[v] = u;                     //更新父节点
                q.push(P(d[v], v));
            }
        }
    }
    return res;
}

这里重点就是求出这几个数组的值, 至于是求次小生成树还是其他相关操作通过这几个数组的辅助, 就非常简单了!

Kruskal实现

kruskla算法中我们枚举的边权值会依次增大,那么就会给我们更新maxd提供一定的便利,但是因为kruskal的实现方式和prim有所不同,所以kruskal需要存储当前最小生成树中的节点(邻接矩阵),然后我们再去更新maxd数组

要注意几个细节

  1. 这里因为父节点数组共用了并查集的par数组, 所以在unite中不能使用路径压缩
  2. 直接对u的父节点进行操作即可, v的父节点一定还是v
#define ms(x, n) memset(x,n,sizeof(x));
typedef  long long LL;
const int inf = 1 << 30;
const LL maxn = 110;

int N, M;
struct Edge {
    int u, v, w;
} es[maxn * maxn];

int par[maxn], rak[maxn];
void init(int n) {
    for(int i = 0; i <= n; i++)
        par[i] = i, rak[i] = 0;
}
int findr(int x) {
    if(x == par[x]) return x;
    else return par[x] = findr(par[x]);
}
bool isSame(int x, int y) {return findr(x) == findr(y);}


bool cmp(const Edge &a, const Edge &b) {return a.w < b.w;}
int maxd[maxn][maxn];  //i->j的最大权值
bool used[maxn*maxn];  //改变是否已经在MST中
vector<int> mst[maxn]; //存储MST
void Kruskal() {
    sort(es+1, es+1+M, cmp);
    init(N);
    ms(maxd, 0); ms(mst, 0); ms(used, 0);
    for(int i = 0; i <= N; ++i)
        mst[i].push_back(i);

    int sum = 0;
    for(int i = 1; i <= M; i++) {
        int u = findr(es[i].u), v = findr(es[i].v), w = es[i].w;
        if(!isSame(u, v)) {
            sum += w, used[i] = true;
            for(int j = 0; j < mst[u].size(); ++j)
                for(int k = 0; k < mst[v].size(); ++k)
                    maxd[mst[u][j]][mst[v][k]] = maxd[mst[v][k]][mst[u][j]] = w;
            par[u] = v;
            for(int j = 0; j < mst[u].size(); ++j)
                mst[v].push_back(mst[u][j]);    //单向存储即可
        }
    }
    int csum = inf;
    for(int i = 1; i <= M; ++i)
        if(!used[i])
            csum = min(csum, sum+es[i].w-maxd[es[i].u][es[i].v]);
    cout << sum << " " << csum << endl;
}

例题 1

The Unique MST POJ - 1679

从前有两只猫。它们实在不知道该出什么题了。

于是它们放弃了治疗,开始玩一个游戏:从乡镇地图中已有的道路里面删除一些道路,并且删除完毕后图仍然是连通的。在所有方案中,删除道路总长度最大的方案为最优方案。

两只猫同时完成了这个游戏。它们都坚信自己是赢家。已知它们的完成方式不同,请判断有没有可能它们的实现方案都是最优的。

Input

第一行是一个整数 t (1 <= t <= 20), 测试用例的数量。每个用例代表一张图,第一行是n和m (1 <= n <= 100), 分别为城镇数和道路数。接下来m行为m个三元组 (xi, yi, wi),表示编号为xi和yi的城镇被长度为wi的道路连接。两个城镇之间最多被一条道路连接。

Output

对于每个用例,如果答案为否定(即不可能都是最优方案),输出最优方案剩余的(注意不是删除的)道路总长度。否则输出字符串 ‘Not Unique!’(不带引号)。

Examples

Sample Input
2
3 3
1 2 1
2 3 2
3 1 3
4 4
1 2 2
2 3 2
3 4 2
4 1 2
Sample Output
3
Not Unique!

题意:

判断最小生成树和次小生成树是否相等, 相等即输出Not Unique!

题解:

这里就用到了我们刚刚的模板, 直接套上, Prim的话判断为加入到MST的边权是否等于当前边的maxd, 如果相等就说明一定存在与MST相等的次小生成树;
Kruskal直接判断ssum是否==sum即可

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define ms(x, n) memset(x,n,sizeof(x));
typedef  long long LL;
const int inf = 1<<30;
const LL maxn = 110;

int N, M, w[maxn][maxn];
int d[maxn];
bool used[maxn];
int maxD[maxn][maxn];   //MST中从i->j的最大权值
int pre[maxn];          //某一点父节点
bool mst[maxn][maxn];   //该点是否已经在MST中
typedef pair<int, int> P;
int Prim(int s){
    fill(d, d+maxn, inf); ms(maxD, 0);
    ms(used, 0); ms(mst, 0);
    fill(pre, pre+maxn, s);
    priority_queue<P, vector<P>, greater<P> > q;
    q.push(P(d[s]=0, s));
    int res = 0;
    while(!q.empty()){
        P cur = q.top();
        q.pop();
        int u = cur.second;
        if(used[u]) continue;
        used[u] = true, res += d[u];
        mst[u][pre[u]] = mst[pre[u]][u] = true; //加入到MST中
        for(int v = 1; v <= N; ++v){
            if(used[v] && w[u][v]<inf)          //只更新MST中的
                maxD[u][v] = maxD[v][u] = max(maxD[pre[u]][u], d[u]);
            if(w[u][v] < d[v]){
                d[v] = w[u][v];
                pre[v] = u;                     //更新父节点
                q.push(P(d[v], v));
            }
        }
    }
    return res;
}

int main()
{
    int T, a, b, c;
    cin >> T;
    while(T--){
        fill(w[0], w[0]+maxn*maxn, inf);
        cin >> N >> M;
        while(M--){
            cin >> a >> b >> c;
            w[a][b] = w[b][a] = c;
        }
        int ans = Prim(1);
        bool flag = false;  //次小生成树是否等于最小生成树
        for(int u = 1; u <= N && !flag; ++u){
            for(int v = 1; v <= N; ++v){
                if(mst[u][v] || w[u][v]==inf)
                    continue;
                if(w[u][v] == maxD[u][v]){
                    flag = true;
                    break;
                }
            }
        }
        if(flag) cout << "Not Unique!\n";
        else cout << ans << endl;
    }
	return 0;
}

例题 2

Qin Shi Huang’s National Road System HDU - 4081

During the Warring States Period of ancient China(476 BC to 221 BC), there were seven kingdoms in China ---- they were Qi, Chu, Yan, Han, Zhao, Wei and Qin. Ying Zheng was the king of the kingdom Qin. Through 9 years of wars, he finally conquered all six other kingdoms and became the first emperor of a unified China in 221 BC. That was Qin dynasty ---- the first imperial dynasty of China(not to be confused with the Qing Dynasty, the last dynasty of China). So Ying Zheng named himself “Qin Shi Huang” because “Shi Huang” means “the first emperor” in Chinese.

Qin Shi Huang undertook gigantic projects, including the first version of the Great Wall of China, the now famous city-sized mausoleum guarded by a life-sized Terracotta Army, and a massive national road system. There is a story about the road system:
There were n cities in China and Qin Shi Huang wanted them all be connected by n-1 roads, in order that he could go to every city from the capital city Xianyang.
Although Qin Shi Huang was a tyrant, he wanted the total length of all roads to be minimum,so that the road system may not cost too many people’s life. A daoshi (some kind of monk) named Xu Fu told Qin Shi Huang that he could build a road by magic and that magic road would cost no money and no labor. But Xu Fu could only build ONE magic road for Qin Shi Huang. So Qin Shi Huang had to decide where to build the magic road. Qin Shi Huang wanted the total length of all none magic roads to be as small as possible, but Xu Fu wanted the magic road to benefit as many people as possible ---- So Qin Shi Huang decided that the value of A/B (the ratio of A to B) must be the maximum, which A is the total population of the two cites connected by the magic road, and B is the total length of none magic roads.
Would you help Qin Shi Huang?
A city can be considered as a point, and a road can be considered as a line segment connecting two points.

Input

The first line contains an integer t meaning that there are t test cases(t <= 10).
For each test case:
The first line is an integer n meaning that there are n cities(2 < n <= 1000).
Then n lines follow. Each line contains three integers X, Y and P ( 0 <= X, Y <= 1000, 0 < P < 100000). (X, Y) is the coordinate of a city and P is the population of that city.
It is guaranteed that each city has a distinct location.

Output

For each test case, print a line indicating the above mentioned maximum ratio A/B. The result should be rounded to 2 digits after decimal point.

Examples

Sample Input
2
4
1 1 20
1 2 30
200 2 80
200 1 100
3
1 1 20
1 2 30
2 2 40
Sample Output
65.00
70.00



题意:

给出N个点的坐标以及每个点的人口, 要求将这N个点通过N-1条边连接起来, 权值为两点直接距离, B为距离和, 同时可以选中一条边, 使得该边权值变为0, A为该边两点人口数量. 求A/B的最大值

题解:

既然要求A/B的最大值, 就一定要A最大, B最小, 所以B需要求一下MST, A的话我们直接枚举每条边就好了, 如果该边已经在MST中, ans = max(ans, A/(B-w[i][j])); 否则ans = max(ans, A/(B-maxD[i][j]));

可以看到, 虽然这里没有直接使用次小生成树, 但是完全用到了次小生成树求出的几个数组, 这也是前面为何没有直接求出次小生成树的原因, 因为大部分的题目必然不是裸题, 所以更要仔细理解次小生成树的思想


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define ms(x, n) memset(x,n,sizeof(x));
typedef  long long LL;
const int inf = 1 << 30;
const LL maxn = 1010;

int N;
double w[maxn][maxn];
struct node {
    int x, y;
    int p;
    node(int xx, int yy, int pp) {x = xx, y = yy, p = pp;}
    node() {}
} vs[maxn];
double getDis(int x1, int y1, int x2, int y2) {
    return sqrt((double)(x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2));
}

double d[maxn];
bool used[maxn];
double maxD[maxn][maxn];   //MST中从i->j的最大权值
int pre[maxn];          //某一点父节点
bool mst[maxn][maxn];   //该点是否已经在MST中
typedef pair<int, int> P;
double Prim(int s) {
    fill(d, d + maxn, inf);
    fill(pre, pre+maxn, s);
    ms(maxD, 0); ms(used, 0); ms(mst, 0);
    priority_queue<P, vector<P>, greater<P> > q;
    q.push(P(d[s] = 0, s));
    double res = 0;
    while(!q.empty()) {
        P cur = q.top();
        q.pop();
        int u = cur.second;
        if(used[u])
            continue;
        used[u] = true, res += d[u];
        mst[u][pre[u]] = mst[pre[u]][u] = true; //加入到MST中
        for(int v = 1; v <= N; ++v) {
            if(used[v] && w[u][v] < inf)        //只更新MST中的
                maxD[u][v] = maxD[v][u] = max(maxD[pre[u]][v], d[u]);
            if(w[u][v] < d[v]) {
                d[v] = w[u][v];
                pre[v] = u;                     //更新父节点
                q.push(P(d[v], v));
            }
        }
    }
    return res;
}
int main() {
    int T, a, b, c;
    scanf("%d",&T);
    while(T--) {
        ms(vs, 0); fill(w[0], w[0]+maxn*maxn, inf);
        scanf("%d",&N);
        for(int i = 1; i <= N; ++i) {
            scanf("%d%d%d",&a,&b,&c);
            vs[i] = node(a, b, c);
        }
        for(int i = 1; i < N; ++i)
            for(int j = i+1; j <= N; ++j)
                w[i][j] = w[j][i] = getDis(vs[i].x, vs[i].y, vs[j].x, vs[j].y);

        //枚举删边, 找出最大值
        double B = Prim(1), A, ans = -1;
        for(int i = 1; i < N; ++i)
            for(int j = i+1; j <= N; ++j){
                A = vs[i].p+vs[j].p;
                //这条边未在MST中使用, 尝试加边并删去生成环中的最长边, 已使用则直接变0
                if(mst[i][j]){
                    ans = max(ans, A/(B-w[i][j]));
                }else{
                    ans = max(ans, A/(B-maxD[i][j]));
                }
            }
        printf("%.2lf\n", ans);
    }

    return 0;
}

你可能感兴趣的:(算法总结,图论)