[题解]逃离地球
题意简述
有一个星系,共有\(n*m\)个星球,排成\(n\)行\(m\)列。
初始星球之间没有道路。接下来给定\(P\)种魔法\(1\),\(Q\)种魔法\(2\):
- 魔法\(1\):第\(i\)种魔法用\(a_i,b_i,c_i\)描述。表示你可以任选星系的一行,在第\(a_i\)和第\(b_i\)个星球之间建立一条航道,消耗\(c_i\)的能量。
- 魔法\(2\):第\(i\)种魔法用\(x_i,y_i,z_i\)描述。表示你可以任选星系的一列,在第\(x_i\)和第\(y_i\)个星球之间建立一条航道,小号\(z_i\)的能量。
询问要想让所有星球互相连通,最少需要消耗多少太阳能。
输入格式:一行\(n,m,p,q\),接下来是\(p+q\)行,每行\(3\)个数用于描述魔法\(1,2\)的信息。
样例:
Sample #1
Input
2 2 1 2
1 2 1
2 1 1
2 1 1
Output
3
Sample #2
Input
2 3 4 1
2 3 5
3 2 7
1 2 6
1 1 8
2 1 5
Output
26
数据范围:\(1\le n,m,P,Q\le 10^5\),\(1≤c_i,z_i≤10^8\)
思路简述
根据贪心的思想,我们如果找到一种魔法消耗很小,那我们会想到对所有行/列都使用这种一次魔法。
于是想到使用最小生成树来解决。把行标和列标看作节点,魔法看作边,魔法消耗看作边权。那么就相当于对这样一个拥有\(2\)棵树的森林跑一次最小生成树。需要注意的是,不能先跑行再跑列,而是需要一同排序,一并处理。原因就是行数和列数是在变化的(比如我们已经对所有列都使用了魔法,连接了\(1,2\)行,那么我们可以理解为我们删掉了\(1\)行。因为只要能连接到第\(2\)行的一定也能连接到第\(1\)行。所以我们再对行使用魔法的时候就可以少考虑一行,即这一组魔法总消耗应该等于 单次消耗\(\times(\)当前行数\(-1)\))。
正确性证明:
设有两边权值分别是\(u,v\),而\(u>v\)。那么
-
显然\(u,v\)同为魔法\(1\)或同为魔法\(2\)时先用\(v\)最优。
-
如果\(u,v\)一个是魔法\(1\),一个是魔法\(2\),则先用\(u\)和先用\(v\)的花费如下:
- 先用\(u\):\(u*n+v*(m-1)=u*n+v*m-v\)
- 先用\(v\):\(v*m+u*(n-1)=u*n+v*m-u\)
故还是先用\(v\)更优。
代码使用Kruskal实现,需要用并查集。为了行标和列标不冲突所以行标节点统一\(+m\)存储,因此注意空间需要开\(2\)倍。
#include<bits/stdc++.h>
#define N 100010
#define M 100010
#define int long long
using namespace std;
struct edge{
int u,v,w;
}edges[2*M];//开两倍是因为行列都要存
bool cmp(edge a,edge b){return a.w<b.w;}
int n,m,p,q,fa[2*N],ans;
int find(int x){
if(fa[x]==x) return x;
else return fa[x]=find(fa[x]);
}
signed main(){
cin>>n>>m>>p>>q;
for(int i=1;i<=n+m;i++) fa[i]=i;
for(int i=1;i<=p;i++){//选行连列
cin>>edges[i].u>>edges[i].v>>edges[i].w;
}
for(int i=p+1;i<=p+q;i++){//选列连行
cin>>edges[i].u>>edges[i].v>>edges[i].w;
edges[i].u+=m,edges[i].v+=m;
}
sort(edges+1,edges+1+p+q,cmp);
int nn=n,mm=m;
for(int i=1,cnt=0;i<=p+q;i++){
int u=find(edges[i].u),v=find(edges[i].v);
if(u==v) continue;
cnt++;
fa[u]=v;
if(u<=m) ans+=nn*edges[i].w,mm--;//列节点
else ans+=mm*edges[i].w,nn--;//行节点
if(cnt==n+m-2) break;
}
cout<<ans;
return 0;
}