利用并查集求最小生成树(见我另一篇文章):
https://blog.csdn.net/weixin_39909619/article/details/88721685
问题描述
栋栋居住在一个繁华的C市中,然而,这个城市的道路大都年久失修。市长准备重新修一些路以方便市民,于是找到了栋栋,希望栋栋能帮助他。
C市中有n个比较重要的地点,市长希望这些地点重点被考虑。现在可以修一些道路来连接其中的一些地点,每条道路可以连接其中的两个地点。另外由于C市有一条河从中穿过,也可以在其中的一些地点建设码头,所有建了码头的地点可以通过河道连接。
栋栋拿到了允许建设的道路的信息,包括每条可以建设的道路的花费,以及哪些地点可以建设码头和建设码头的花费。
市长希望栋栋给出一个方案,使得任意两个地点能只通过新修的路或者河道互达,同时花费尽量小。
输入格式
输入的第一行包含两个整数n, m,分别表示C市中重要地点的个数和可以建设的道路条数。所有地点从1到n依次编号。
接下来m行,每行三个整数a, b, c,表示可以建设一条从地点a到地点b的道路,花费为c。若c为正,表示建设是花钱的,如果c为负,则表示建设了道路后还可以赚钱(比如建设收费道路)。
接下来一行,包含n个整数w_1, w_2, …, w_n。如果w_i为正数,则表示在地点i建设码头的花费,如果w_i为-1,则表示地点i无法建设码头。
输入保证至少存在一个方法使得任意两个地点能只通过新修的路或者河道互达。
输出格式
输出一行,包含一个整数,表示使得所有地点通过新修道路或者码头连接的最小花费。如果满足条件的情况下还能赚钱,那么你应该输出一个负数。
样例输入
5 5
1 2 4
1 3 -1
2 3 3
2 4 5
4 5 10
-1 10 10 1 1
样例输出
9
样例说明
建设第2、3、4条道路,在地点4、5建设码头,总的花费为9。
数据规模和约定
对于20%的数据,1<=n<=10,1<=m<=20,0<=c<=20,w_i<=20;
对于50%的数据,1<=n<=100,1<=m<=1000,-50<=c<=50,w_i<=50;
对于70%的数据,1<=n<=1000;
对于100%的数据,1 <= n <= 10000,1 <= m <= 100000,-1000<=c<=1000,-1<=w_i<=1000,w_i≠0。
思想:这道题显然是一个求最小生成树问题,但是有坑。。。总的思路是引入新的节点0,让所有能建造码头的地点与0相连,边的权重即为修码头的费用,然后求最小生成树。
但我们要注意到两个问题:
①对于挣钱的路(即权值为负的边),在利用Kruskal构造最小生成树过程中,无论这个挣钱的路与之前已经放入的路是否构成环,我们都要修。。因为他肯定挣钱啊(会使最后的生成树权值和小),最最最大的坑来了。。虽然我们要把这样的边放入树中,但路的条数不能因此+1(除非他不构成环),因为最小生成树的最大条数为n-1,这里是要注意的(代码中我会标注)
②也是个挺大的坑。。。如果我们直接求加了0节点后的最小生成树,结果可能不对,因为求出的结果如果带码头的话,有可能会只有一个码头。。。。但一个码头并没有用啊(至少两个码头啊或者没有码头),因为如果不修码头的话,n个重要地点若也能通过修路互通,但若引入0节点后,要想生成最小生成树就必须有通往0的路。这样就有可能出现这样的局面:只修了一个码头。这样分析完我们就也知道怎么解决了。
正解:
我们先对n个节点求最小生成树(如果没有最小生成树就把树的权值设为最大值)即t1
然后对n+1(加了0节点后的)个节点求最小生成树。即t2
那么最后的的结果为min(t1,t2)
分析为什么min(t1,t2)就是正确的:
我们分情况讨论:
①如果n个节点不连通,那么t1为最大值,那么n+1个节点的生成树不会出现只修一个码头的情况。此时结果正确
②如果n个节点连通,那么会得到最小生成树值t1,此时n+1个节点得到的最小生成树会有两种情况一种是只修了一个码头(即只有一个可以修码头的地点与0相连),但是这种情况的值一定为t1 + 某个码头的建设费用,因此一定小于t1,因此这种情况也正确。第二种情况是,修了大于两个码头,那么这样就min(t1,t2),谁小就谁小,属于正常情况。。。。。累死我了。。。
代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
#define min(x,y) (x < y ? x : y)
using namespace std;
struct edge{ //边的结构体
int s;
int e;
int v;
edge(int ss,int ee,int vv){
s = ss;
e = ee;
v = vv;
}
};
vector<edge> vc;//存放所有边
int pre[10010]; //记录前节点
bool cmp(const edge&a,const edge&b){
return a.v < b.v;
}
int Find(int a){ //找根节点
int t = a;
while(pre[t] != t){
t = pre[t];
}
while(pre[a] != t){ //路径压缩
int tt = a;
a = pre[a];
pre[tt] = t;
}
return t;
}
int merg(int a,int b,int val){ //引入新边
int ta = Find(a);
int tb = Find(b);
if(ta != tb){
pre[ta] = tb;
return 1;
}else{
if(val <= 0){ //这就是最大的坑,必须当这条边为负,且加入后构成环时返回2(意味着边数不加1)
}
}
int kruskal(vector<edge> &vc,int n){ //生成最小生成树,返回值为最小生成树值
sort(vc.begin(),vc.end(),cmp);
int te = 0; //边数
int tsum = 0; //权值和
for(int i = 0;i < vc.size();i++){
int tt;
if(tt = merg(vc[i].s,vc[i].e,vc[i].v)){
if(tt != 2){ ///最大的坑,不为2时边数才++
te++;
}
tsum += vc[i].v;
}
}
if(te == n - 1){ //构成生成树了
return tsum;
}
return INT_MAX; //不连通
}
int main(){
int n;
int m;
cin>>n>>m;
for(int i = 0;i < m;i++){
int a,b,c;
cin>>a>>b>>c;
vc.push_back(edge(a,b,c));
}
for(int i = 1;i <= n;i++){
pre[i] = i;
}
int t1 = kruskal(vc,n); //求n个节点的最小生成树
for(int i = 1;i <= n;i++){
int t;
cin>>t;
if(t != -1){
vc.push_back(edge(0,i,t)); //vc中加入可以修建码头的点到0点的边
}
}
for(int i = 0;i <= n;i++){
pre[i] = i;
}
int t2 = kruskal(vc,n + 1); //求n+1个节点的最小生成树
cout<<min(t1,t2);
return 0;
}