题目链接:
https://www.dotcpp.com/oj/problem1437.html
题目大意:
给你一个图,n为图中的结点数,结点编号从1到n。m则为路径数。然后没有结点本身有一个值,代表着这个地方建设港口需要的钱为这个值,而当这个自身的值等于-1时,则证明这个地方无法建设港口。再说回来,m条路径的中的每条路径都有三个值,a,b,c,分别代表前两个为连接的结点,第三个c则为连接两个结点所需要花费的钱,当然c如果是负数的话,则代表盈利。
看题目给的例子:
5 5
1 2 4
1 3 -1
2 3 3
2 4 5
4 5 10
-1 10 10 1 1
第一行 5 5就分别代表n和m,即n=5,m=5
所以有5个结点,分别是1,2,3,4,5
紧跟下来的就有m条路径,
1 2 4(代表着1与2中建设道路所需要花费4)
1 3 -1(代表着1与3这个道路建设是收费的,反而盈利1)
2 3 3(同理,不再赘述)
2 4 5
4 5 10
最后的一行五个数,就是代表着n个点分别自己建设港口的费用,除了第一个,因为它是-1,则代表着第一个结点没有办法建设港口。
然后这样下来需要你找到一个建设港口和道路的方法,使得使用的钱最少,输出钱即可。
思路:
这题一拿到,如果除去港口的建设不看就完完全全的是卡鲁斯卡尔算法配合着并查集寻找最小生成树问题。
但是,这里多了一个港口建设需要考虑。其实如果加入港口的考虑的话就只是将港口转化成路径,然后再进行一次查找最小生成树即可了。
所以上面的思路汇总就是:
①不管港口,先进行查找没有港口的最小生成树,并且记录下来注意:单独只建设道路的话,可能会导致有结点没有考虑到,即会漏掉。如下图:
这种情况就会漏掉4号结点
②将港口作为路径加入进来,再进行一次最小生成树的查找。如果①中是查询到了全部的结点,那么就②与①中取最小值,否则的话就直接输出②找出来的值。
这道题的关键我认为就是将港口转化为路径:把那些建设的港口,转化为起点为0,终点为1到n中建设港口不为-1的地点,路径值就为港口值 代码如下:
for(int i = 1;i <= n;i++){//将港口转化为路径
if(port[i]!=-1){
m++;
r[m].a = 0;
r[m].b = i;
r[m].c = port[i];
}
}
//port[i]代表着i号结点建设港口的值
//r[m]即代表着道路
①中查询到了全部结点的时候,即我们用一个变量sum去记录卡鲁斯卡尔中的合并次数,如果合并次数恰好等于n-1,那么就遍历到了全部结点。代码如下:
if(sum == n-1){//证明找到了全部的结点
flag = 1;
}
然后用到了并查集:附上大佬讲解并查集的传送门:
并查集代码:传送门
//并查集
void init(int n){
for(int i = 0;i <= n;i++){//注意 这里因为要包含港口新设立的结点0,故从0开始
f[i] = i;
}
}
int getFather(int x){
if(x==f[x]) return f[x];
return f[x] = getFather(f[x]);
}
void merge(int x,int y){
int t1 = getFather(x);
int t2 = getFather(y);
if(t1!=t2){
if(high[t1]>high[t2]){
f[t2] = t1;
}else{
f[t1] = t2;
if(high[t1] == high[t2]){
high[t2]++;
}
}
}
}
//上面为并查集
卡鲁斯卡尔算法:
//排序的cmp函数
bool cmp(struct route x,struct route y){//按照路的权值升序排序
return x.c<y.c;
}
ll karuskal(int n,int m){
ll ans = 0;
memset(high,0,sizeof(high));
init(n);
sort(r+1,r+m+1,cmp);
for(int i = 1;i <= m;i++){
int t1 = getFather(r[i].a);
int t2 = getFather(r[i].b);
if(t1!=t2||r[i].c<0){//价值小于0的道路是盈利的,有多少条就建多少条
ans += r[i].c;//有可能是已经合并的里面再建负的桥,这样的话就不用再合并和sum++了
if(t1!=t2){
merge(r[i].a,r[i].b);
sum++;
}
}
}
return ans;
}
ac代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M = 200005;
const int N = 10005;
struct route{
int a;
int b;
int c;
}r[M];
int f[N];
int high[N];//用来优化并查集的数组
int port[N];
int sum;//用来记录总共合并次数
//并查集
void init(int n){
for(int i = 0;i <= n;i++){//注意 这里因为要包含港口新设立的结点0,故从0开始
f[i] = i;
}
}
int getFather(int x){
if(x==f[x]) return f[x];
return f[x] = getFather(f[x]);
}
void merge(int x,int y){
int t1 = getFather(x);
int t2 = getFather(y);
if(t1!=t2){
if(high[t1]>high[t2]){
f[t2] = t1;
}else{
f[t1] = t2;
if(high[t1] == high[t2]){
high[t2]++;
}
}
}
}
//上面为并查集
//排序的cmp函数
bool cmp(struct route x,struct route y){//按照路的权值升序排序
return x.c<y.c;
}
ll karuskal(int n,int m){
ll ans = 0;
memset(high,0,sizeof(high));
init(n);
sort(r+1,r+m+1,cmp);
for(int i = 1;i <= m;i++){
int t1 = getFather(r[i].a);
int t2 = getFather(r[i].b);
if(t1!=t2||r[i].c<0){//价值小于0的道路是盈利的,有多少条就建多少条
ans += r[i].c;//有可能是已经合并的里面再建负的桥,这样的话就不用再合并和sum++了
if(t1!=t2){
merge(r[i].a,r[i].b);
sum++;
}
}
}
return ans;
}
//关键:把那些建设的港口,转化为起点为0,终点为1到n中建设港口不为-1的地点,路径值就为港口值
int main(){
int n,m;
ll ans1,ans2;//ans1用来记录没有加港口所遍历的最小生成树的值
//ans2用来记住加入港口算出最小生成树的值
int flag = 0;//记录第一次寻找最小生成树是否有找到全部结点
cin>>n>>m;
for(int i = 1;i <= m;i++){//接收道路
cin>>r[i].a>>r[i].b>>r[i].c;
}
for(int j = 1;j <= n;j++){//接收港口
cin>>port[j];
}
sum = 0;
ans1 = karuskal(n,m);
if(sum == n-1){//证明找到了全部的结点
flag = 1;
}
for(int i = 1;i <= n;i++){//将港口转化为路径
if(port[i]!=-1){
m++;
r[m].a = 0;
r[m].b = i;
r[m].c = port[i];
}
}
ans2 = karuskal(n,m);
if(flag == 1){
cout<<min(ans1,ans2)<<endl;
}else{
cout<<ans2<<endl;
}
return 0;
}