解题报告
题意理解
给定一张N个点,M个边的无向图,求出无向图的一颗最小生成树,但是我们要求一号节点的入度不可以超过给定的整数S
也就是一个最小生成树,要求它的一号节点,最多只能和S个节点相连.
思路确定
题意就已经告诉我们,我们的必备算法必然是最小生成树.但是具体的算法流程,我们还得思考一下.
首先,我们要知道两个性质.
-
一个最小生成树,它的任意一棵子树都是最小生成树.
-
也就是一个最小生成树,实际上是由很多棵最小生成树构成的.
根据上面所言的性质,我们可以这么考虑这道题目.
首先我们不妨不去考虑这个有特殊限制的一号节点,那么我们忽略掉这个一号节点后.
原图变成了T个连通块.
那么我们对于每一个连通块,都可以在这个连通块内部求出它的最小生成树.
我们接下来再来考虑,如何让这些连通块与我们的一号节点相连,构成我们题目的最小生成树.
首先我们很容易求出一个相对较小的生成树.切记不是最小生成树
对于每一个连通块而言,显然要在每一个连通块之中选出一个节点与我们的一号节点的权值最小.即(1,p)的权值要尽量小.
那么此时,我们发现我们成功将节点们连接在一起了,构成了一个最小生成树,那么现在的问题就是,如何优化我们的生成树,将其变成我们的最小生成树.
我们现在知道和一号节点连接的点,一共有T个,但是题目中要求不多于S个节点就好了.
分类讨论一下
那么我们必然就是无解情况.
那么此时我们的生成树,就是我们的最小生成树.
我们发现,对于一个节点而言,它不一定要属于自己原本的连通块,它可以和节点11相连
我们可以考虑无向图从节点11出发的每条边(1,x,z),其中边权为zz,那么假如说(1,x)这条边,它不在当前的生成树内.
那么我们就可以找到从当前生成树总(1,x)这条路径上的权值最大边(u,v,w)将它删除.
如果说我添加(1,x,z)这条边,我就可以删除(u,v,w)这条边.
然后我们这颗生成树的权值就可以减少w−z
综上所述,我们每一次从每一个连通块里面找,找到让w−z的值最大的点,然后添加(1,x,z),删除(u,v,w)
直到
也就是不可以加边,或者加边已经木有意义了.
具体参见 汪汀2004年的国家集训队论文,讲的很详细。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include <bits/stdc++.h> using namespace std; const int N=1010; #define quick() ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)//读入优化 #define mem(a,b) memset(a,b,sizeof(a))//初始化 #define init() mem(g,0),mem(vis,false),root=cnt=tot2=tot=0,q.clear(),q2.clear()//初始化大军 #define add(a,b,c) g[a][b]=g[b][a]=c//无向图加边 #define mk(a,b) make_pair(a,b)//简便 int tot2,tot,n,m,fa[N],cnt,c,s,root,g[31][31],dis[31][31]; //tot2为边的数量 //tot为点的数量 //cnt为编号的数量 map<string,int> q;//映射关系 map<pair<int,int>,bool> q2;//统计这条边是否出现在最小生成树中 int vis[N];//标记连通块的编号 string a,b; int find(int x) { return x==fa[x]?fa[x]:fa[x]=find(fa[x]);//并查集找红太阳 } struct edge1 { int x,y,w; } g2[N]; int cmp (edge1 a,edge1 b) { return a.w<b.w;//最小边排序 } struct node { int u,v,d;//(u,v)节点权值为d inline void inits() { u=v=0; d=-1; } } dp[N]; struct edge2 { inline void add_edge(int a,int b,int c)//加入一条边 { g2[++tot2].x=a;//起点 g2[tot2].y=b;//终点 g2[tot2].w=c;//权值 } inline int kruskal() { sort(g2+1,g2+1+tot2,cmp);//排序,找最小 int ans=0;//我们的目标连通块的连通块编号 for(int i=1; i<=tot2; i++) { int x=find(g2[i].x),y=find(g2[i].y);//求出所在连通块 if (x==1 || y==1 || x==y)//不是目标连通块,或者已经在一起了 continue; fa[x]=y;//合并 ans+=g2[i].w;//统计 q2[mk(g2[i].x,g2[i].y)]=true;//这条边出现过 q2[mk(g2[i].y,g2[i].x)]=true; } return ans; } } g3; void read() { quick(); init(); cin>>n; root=q["Park"]=tot=1;//Park节点就是我们的一号节点 for(int i=1; i<=n; i++) { cin>>a>>b>>c; if (!q[a])//名字读入 q[a]=(++tot);//新编号 if (!q[b]) q[b]=(++tot);//新编号 g3.add_edge(q[a],q[b],c);//加边 add(q[a],q[b],c);//加边 fa[i]=i;//初始化每个点的父亲节点 } cin>>s; } void dfs(int x) { for(int j=2; j<=tot; j++) if (g[x][j] && !vis[j])//有边,但是木有被标记 { vis[j]=cnt; dfs(j); } } void pd()//连通块划分 { for(int i=2; i<=tot; i++) if (!vis[i]) { cnt++;//又来一个 vis[i]=cnt; dfs(i); } } void dfs(int now,int last)//计算(1,x)路径上最大边 { for(int i=2; i<=tot; i++) { if(i==last || !q2[mk(now,i)])//点重叠,或者没有这条边 continue; if(dp[i].d==-1)//没有来过 { if(dp[now].d>g[now][i]) dp[i]=dp[now]; else { dp[i].u=now; dp[i].v=i; dp[i].d=g[now][i]; } } dfs(i,now); } } void work() { pd(); int ans=g3.kruskal();//统计每一个连通块的值 for(int i=1; i<=cnt; i++) { int now=0x3f3f3f3f,st=0;//初始值为INF for(int j=2; j<=tot; j++) if (vis[j]==i)//属于这个连通块 if (now>g[1][j] && g[1][j]!=0) { now=g[1][j];//找到与1相连最小的边 st=j; } ans+=now;//将每一个连通块与1相连 q2[mk(1,st)]=q2[mk(st,1)]=true; } int t=cnt; while(s>t) { s--; int now=0,idx=0; for(int i=1; i<=1100; i++) dp[i].inits(); dfs(1,-1);//求每一个点到1的途中最大边是谁? for(int j=2; j<=tot; j++) { if(now<dp[j].d-g[1][j] && g[1][j]) { now=dp[j].d-g[1][j];//找到最大权值边 idx=j; } } if (now<=0)//已经不会多优秀了 break; ans=ans-now; q2[mk(dp[idx].u,dp[idx].v)]=false;//删除边 q2[mk(1,idx)]=q2[mk(idx,1)]=true;//添加边 } cout<<"Total miles driven: "<<ans; } int main() { read(); work(); return 0; }