uva1151 Buy or Build (最小生成树+子集枚举)

前提知识:子集枚举
题意:平面上有n个点(1<=N<=1000),你的任务是让所有n个点连通,为此,你可以新建一些边,费用等于两个端点的欧几里得距离的平方。
另外还有q(0<=q<=8)个套餐,可以购买,如果你购买了第i个套餐,该套餐中的所有结点将变得相互连通,第i个套餐的花费为ci
求最小花费。

思路:最简单直接的做法就是先求出一个最小生成树,然后依次枚举套餐的不同选法(有2^q种),然后将套餐中的边之间的权值全部设为0,再与原先的边一起求最小生成树,一一比较求一个最小值。

但是!!!这样子会T

因此我们需要缩小数据的规模。于是我们想到了kruskal算法的特点,它是先将边排序,然后再一一合并到集合上。然后对于最优解来说,要么是原来最先的求出的最小生成树,要么就是通过购买套餐的最小生成树。但是无论哪一种方式,有一些边总是不会选用的!

那是哪些边呢?
就是一开始不在最小生成树中的边是不会选用的,除非套餐中有这些边。那么我们就可以将最小生成树上的边用一个数组存储起来。这样一来每次使用kruskal算法的数据规模就会小很多,规模从原来任意两个点都有边(总边数是(n-1)*n/2)变为了只有n-1条边。这样一来,就不会超时了。

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int maxn = 1005;
struct Point {//记录点
    int x, y;
} point[maxn];
struct node {
    int u, v, w;
} e[maxn * maxn / 2], ee[maxn * maxn / 2];
int  f[maxn], cost[maxn], c[maxn], net[9][maxn];
int n, m, t, num, cnt;
bool cmp(node a, node b) {
    return a.w < b.w;
}
int  find(int x) {
    return f[x] == x ? x : f[x] = find(f[x]);
}
int dist(Point a, Point b) {
    return  (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);//求两点之间的距离
}
int kruskal() {//求加入套餐后的最小生成树
    int ans = 0;
    for(int i = 1; i <= cnt; i++) {
        int t1 = find(ee[i].u);//ee数组是最小生成树上的边
        int t2 = find(ee[i].v);
        if(t1 != t2) {
            ans += ee[i].w;
            f[t1] = t2;
        }
    }
    return ans;
}
int main() {
    scanf("%d", &t);
    while(t--) {
        scanf("%d%d", &n, &m);
        for(int i = 0; i < m; i++) {
            scanf("%d%d", &c[i], &cost[i]);
            for(int j = 1; j <= c[i]; j++) scanf("%d", &net[i][j]);
        }
        for(int i = 1; i <= n; i++) scanf("%d%d", &point[i].x, &point[i].y);
        num = 0;
        for(int i = 1; i <= n; i++) {
            for(int j = i + 1; j <= n; j++) {
                e[++num].u = i;//e数组是任意两点的边
                e[num].v = j;
                e[num].w = dist(point[i], point[j]);
            }
        }
        sort(e + 1, e + 1 + num, cmp);
        int sum = 0;//求一个最小值
        cnt = 1;//用来作为ee数组的下标
        for(int i = 1; i <= n; i++) f[i] = i;
        for(int i = 1; i <= num; i++) {
            int t1 = find(e[i].u);
            int t2 = find(e[i].v);
            if(t1 != t2) {
                ee[++cnt] = e[i];//将是最小生成树上的边用ee数组存起来
                f[t1] = t2;
                sum += e[i].w;
            }
        }
        for(int s = 0; s < (1 << m); s++) {//子集枚举
                int res = 0;
            for(int i = 1; i <= n; i++) f[i] = i;
            for(int i = 0; i < m; i++) {
                if(s & (1 << i)) {
                    res += cost[i];//加上套餐价格
                    for(int j = 2; j <= c[i]; j++) {//注意要从2开始,因为总要有一个根
                        f[find(net[i][j - 1])] = find(net[i][j]);//合并套餐中的边
                    }
                }
            }
            res += kruskal();
            sum = min(sum, res);
        }
        printf("%d\n", sum);
        if(t) printf("\n");
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/yiqzq/article/details/80247845