oj题目:城市建设

快要考研机试了,练练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;  
}

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值