題意簡述
有一個星系,共有\(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\)的能量。
詢問要想讓所有星球互相連通,最少需要消耗多少太陽能。
思路簡述
根據貪心的思想,我們如果找到一種魔法消耗很小,那我們會想到對所有行/列都使用這種一次魔法。
於是想到使用最小生成樹來解決。把行標和列標看作節點,魔法看作邊,魔法消耗看作邊權。那麼就相當於對這樣一個擁有\(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;
}