紫书 例题 11-3 UVa 1151 (有边集的最小生成树+二进制枚举子集)
标题指的边集是说这道题的套餐, 是由几条边构成的。
思路是先做一遍最小生成树排除边, 因为如果第一次做没有加入的边, 到后来新加入了很多权值为0的边,这些边肯定排在最前面,然后这条边的前面的那些边肯定都要再扫一遍, 也就是这条边无论如何都不会选。
那么后来就是二进制枚举套餐, 从头开始, 加入套餐中的边然后权值加上套餐的权值, 然后把之前筛选下来的边做kruskal就ok了。
注意要对数据范围敏感, 这里套餐最多也就8个所以可以二进制枚举子集。
#include<cstdio>
#include<vector>
#include<algorithm>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;
const int MAXN = 1123;
int x[MAXN], y[MAXN], f[MAXN];
int cost[MAXN], n, m, q, cnt, sum;
struct node
{
int u, v, w;
node(int u = 0, int v = 0, int w = 0) : u(u), v(v), w(w) {}
bool operator < (const node& rhs) const
{
return w < rhs.w;
}
};
vector<node> Edge, need;
vector<int> k[9];
int find(int x)
{
if(f[x] != x)
f[x] = find(f[x]);
return f[x];
}
int solve()
{
int ret = 0;
REP(i, 0, need.size())
{
int u = find(need[i].u), v = find(need[i].v);
if(u != v)
{
f[u] = v;
ret += need[i].w;
if(--cnt == 1) break;
}
}
return ret;
}
int main()
{
int T;
scanf("%d", &T);
while(T--)
{
Edge.clear(); need.clear();
scanf("%d%d", &n, &q);
REP(i, 0, q)
{
int t, x; k[i].clear();
scanf("%d%d", &t, &cost[i]);
while(t--)
{
scanf("%d", &x);
k[i].push_back(x);
}
}
REP(i, 1, n + 1) scanf("%d%d", &x[i], &y[i]), f[i] = i;
REP(i, 1, n + 1)
REP(j, i + 1, n + 1)
{
int t = (x[i]-x[j]) * (x[i]-x[j]) + (y[i]-y[j]) * (y[i]-y[j]);
Edge.push_back(node(i, j, t));
}
sort(Edge.begin(), Edge.end());
int ans = 0; cnt = n;
REP(i, 0, Edge.size())
{
int u = find(Edge[i].u), v = find(Edge[i].v);
if(u != v)
{
need.push_back(Edge[i]);
f[u] = v;
ans += Edge[i].w;
if(--cnt == 1) break; //注意是1
}
}
REP(num, 0, 1 << q)
{
sum = 0; cnt = n;
REP(i, 1, n + 1) f[i] = i;
REP(i, 0, q)
if(num & (1 << i))
{
sum += cost[i];
REP(j, 0, k[i].size())
{
int u = find(k[i][j]), v = find(k[i][0]);
if(u != v) f[u] = v, cnt--;
}
}
sum += solve();
ans = min(ans, sum);
}
printf("%d\n", ans);
if(T) puts("");
}
return 0;
}