第一道A掉的紫题。。(虽然比较水)
修修改改写了将近五六个小时,还是在紫书的帮助下写的。
不过正解想出来后也就很快A了,(除了忘了重置sum的值WA了一发)
题解:
这题其实相当于一个暴力,枚举每个套餐是否会被选到,q很小,最大为8。2^8也仅有256种情况
枚举完套餐,把这些套餐中包含的点加一个权值为0的边然后求最小生成树就行。
粗略分析一下时间复杂度:
先建立每个点之间的边,则有n*(n+1)/2条边,也就是n²量级条边
排序是n²logn²≈n²logn
然后枚举套餐过程是(2^q)
Kruskal算法的复杂度基本上就是排序的复杂度(O(mlogm)时间主要花在排序上)也就是n²logn
总的时间复杂度为(2^q)·n²logn(与紫书貌似有些出入) 基本上达到了10^8级别,这是不能接受的
我们需要先做一遍最小生成树,把无用的边去掉
剩下的话只有n-1条边了,枚举量大大减小.
优化后时间复杂度为(2^q)·nlogn+nlogn 大概只有10^5~10^6左右
正如lrj在原文中说的:因为Kruskal在连通分量包含n个点时会终止,所以对于随机数据,即使用原始的“暴力算法”,也能很快出解
思路是:
①先建立所有点之间的边,然后求其最小生成树,把无用的边去掉(这步很重要)。
②然后我们就可以开始枚举套餐了。
我想的是用递归来枚举,当时也踩了不少坑(如:当时想每一层递归进行加边/恢复边处理,实则使问题复杂化了)
最好是先开一个数组记录一下每个套餐选不选
③递归枚举完后调用Kruskal函数,挨个判断套餐选不选,选的话直接加一个权值为0的边(也不需要开一个二维数组,使问题更复杂了...),然后求最小生成树,并记录一下花费就好了
各个步骤的代码如下:
①:
//先求一次原图的最小生成树得到n-1条边
int k=0;
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
int d=(y[j]-y[i])*(y[j]-y[i])+(x[j]-x[i])*(x[j]-x[i]);
t[k++]={i,j,d};
}
}
int idx=0;
sort(t,t+k,com);
for(int i=1;i<=n;i++) p[i]=i;
int cnt=0;
for(int i=0;i<k;i++)
{
int a=t[i].a,b=t[i].b;
a=find(a),b=find(b);
if(a!=b)
{
p[a]=b;
e[idx++]=t[i];//把无用的边扔掉,有用的存进e里
if(++cnt==n-1) break;
}
}
②
int solve(int step)
{
if(step==q+1)
{
int t=kruskal();
// printf(":%d\n",t);
ans=min(ans,t);
}
else
{
vis[step]=0;
solve(step+1);
vis[step]=1;
solve(step+1);
}
return 0;
}
③(这一步注释就先不删了)
int kruskal()
{
// printf("\n选择方式:");
// for(int i=1;i<=q;i++)
// {
// printf("%d ",vis[i]);
// }
// printf("\n");
for(int i=1;i<=n;i++)p[i]=i;//初始化并查集
//复制边
for(int i=0;i<n-1;i++) kru[i]=e[i];//0~n-2共n-1条边
int idx=n-1;
int sum=0;
// printf("\n加加边前:\n");
// for(int i=0;i<idx;i++)
// {
// printf("%d %d %d\n",kru[i].a,kru[i].b,kru[i].c);
//
// }
// printf("加的边有:\n");
for(int i=1;i<=q;i++)
{
if(vis[i])
{
sum+=meal[i].price;
for(int j=2;j<=meal[i].m;j++)
{
kru[idx++]={meal[i].city[1],meal[i].city[j],0};
// printf("%d %d %d\n",kru[idx-1].a,kru[idx-1].b,kru[idx-1].c);
}
}
}
sort(kru,kru+idx,com);
// printf("\n加完边后:\n");
// for(int i=0;i<idx;i++)
// {
// printf("%d %d %d\n",kru[i].a,kru[i].b,kru[i].c*kru[idx-1].c);
//
// }
for(int i=0;i<idx;i++)
{
int a=kru[i].a,b=kru[i].b;
a=find(a),b=find(b);
if(a!=b)
{
// printf("%d %d :%d\n",kru[i].a,kru[i].b,kru[i].c);
p[a]=b;
sum+=kru[i].c;
}
}
return sum;
}
至此,主要代码部分完成
完整代码献上
#include<stdio.h>
#include<iostream>
#include<cstdlib>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=1010,M=1e6+10;
int n,q;
struct meal
{
int m;
int price;
int city[1000];
}meal[10];
int vis[10];
struct edge
{
int a,b,c;
}e[M],t[M];
bool com(edge a,edge b)
{
return a.c<b.c;
}
int p[N];
int find(int x)
{
if(p[x]!=x)
{
p[x]=find(p[x]);
}
return p[x];
}
int x[N],y[N];
edge kru[M];
int kruskal()
{
// printf("\n选择方式:");
// for(int i=1;i<=q;i++)
// {
// printf("%d ",vis[i]);
// }
// printf("\n");
for(int i=1;i<=n;i++)p[i]=i;//初始化并查集
//复制边
for(int i=0;i<n-1;i++) kru[i]=e[i];//0~n-2共n-1条边
int idx=n-1;
int sum=0;
// printf("\n加加边前:\n");
// for(int i=0;i<idx;i++)
// {
// printf("%d %d %d\n",kru[i].a,kru[i].b,kru[i].c);
//
// }
// printf("加的边有:\n");
for(int i=1;i<=q;i++)
{
if(vis[i])
{
sum+=meal[i].price;
for(int j=2;j<=meal[i].m;j++)
{
kru[idx++]={meal[i].city[1],meal[i].city[j],0};
// printf("%d %d %d\n",kru[idx-1].a,kru[idx-1].b,kru[idx-1].c);
}
}
}
sort(kru,kru+idx,com);
// printf("\n加完边后:\n");
// for(int i=0;i<idx;i++)
// {
// printf("%d %d %d\n",kru[i].a,kru[i].b,kru[i].c*kru[idx-1].c);
//
// }
for(int i=0;i<idx;i++)
{
int a=kru[i].a,b=kru[i].b;
a=find(a),b=find(b);
if(a!=b)
{
// printf("%d %d :%d\n",kru[i].a,kru[i].b,kru[i].c);
p[a]=b;
sum+=kru[i].c;
}
}
return sum;
}
int ans=0x3f3f3f3f;
int solve(int step)
{
if(step==q+1)
{
int t=kruskal();
// printf(":%d\n",t);
ans=min(ans,t);
}
else
{
vis[step]=0;
solve(step+1);
vis[step]=1;
solve(step+1);
}
return 0;
}
int main()
{
// freopen("uva1151.txt","r",stdin);
int T;
scanf("%d",&T);
int num=0;
while(T--)
{
scanf("%d%d",&n,&q);
for(int i=1;i<=q;i++)
{
int m;
scanf("%d",&m);
int price;
scanf("%d",&price);
meal[i].m=m,meal[i].price=price;
for(int j=1;j<=m;j++)
{
scanf("%d",&meal[i].city[j]);
}
}
for(int i=1;i<=n;i++)
{
scanf("%d%d",&x[i],&y[i]);
}
//建立边数 0~k-1 k=n*(n+1)/2
int k=0;
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
int d=(y[j]-y[i])*(y[j]-y[i])+(x[j]-x[i])*(x[j]-x[i]);
t[k++]={i,j,d};
//g[i][j]=abs(y[j]-y[i])+abs(x[j]-x[i]);
}
}
//先求一次原图的最小生成树得到n-1条边
int idx=0;
sort(t,t+k,com);
// for(int i=0;i<k;i++)
// {
// printf("%d %d %d\n",t[i].a,t[i].b,t[i].c);
// }
for(int i=1;i<=n;i++) p[i]=i;
int cnt=0;
for(int i=0;i<k;i++)
{
int a=t[i].a,b=t[i].b;
a=find(a),b=find(b);
if(a!=b)
{
p[a]=b;
e[idx++]=t[i];
//if(t[i].a==6&&t[i].b==7)printf("----%d---",t[i].c);
//if(a==6&&b==7)printf("-----%d---",t[i].c);
//cout<<t[i].c<<endl;
if(++cnt==n-1) break;
}
}
//2^p种情况
solve(1);
if(++num!=1) cout<<endl;
cout<<ans<<endl;
ans=0x3f3f3f3f;
}
return 0;
}