A.公路修建
Description
现有 \(n\) 个互相不联通的城市,要求进行若干轮连接,每一轮中每个城市都会找与自己相距最近的城市连接。如果一轮当中修建的某些城市修建的路连成了一个环,就将环中最短的边删去。每一轮后联通的城市将构成城市联盟,下一轮中城市联盟作为一个城市与别的城市连接。直到所有城市联通为止。
Solution
容易发现,因为每个城市都会去找离自己最近的城市,所以如果构成了环,那么环上的每条边的长度都是相同的。因为已经联通的城市无法再互相连接,所以最终一定会形成一棵树。那么求最小生成树即可。因为空间问题,这题最好用 \(Prim\) 算法。
Code
#include<bits/stdc++.h>
#define N 5005
#define ll long long
using namespace std;
int n,m,x[N],y[N],vis[N];
double ans,g[N];
double dis(int i,int j){return sqrt((ll)(x[i]-x[j])*(x[i]-x[j])+(ll)(y[i]-y[j])*(y[i]-y[j]));
}
void prim(){vis[1]=1;for(int i=2;i<=n;i++)g[i]=dis(1,i);for(int nw=1;nw<n;nw++){double dist=0x3f3f3f3f;int id=0;for(int i=1;i<=n;i++){if(vis[i])continue;if(g[i]<dist){dist=g[i];id=i;}}for(int i=1;i<=n;i++)if(id!=i&&!vis[i])g[i]=min(g[i],dis(id,i));vis[id]=1;ans+=dist;}
}
signed main(){cin>>n;for(int i=1;i<=n;i++)cin>>x[i]>>y[i];prim();printf("%.2lf",ans);return 0;
}
B.兽径管理
Description
有 \(n\) 个节点,并加入 \(w\) 条边,让你在每次加边时,输出当前最小生成树的长度,如果图不连通则输出 -1。
Solution
每次加边后跑 \(Kruskal\) 即可。
Code
#include<bits/stdc++.h>
#define N 205
using namespace std;
int n,w,f[N];
struct edge{int x,y,dis;bool operator<(const edge x)const{return dis<x.dis;}
}e[6005];
int findf(int x){if(x==f[x])return x;return f[x]=findf(f[x]);
}
signed main(){cin>>n>>w;for(int nw=1;nw<=w;nw++){cin>>e[nw].x>>e[nw].y>>e[nw].dis;if(nw<n-1){cout<<"-1\n";continue;}int ans=0;for(int i=1;i<=n;i++)f[i]=i;int m=0;sort(e+1,e+1+nw);for(int i=1;i<=nw;i++){int x=e[i].x,y=e[i].y,dis=e[i].dis;int fx=findf(x),fy=findf(y);if(fx==fy)continue;f[fx]=fy;++m;ans+=dis;if(m==n-1)break;}if(m<n-1)ans=-1;cout<<ans<<"\n";}return 0;
}
C.公路修建问题
Description
有 \(n\) 个点,\(m\) 条边,每条边有两种权值,1 号和 2 号,保证 1 号边的权值大于等于 2 号边。现在让你在求出一棵最小生成树使得上面至少有 \(k\) 个 1 号权值。
Solution
按照 \(Kruskal\) 的思路,先将所有边按照 1 号边的长度排序,在建完 \(k\) 条 1 号边后,将剩下的边按照 2 号权值排序,再建剩下的边。
Code
#include<bits/stdc++.h>
#define N 10005
using namespace std;
int n,m,k,ans1,f[N],cnt,nw;
vector<pair<int,int> >ans2;
struct edge{int x,y,d1,d2,id;
}e[20005];
bool cmp(edge a,edge b){if(a.d1!=b.d1)return a.d1<b.d1;return a.d2>b.d2;}
bool tmp(edge a,edge b){return a.d2<b.d2;}
int findf(int x){if(x==f[x])return f[x];return f[x]=findf(f[x]);
}
signed main(){cin>>n>>k>>m;--m;for(int i=1;i<=n;i++)f[i]=i;for(int i=1;i<=m;i++){cin>>e[i].x>>e[i].y>>e[i].d1>>e[i].d2;e[i].id=i;}sort(e+1,e+1+m,cmp);for(int i=1;i<=m;i++){int x=e[i].x,y=e[i].y,dis=e[i].d1;int fx=findf(x),fy=findf(y);if(fx==fy)continue;f[fx]=fy;ans1=dis;++cnt;ans2.push_back({e[i].id,1});if(cnt==k){nw=i+1;break;}}sort(e+nw,e+1+m,tmp);for(int i=nw;i<=m;i++){int x=e[i].x,y=e[i].y,dis=e[i].d2;int fx=findf(x),fy=findf(y);if(fx==fy)continue;f[fx]=fy;ans1=max(ans1,dis);++cnt;ans2.push_back({e[i].id,2});if(cnt==n-1)break;}cout<<ans1<<"\n";sort(ans2.begin(),ans2.end());for(int i=0;i<n-1;i++)cout<<ans2[i].first<<" "<<ans2[i].second<<"\n";return 0;
}
D.逐个击破
Description
有一棵节点数为 \(n\) 的树,断开每一条边都需要一定的代价。这 \(n\) 个节点上有 \(k\) 个是被敌人占领的,现在要求你用最小的代价将这些敌占的节点分开,使其互相不联通。
Solution
正难则反,我们把问题转化成在一个没有边的树上建边,使其成为一个森林,保证其中有 \(k\) 棵树,其中每棵树中有一个节点是敌占的,可以保证没有敌占的节点向联通。
选择用 \(Kruskal\) 建边,最终只需要 \(n-k\) 条边,在用并查集判环时,注意记录当前的这一个集合是否有敌占的节点,如果两个集合要相连就必须保证两个集合中,最多有一个是有被敌人占领的节点的。
Code
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,m,isx[N],f[N],ans;
struct edge{int x,y,dis;bool operator<(const edge x)const{return dis>x.dis;}
}e[N];
int findf(int x){if(x==f[x])return x;return f[x]=findf(f[x]);
}
signed main(){cin>>n>>m;for(int i=1;i<=n;i++)f[i]=i;for(int i=1;i<=m;i++){int x;cin>>x;isx[x]=1;}for(int i=1;i<n;i++){int x,y,z;cin>>x>>y>>z;e[i]={x,y,z};ans+=z;}sort(e+1,e+n);for(int i=1;i<n;i++){int x=e[i].x,y=e[i].y,dis=e[i].dis;int fx=findf(x),fy=findf(y);if(fx==fy)continue;if(isx[fx]&&isx[fy])continue;f[fx]=fy;isx[fy]|=isx[fx];ans-=dis;}cout<<ans;return 0;
}
E.I Would Walk 500 Miles G
Description
给你 \(n\) 个奶牛,要求将它们分成 \(k\) 组,组与组之间的成员有一个距离,要求使所有的距离中最小的那个最大化,求这个值。
Solution
还是正难则反,我们可以将问题转化为建 \(n-k\) 条边,互相联通的点为一个组,然后使相连的边的最大值最小,也就相当于求 \(Kruskal\) 中最后加的那条边的下一条边的长度。
Code
#include<bits/stdc++.h>
#define N 7505
#define ll long long
#define xn 2019201913
#define yn 2019201949
#define mod 2019201997
using namespace std;
int n,m,ans,ecnt,f[N];
struct edge{int x,y;ll dis;bool operator<(const edge x)const{return dis<x.dis;}
}e[N*N/2];
int findf(int x){if(x==f[x])return x;return f[x]=findf(f[x]);
}
signed main(){cin>>n>>m;for(int i=1;i<=n;i++)f[i]=i;for(int i=1;i<n;i++){for(int j=i+1;j<=n;j++){++ecnt;e[ecnt].x=i,e[ecnt].y=j;e[ecnt].dis=((ll)i*xn+(ll)j*yn)%mod;}}sort(e+1,e+1+ecnt);int cnt=0;for(int i=1;i<=ecnt;i++){if(cnt==n-m)break;int x=e[i].x,y=e[i].y;int fx=findf(x),fy=findf(y);if(fx==fy)continue;++cnt;f[fx]=fy;}for(int i=1;i<=ecnt;i++){if(findf(e[i].x)!=findf(e[i].y)){cout<<e[i].dis;return 0;}}return 0;
}
F.网格图
Description
给定一个 \(n\times m\) 的网格图,行从 \(1~n\) 编号,列从 \(1~m\) 编号,每个点可用它所在的行编号 \(r\) 与所在的列编号 \(c\) 表示为 \((r,c)\)。
点 \((i,j)\) 与 \((i,j+1)\) 间连有一条权值为 \(a_i\) 的边,其中 \(1\le i\le n,1\le j<m\)。
点 \((i,j)\) 与 \((i+1,j)\) 间连有一条权值为 \(b_j\) 的边,其中 \(1\le i<n,1\le j\le m\)。
请你求出这个网格图的最小生成树。
Solution
一条条地往里加肯定不现实,但我们发现每一排和每一列的长度都一样,用 \(Kruskal\) 的想法,我们把这个排一下序,每次只需要用这个权值乘上建的边的数量。这个数量改怎么确定?因为有判环的操作,所以肯定不是简单的乘 \(n-1\) 或乘 \(m-1\)。我们考虑什么时候会出现环:对于加横边的来说,当有不止一条横边已经建好时,且已有两条以上的竖边时,它直接建就会形成环,那么如果有 \(k\) 个竖边已经建好,就减去 \(k-1\) 条边即可;建竖边同理。同时判断是否超过 \(n\times m-1\) 条边即可。
Code
#include<bits/stdc++.h>
#define N 300005
#define int long long
using namespace std;
int n,m,ecnt,ans;
struct node{int id,x;bool operator<(const node a)const{return x<a.x;}
}a[N],b[N];
signed main(){cin>>n>>m;for(int i=1;i<=n;i++)cin>>a[i].x,a[i].id=i;for(int i=1;i<=m;i++)cin>>b[i].x,b[i].id=i;sort(a+1,a+1+n);sort(b+1,b+1+m);a[n+1].x=b[m+1].x=0x3f3f3f3f3f3f3f3f;int cnt=0;for(int i=0,j=0;(i<n||j<m)&&cnt<n*m-1;){if(a[i+1].x<b[j+1].x){++i;if(i>1){int num=m-j;if(!j)--num;if(cnt+num<=n*m-1)ans+=num*a[i].x,cnt+=num;else ans+=(n*m-1-cnt)*a[i].x,cnt=n*m-1;}else{if(cnt+m-1<=n*m-1)ans+=(m-1)*a[i].x,cnt+=m-1;else ans+=(n*m-1-cnt)*a[i].x,cnt=n*m-1; }}else{++j;if(j>1){int num=n-i;if(!i)--num;if(cnt+num<=n*m-1)ans+=num*b[j].x,cnt+=num;else ans+=(n*m-1-cnt)*b[j].x,cnt=n*m-1;}else{if(cnt+n-1<=n*m-1)ans+=(n-1)*b[j].x,cnt+=n-1;else ans+=(n*m-1-cnt)*b[j].x,cnt=n*m-1; }}}cout<<ans;return 0;
}
G.游戏
Description
有 \(n\) 个城市(编号为 \(0,\cdots,n−1\)),其中有些城市之间有航线。每个航线连接两个城市,并且是双向的。
梅玉问健佳,是否任意两个城市之间都可以坐飞机互达(直接或间接),健佳不想直接回答,而是要通过做游戏的方式来告诉她。梅玉可以问"城市 \(u\) 和 \(v\) 之间有直接航线吗?",健佳会立刻直接回答该问题。梅玉会询问每对城市恰好一次,因此总计会有 \(r = \frac{n (n−1)}{2}\) 个问题。如果由前 \(i\)(\(i<r\))个问题的答案可以推断出整个航空网是否连通,也就是说,是否任意一对城市之间都可以坐飞机互达(直接或间接),梅玉就获胜。否则意味着她需要知道全部 \(r\) 个回答,此时健佳获胜。
为了让游戏更好玩,他们俩同意,健佳可以随着游戏的进展而编造航空网,也就是根据梅玉此前的提问来决定此后如何作答。你的任务是,通过决定健佳如何回答,来帮助他赢得游戏。
Solution
题目要求的是在最后一个知道图是否联通,也就是在最后一个之前,图一定完全成为了两个联通块,然后我们倒着推可以发现,我们只需从后往前建边,跑最小生成树即可,说简单点就是后面回答“是”一定比前面回答“是”优。然后统计答案,能建的边就输出 1,不能建就输出 0。
Code
#include<bits/stdc++.h>
#define N 1150005
using namespace std;
int n,r,u,v,f[N],ans[N];
struct edge{int x,y,dis,id;bool operator<(const edge x){return dis>x.dis;}
}e[N];
int findf(int x){if(x==f[x])return x;return f[x]=findf(f[x]);
}
signed main(){cin>>n;r=n*(n-1)/2;for(int i=1;i<=r;i++){cin>>u>>v;e[i].x=u,e[i].y=v,e[i].dis=i;}sort(e+1,e+1+r);for(int i=0;i<n;i++)f[i]=i;for(int i=1;i<=r;i++){int x=e[i].x,y=e[i].y,id=e[i].dis;int fx=findf(x),fy=findf(y);if(fx==fy)continue;f[fx]=fy;ans[id]=1;}for(int i=1;i<=r;i++)cout<<ans[i]<<"\n"; return 0;
}
H.Kuglarz
Description
魔术师的桌子上有 \(n\) 个杯子排成一行,编号为 \(1,2,…,n\),其中某些杯子底下藏有一个小球,你现在要准确地猜出哪些杯子下是有小球的。花费 \(c_{ij}\) 元,魔术师就会告诉你杯子 \(i,i+1,…,j\) 底下藏有球的总数的奇偶性,你至少需要花费多少元,才能保证猜出哪些杯子底下藏着球?
Solution
首先我们可能会想到区间 DP,但显然无法判断当前的状态是否满足条件。换个思路,我们这么想:因为要知道每一个下面是否有,就需要通过区间的加减合并操作,来处理出每一个杯子下的奇偶性。那我们看以下两种操作:
- 由 \(\left[i,k \right]\) 和 \(\left[k+1,j \right]\) 得到 \(\left[i,j \right]\)
- 由 \(\left[i,j \right]\) 和 \(\left[i,k \right]\) 得到 \(\left[k+1,j \right]\)
那么我们发现,为了让其简洁一些,我们可以将其改为一个左闭右开的区间,即:
- 由 \(\left[i,k \right)\) 和 \(\left[k+1,j \right)\) 得到 \(\left[i,j \right)\)
- 由 \(\left[i,j \right)\) 和 \(\left[i,k \right)\) 得到 \(\left[k+1,j \right)\)
这样首位相接的形式显然可以简洁一些。
那么为了使每一个杯子都确定,我们需要保证每一个 \(\left[i,i+1\right)\) 都可以被转移出来,也就是保证每个 \(i\) 和 \(i+1\) 都是联通的。
权值和最小,全部互相连通,直接用最小生成树即可,最后需要建 \(n\) 条边。
Code
#include<bits/stdc++.h>
#define int long long
#define N 2005
using namespace std;
int n,x,ecnt,f[N],ans;
struct edge{int x,y,dis;bool operator<(const edge x)const{return dis<x.dis;}
}e[N*N/2];
int findf(int x){if(x==f[x])return x;return f[x]=findf(f[x]);
}
signed main(){cin>>n;for(int i=1;i<=n;i++)for(int j=i;j<=n;j++){cin>>x;e[++ecnt]={i,j+1,x};}for(int i=1;i<=n+1;i++)f[i]=i;sort(e+1,e+1+ecnt);int cnt=0;for(int i=1;i<=ecnt;i++){int x=e[i].x,y=e[i].y,dis=e[i].dis;int fx=findf(x),fy=findf(y);if(fx==fy)continue;f[fx]=fy;ans+=dis;++cnt;if(cnt==n)break;}cout<<ans;return 0;
}
J.Tree I
Description
给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 \(need\) 条白色边的生成树,题目保证有解。
Solution
会发现一个事情,如果我们直接按照长度排序,然后跑 \(Kruskal\) 不一定能满足 \(need\) 条白边。那么如果我们稍微改一下排序的顺序,保证排序后白边间的相对位置与黑边间的相对位置不变即可。怎么改变这个顺序?我们可以给每条白边的长度都加上一个值,然后再排序,那么一定有一个值可以达到我们想要的效果,也就是在给所有白边加上这个值后进行排序,可以使此使用 \(Kruskal\) 跑出来的最小生成树恰好有 \(need\) 条白边。至于这个值怎么找,用二分就行了。每次判断当前排序情况下的白边数量与 \(need\) 的关系,如果大于等于 \(need\),就说明白边多了,所以将这个值增加;否则减少。
但我们会发现这么一个问题,如果某个值为 \(x\),它会导致白边的数量为 \(need+1\),但是下一次二分时又成了 \(need-1\),即无法找到恰好等于 \(need\) 的情况。因为题目中说保证有解,所以数量为当白边的数量大于 \(need\) 时,有可能是改变后的白边长度与黑边长度相等,所以只需把它当作有 \(need\) 条白边就行了。
Code
#include<bits/stdc++.h>
#define N 100005
using namespace std;
int n,m,need,l,r,ans,f[N];
struct edge{int x,y,dis,col;bool operator<(const edge &x)const{if(dis!=x.dis)return dis<x.dis;return col<x.col;}
}e[N];
int findf(int x){if(x==f[x])return x;return f[x]=findf(f[x]);
}
int cnt,sum,num;
void kruskal(int mid){cnt=sum=num=0;for(int i=1;i<=m;i++){if(cnt==n-1)break;int fx=findf(e[i].x),fy=findf(e[i].y);if(fx==fy)continue;f[fx]=fy;++cnt;sum+=e[i].dis;if(!e[i].col){++num;if(num<=need)sum-=mid;}}
}
signed main(){cin>>n>>m>>need;for(int i=1;i<=m;i++)cin>>e[i].x>>e[i].y>>e[i].dis>>e[i].col;l=-101,r=101;while(l<=r){int mid=(l+r)>>1;for(int i=0;i<n;i++)f[i]=i;for(int i=1;i<=m;i++)if(!e[i].col)e[i].dis+=mid;sort(e+1,e+1+m);kruskal(mid);if(num>=need){ans=sum;l=mid+1;}else r=mid-1;for(int i=1;i<=m;i++)if(!e[i].col)e[i].dis-=mid;}cout<<ans;return 0;
}