快要考研机试了,练练oj题目,这道题目,还是很有代表性的,分析的也比较透彻。
原题目:栋栋居住在一个繁华的C市中,市长准备重新修一些路以方便市民,于是找到了栋栋,希望栋栋能帮助他。
C市中有n个比较重要的地点,希望可以修一些道路来连接其中的一些地点,每条道路可以连接其中的两个地点。
另外由于C市有一条河从中穿过,也可以在其中的一些地点建设码头,所有建了码头的地点可以通过河道连接。
栋栋拿到了允许建设的道路的信息,包括每条可以建设的道路的花费,以及哪些地点可以建设码头和建设码头的花费。
市长希望栋栋给出一个方案,使得任意两个地点能只通过新修的路或者河道互达,同时花费尽量小。
Sample Input:
5 5 1 2 4 1 3 -1 2 3 3 2 4 5 4 5 10 -1 10 10 1 1
Input中就是两个地点之间,建一条路的价格,和建码头的价格。
分析题目:
要求所有地点之间都连通,即“给一个图”,找出“连通树”,而且花费最少,马上想到,很类似“最小生成树”,但是如果细分析起来,还是有些许不相同:
① 最小生成树一般边都是大于0;而在这,出现了权值为负的边(能赚钱的路),这样的边那我们必须要建,即便形成了回路也要建;在算法中,要注意这一点;
② 码头应该这样处理: 建立一个虚拟节点0点,任何一个地点建立码头,即相当于该点和0点相连接,权值就是修码头的费用;任何两个地点通过水路相连,即相当于都与0点相连通;(这样的好处就在于,建立码头也可以算作普通的权值边,我们可以把整个问题都当做找“最小生成树”来处理)
逻辑部分:
核心代码逻辑,一定就是我们的“最小生成树”,即 Prim算法 和 Kruscal算法,两种;
Prim找权值最小的边,当边的两端属于不同的树时,选中这条边;
Kruscal以一个小树为中心不断向外扩张,每次找到与该树相连的最小边;(即找到一个边,一段在树里,一段在树外,且权值小)
需要注意的是: 一、陆路当中,即便有可能成环的负权重值的路,也要建; 二、水路当中,如果最终,与0点相连的只有一个点,也就是说,只有一个点建立了码头,那么实际上是没有意义的,要排除这条边(排除这种情况)
代码具体实现(具体代码见最后):
① 首先我们想到,对于给定的输入(边及权值),我们如何读入?(或者,如何构造这样的图?)
本题的输入相比于图的全连接,边很少
糟糕的想法: 用矩阵表示 a[i][j]=x 相当于 点i与j之间的权值为x;(糟糕在会有大量的稀疏值,拖慢我们的运行效率)
正确的做法:直接用struct记录每一条边(node 包括 边的两端和权重值)
②找最小边?
糟糕的做法: 每一次要找一条新的边加入我的“生成树”中,都要把所有符合要求的边都比较一次,找到最小的!!!
(事实证明,如果每一次都去找最小的边,大大拖慢了我们的运行速度,会运行超时!!!)
正确的做法:在读完所有数据之后,马上对边(和码头)进行一次排序,按照权值大小;这样可以避免之后每一次耽误时间的找最小边。
使用 C++
#include<algorithm>
struct edge{
int u,v,val;
}e[1000000];
bool cmp(edge a,edge b){
return a.val<b.val;
}
sort(e+1,e+M+1,cmp);
具体关于 sort函数:关于C++的sort函数
sort(first_pointer,first_pointer+n,cmp)
在使用sort函数时,强调几点:
(1) #include<algorithm> (2) 该函数可以给数组,或者链表list、向量排序。(3) 注意起始和结束为止,左闭右开!!! (4)要自己写一下 cmp函数,告诉sort函数按照什么规则排序(递增或者递减);
扯远了,说回来,代码实现部分
一定要用sort函数,而且一定不可以自己来写(我之前有一版代码是自己写的排序函数,插入排序之类的),运行非常慢!!!
③ 两种“最小生成树”的代码精华:
Kruscal: 以一颗小树为主体,不断找与其相连的最小边(即搜索已经排序好的边的数组,找到第一个符合 一个在树内,一个在树外,就ok)
int find_min_path(){
int min_money=100000;
int x_tmp,y_tmp,t_tmp;
int kind;// 1 表示陆地 2表示水路
for(int i=1;i<=M;i++){
if(set_pos[e[i].u]+set_pos[e[i].v]==1) //判断是否一个在树内,一个在树外
if(e[i].val < min_money){
min_money=e[i].val;
kind = 1;
x_tmp = e[i].u;
y_tmp = e[i].v;
t_tmp = i;
break;
}
}
for(int i=1;i<=N_pier;i++)
if(set_pos[0]+set_pos[p[i].u]==1)
if(p[i].val < min_money){
min_money=p[i].val;
kind = 2;
x_tmp = 0;
y_tmp = p[i].u;
t_tmp = i;
break;
}
if(kind == 1){ //陆路
set_pos[x_tmp]=1;set_pos[y_tmp]=1;
change_land_road(t_tmp); //把选中的路权值设为0,这样最后还有权值为负的就分得清楚了
return min_money;
}
if(kind == 2){ //水路
set_pos[x_tmp]=1;set_pos[y_tmp]=1;
if(x_tmp == 0){ //设置的额外的信号量,记录多少点与0点相连,如果最后只有一个,那么就要删去
pier_num_0_array[pier_num_0_connect]=t_tmp;
pier_num_0_connect++;
}
return min_money;
}
}
Prim算法:找到所有边里权值最小的,要求该边两端分别属于不同的树中;难点就在于,如何判断那些点在一个树里(因为完全选择出来之前,很有可能是密密麻麻的森林,很容易混淆!)
我是看了网上有些大佬的代码:大佬的代码
还有很多版本,但基本上都是这个思路,大家可以自行百度:
这一种里的难点在于:
int find(int x)
{
if(x==fa[x]) return x; //说明自己就是这个树的根节点
return fa[x]=find(fa[x]); //要向上继续找根节点,并且重置自己的根节点
}
int kruscal(int n)
{
int fu,fv,w,sum=0;
for (int i=0;i<n;i++)
{
fu=find(edge[i].u);
fv=find(edge[i].v);
w=edge[i].w;
if (fu!=fv||w<0){
sum+=w;
fa[fu]=fv;
}
}
return sum;
}
这里的find函数,会让人头晕眼花,我们不妨这样来解释:
先init, 在连接之前,所有的点都单独城一棵树;第一次显然是连接 【2,1】这条边,连接之后,fa[1]=2,2就成了这颗森林的根节点;下一次显然是连接【3,4】,连接之后 fa[4]=3,3就变成了这棵树的根节点;
(如此一来,不同的树就可以用固定的相同的根节点来识别,可以判断是否是一个树)
find函数就是专门用来找到根节点的函数(find函数集合 查找与重置为一体的函数,非常精妙!!!)
全部代码:
Kruscal:
#include<iostream>
#include<string>
#include<sstream>
#include <cmath>
#include<algorithm>
using namespace std;
struct edge{
int u,v,val;
}e[1000000];;
int set_pos[10010]={0}; //被占用设为1
int N,M;
int N_pier=0;
struct pier{
int u,val;
}p[100000];
int pier_num_0_connect=0;
int pier_num_0_array[10010]={0};
int change_land_road(int t){
e[t].val=0; return 0;
}
bool cmp(edge a,edge b){
return a.val<b.val;
}
bool cmp_2(pier a,pier b){
return a.val<b.val;
}
int find_min_path(){
int min_money=100000;
int x_tmp,y_tmp,t_tmp;
int kind;// 1 表示陆地 2表示水路
for(int i=1;i<=M;i++){
if(set_pos[e[i].u]+set_pos[e[i].v]==1) //判断是否一个在树内,一个在树外
if(e[i].val < min_money){
min_money=e[i].val;
kind = 1;
x_tmp = e[i].u;
y_tmp = e[i].v;
t_tmp = i;
break;
}
}
for(int i=1;i<=N_pier;i++)
if(set_pos[0]+set_pos[p[i].u]==1)
if(p[i].val < min_money){
min_money=p[i].val;
kind = 2;
x_tmp = 0;
y_tmp = p[i].u;
t_tmp = i;
break;
}
if(kind == 1){ //陆路
set_pos[x_tmp]=1;set_pos[y_tmp]=1;
change_land_road(t_tmp); //把选中的路权值设为0,这样最后还有权值为负的就分得清楚了
return min_money;
}
if(kind == 2){ //水路
set_pos[x_tmp]=1;set_pos[y_tmp]=1;
if(x_tmp == 0){ //设置的额外的信号量,记录多少点与0点相连,如果最后只有一个,那么就要删去
pier_num_0_array[pier_num_0_connect]=t_tmp;
pier_num_0_connect++;
}
return min_money;
}
}
int main(){
cin >> N >> M;
int total_money=0;
int x=0;
for(int i=1;i<=M;i++)
cin >> e[i].u >> e[i].v >>e[i].val;
for(int i=1;i<=N;i++){
cin >> x;
if(x>0){
N_pier++;
p[N_pier].u=i;
p[N_pier].val=x;
}
}
sort(e+1,e+M+1,cmp);
sort(p+1,p+N_pier+1,cmp_2);
set_pos[1]=1;
for(int i=0;i<N;i++){
total_money+=find_min_path();
}
//对于水路,滞后性处理
if(pier_num_0_connect==1){
int the_only_one_pier=pier_num_0_array[0];
total_money-=p[the_only_one_pier].val;
}
//对于陆路的,滞后性处理
for(int i=1;i<=M;i++){
if(e[i].val < 0)
total_money += e[i].val;
}
cout << total_money <<endl;
return 0;
}
Prim:
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
#define MAXN 10005
#define MAXM 100005
struct node{
int u,v,w;
}edge[MAXM+MAXN];
int fa[MAXN];
bool cmp(node a,node b)
{
return a.w<b.w;
}
int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
void init(int n)
{
for (int i=0;i<=n;i++)
fa[i]=i;
}
int kruscal(int n)
{
int fu,fv,w,sum=0;
for (int i=0;i<n;i++)
{
fu=find(edge[i].u);
fv=find(edge[i].v);
w=edge[i].w;
if (fu!=fv||w<0){
sum+=w;
fa[fu]=fv;
}
}
return sum;
}
int route(int n,int m)
{
init(n);
sort(edge,edge+m,cmp);
int ans=kruscal(m);
return ans;
}
int main()
{
int n,m,cost,i,fu,fv,f,k,ans,ans1;
while (cin>>n>>m)
{
for (i=0;i<m;i++)
cin>>edge[i].u>>edge[i].v>>edge[i].w;
k=m;
for (i=1;i<=n;i++)
{
cin>>cost;
if (cost!=-1)
{
edge[k].u=0;
edge[k].v=i;
edge[k++].w=cost;
}
}
init(n);
for (i=0;i<m;i++)
{
fu=find(edge[i].u);
fv=find(edge[i].v);
if (fu!=fv)
fa[fu]=fv;
}
f=find(1);
for (i=2;i<=n;i++)
{
if (f!=find(i))
break;
}
if (i==n+1)
{
ans=route(n,m); //考虑码头(即考虑0点)
ans1=route(n,k); //不考虑码头
if (ans>ans1)
cout<<ans1<<endl;
else cout<<ans;
continue;
}
cout<<route(n,k)<<endl;
}
return 0;
}