前提知识:子集枚举
题意:平面上有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;
}