CCF-201412-4(最优灌溉)

一:问题
1.问题描述

雷雷承包了很多片麦田,为了灌溉这些麦田,雷雷在第一个麦田挖了一口很深的水井,所有的麦田都从这口井来引水灌溉。
  为了灌溉,雷雷需要建立一些水渠,以连接水井和麦田,雷雷也可以利用部分麦田作为“中转站”,利用水渠连接不同的麦田,这样只要一片麦田能被灌溉,则与其连接的麦田也能被灌溉。
  现在雷雷知道哪些麦田之间可以建设水渠和建设每个水渠所需要的费用(注意不是所有麦田之间都可以建立水渠)。请问灌溉所有麦田最少需要多少费用来修建水渠。

2.格式

输入格式
  输入的第一行包含两个正整数n, m,分别表示麦田的片数和雷雷可以建立的水渠的数量。麦田使用1, 2, 3, ……依次标号。
  接下来m行,每行包含三个整数ai, bi, ci,表示第ai片麦田与第bi片麦田之间可以建立一条水渠,所需要的费用为ci。
输出格式
  输出一行,包含一个整数,表示灌溉所有麦田所需要的最小费用。

3.样例

样例输入
4 4
1 2 1
2 3 4
2 4 2
3 4 3
样例输出
6
样例说明
  建立以下三条水渠:麦田1与麦田2、麦田2与麦田4、麦田4与麦田3。

4.评测用例规模与约定

前20%的评测用例满足:n≤5。
  前40%的评测用例满足:n≤20。
  前60%的评测用例满足:n≤100。
  所有评测用例都满足:1≤n≤1000,1≤m≤100,000,1≤ci≤10,000。

二:理解
最小生成树
kruskal算法和prim算法

一.kruskal算法
1.数据结构:

struct Node {
    
    
	int a;
	int b;
	int c;  //花销
};   //存储渠道信息 
vector<Node>p;   //存储输入的渠道信息
int n, m;
int sign[1005];  //标志着某一麦田属于哪一类别。

2.将建立渠道的信息都存到vector中,然后针对花销进行排序

bool cmp(const Node s, const Node e)
{
    
    
	return s.c < e.c;  //按着耗资来排序 
}
sort(p.begin(),p.end(),cmp);

3.kruskal算法
如果不属于同一类的话,那么这个渠道就需要建立,直接计算费用就可以。
后续的处理就是:将不同的一类归为一类。

for(int i=0; i<m; i++)
{
    
    
	int x = sign[p[i].a];
	int y = sign[p[i].b];
	if(x != y)  //不是一类 
	{
    
    
		counts += p[i].c;
		for(int j=1; j<=n; j++)   //将不同类别的归为一类, 
			if(sign[j] == y)
				sign[j] = x;
	}
}

二:prim算法
①.模板式:
理解:
1.num[i][j] = c;记录渠道信息,表示麦田i和麦田j建立渠道,花费c
注意: num[i][j] = num[j][i] = c;(无向)

fill(num[0],num[1004]+1005,inf);
loop(i,0,m)
{
    
    
	int a, b, c;
	cin >> a >> b >> c;
	num[a][b] = c;
	num[b][a] = c;
}

2.初始化第一个点
sign[i] 记录 从1点开始到第i点的花销。

sign[1] = -1;
loop(i, 2, n+1)  //保存和1有关系的边 
	sign[i] = num[1][i];

3.判断哪个是最少的花销,并且将这个点置为B类。

loop(j, 2, n+1)  //找和判断的这个点联系最小的边 
{
    
    
	if(sign[j]!=-1 && sign[j]<minnum)
	{
    
    
		minnum = sign[j];
		temp = j;	
	}
}
sum += minnum;  //把合适的边加起来 
sign[temp] = -1;   //并且标记指向的这个点不能再寻找 

4.继续从找到的那个点出发,更新sign的值

loop(j, 2, n+1)  //更新权值 
if(sign[j]!=-1 && sign[j]>num[temp][j])
 	sign[j] = num[temp][j];	

注:
感觉这个不是真的prim算法,原因:
在找到最小的边,将对应的点记录下来,只是直接对这个点进行扩展(应该是对于整个遍历过得点进行扩展)。
例如:
一共5个点,其中1,2两个点已经加入到另一类B类(遍历过的一类),那么A类就还剩3,4,5三个点。在进行扩展的时候,我们就看从1点或者2点出发(终点不能是这两个点)的边,哪一个权值最小。就在继续记录这个边,将对应的点加入A类。以此类推…
我改了一下代码:
(这个是超时的)

#include<bits/stdc++.h>
#include<iostream>
#include<algorithm>
using namespace std;
#define inf  0x7f7f7f7f
#define loop(i,start,end) for(int i=start; i<end; i++) 

//记录渠道信息,num[i][j] = c;表示麦田i和麦田j建立渠道,花费c
int num[1005][1005];
//sign[i] = c;表示指向麦田i的渠道,花费c。
//为-1时表示,已经判断过指向i的渠道(边) 
int sign[1005];
int n, m;

int prim()
{
    
    
	int sum = 0;
	sign[1] = -1;
	loop(i, 2, n+1)  //保存和1有关系的边 
		sign[i] = num[1][i];
	loop(i,0,n-1)  //找n-1个边 
	{
    
    
		int minnum = inf;
		int temp;
		loop(j, 2, n+1)  //找和判断的这个点联系最小的边 
		{
    
    
			if(sign[j]!=-1 && sign[j]<minnum)
			{
    
    
				minnum = sign[j];
				temp = j;	
			}
		}
		sum += minnum;  //把合适的边加起来 
		sign[temp] = -1;   //并且标记指向的这个点不能再寻找 
		loop(k,1,n+1)
		{
    
    
			if(sign[k] == -1)	//在B类里面
			{
    
    
				loop(j, 2, n+1)  //更新权值 
					if(sign[j]!=-1 && sign[j]>num[k][j])
					 	sign[j] = num[k][j];	
			}
		}
	}
	return sum;	
}
int main()
{
    
    
	cin >> n >> m;
	fill(num[0],num[1004]+1005,inf);
	loop(i,0,m)
	{
    
    
		int a, b, c;
		cin >> a >> b >> c;
		num[a][b] = c;
		num[b][a] = c;
	}
	cout << prim();
	return 0;
}

②.优先队列式:
优点就是不用判断大小,优先队列不是普通的队列,是优先级高的先输出。
1.数据结构
vector数组+优先队列

struct Node {
    
    
    int to;
    int c;
    //优先队列,从小到大 
    friend bool operator <(Node a, Node b)
	{
    
    
        return a.c > b.c;
    }
};
vector<Node> node[1005];  //没用二维数组 
priority_queue<Node> p;
int vis[1005] = {
    
    0};
int n,m;

注意这个优先队列需要有这一句话:

friend bool operator <(Node a, Node b)
{
    
    
       return a.c > b.c;  //由小到大
}

2.初始化vector,初始化优先队列

for(int i=0; i<m; ++i)
{
    
    
     int a,b,c;
      cin >> a >> b >> c;
      node[a].push_back(Node{
    
    b, c});
      node[b].push_back(Node{
    
    a, c});
}
for(int i=0; i<node[1].size(); ++i)
    p.push(node[1][i]);
vis[1] = 1;

3.由选中的边扩展边

Node temp = p.top();  //最小的那个 
p.pop();
 
int i = temp.to;
if(vis[i]) 
	continue;  //如果已经访问过了,需要跳过,否则构成回路了
vis[i] = 1;
for(int j=0; j<node[i].size(); j++)
{
    
    
    if(!vis[node[i][j].to]) //将连接没有访问过的点的边加入优先队列
        p.push(node[i][j]);
}

注:这个思路比较好理解,就是按照prim算法的思路来的。
由优先队列加持。
开始就将由与1点连接的边加入队列,选中最小的边,按着这个边的终点假如是2继续扩展边。将对应的边也都入队,这时候队列里面的边是与1或2有连接的边然后在从这些边里面找最小的边。依次类推…,就会找到最优的路,并且,有visited[i]来判断是否遍历过,遍历过的就不考虑。

总注:
1.->介绍sort()排序<-

升序:sort(begin,end,less<data-type>());

降序:sort(begin,end,greater<data-type>()).

2.介绍kruskal算法
->kruskal算法<-

3:介绍prim算法
->prim算法<-

4.fill()函数
fill()函数参数:fill(first,last,val);
// first 为容器的首迭代器,last为容器的末迭代器,val为将要替换的值。
注意:

fill()中 ,它的原理是把那一块单元赋成指定的值,也就是说任何值都可以
memset(),则是将s所指向的某一块内存中的每个字节的内容全部设置为ch指定的ASCII值,即0 、1

5.介绍优先队列
->队列与优先队列<-

三:代码
kruskal代码:

#include<bits/stdc++.h>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

struct Node {
    
    
	int a;
	int b;
	int c;
};
//Node ps[100,005]; 
vector<Node>p;
int n, m;
int sign[1005];

bool cmp(const Node s, const Node e)
{
    
    
	return s.c < e.c;  //按着耗资来排序 
}
int Kruskal()
{
    
    
	long long counts = 0;
	for(int i=1; i<=n; i++)
		sign[i] = i;
	for(int i=0; i<m; i++)
	{
    
    
		int x = sign[p[i].a];
		int y = sign[p[i].b];
		if(x != y)  //不是一类 
		{
    
    
			counts += p[i].c;
			for(int j=1; j<=n; j++)   //将不同类别的归为一类, 
				if(sign[j] == y)
					sign[j] = x;
		}
	}
	return counts;
}
int main()
{
    
    
	Node temp;
	cin >> n >> m;
	for(int i=0; i<m; i++)
	{
    
    
		cin >> temp.a >> temp.b >> temp.c;
		p.push_back(temp); 
	}
	sort(p.begin(),p.end(),cmp);
	cout << Kruskal(); 
	return 0;
}

prim算法代码:
模板式:

#include<bits/stdc++.h>
#include<iostream>
#include<algorithm>
using namespace std;
#define inf  0x7f7f7f7f
#define loop(i,start,end) for(int i=start; i<end; i++) 

//记录渠道信息,num[i][j] = c;表示麦田i和麦田j建立渠道,花费c
int num[1005][1005];
//sign[i] = c;表示指向麦田i的渠道,花费c。
//为-1时表示,已经判断过指向i的渠道(边) 
int sign[1005];
int n, m;

int prim()
{
    
    
	int sum = 0;
	sign[1] = -1;
	loop(i, 2, n+1)  //保存和1有关系的边 
		sign[i] = num[1][i];
	loop(i,0,n-1)  //找n-1个边 
	{
    
    
		int minnum = inf;
		int temp;
		loop(j, 2, n+1)  //找和判断的这个点联系最小的边 
		{
    
    
			if(sign[j]!=-1 && sign[j]<minnum)
			{
    
    
				minnum = sign[j];
				temp = j;	
			}
		}
		sum += minnum;  //把合适的边加起来 
		sign[temp] = -1;   //并且标记指向的这个点不能再寻找 
		loop(j, 2, n+1)  //更新权值 
			if(sign[j]!=-1 && sign[j]>num[temp][j])
			 	sign[j] = num[temp][j];	
	
	}
	return sum;	
}
int main()
{
    
    
	cin >> n >> m;
	fill(num[0],num[1004]+1005,inf);
	loop(i,0,m)
	{
    
    
		int a, b, c;
		cin >> a >> b >> c;
		num[a][b] = c;
		num[b][a] = c;
	}
	cout << prim();
	return 0;
}

优先队列式:

#include<bits/stdc++.h>
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;

struct Node {
    
    
    int to;
    int c;
    //优先队列,从小到大 
    friend bool operator <(Node a, Node b)
	{
    
    
        return a.c > b.c;
    }
};
vector<Node> node[1005];
priority_queue<Node> p;
int vis[1005] = {
    
    0};
int n,m;

int prim()
{
    
    
    for(int i=0; i<node[1].size(); ++i)
        p.push(node[1][i]);
    vis[1] = 1;
    int num = 0, counts = 0;
    while(!p.empty())
	{
    
    
        Node temp = p.top();
        p.pop();
        
		int i = temp.to;
        if(vis[i]) 
			continue;  //如果已经访问过了,需要跳过,否则构成回路了
        vis[i] = 1;
        for(int j=0; j<node[i].size(); j++)
		{
    
    
            if(!vis[node[i][j].to]) //将连接没有访问过的点的边加入优先队列
                p.push(node[i][j]);
        }
        counts += temp.c;
        num++;
        if(num==n-1) 
			return counts; //n个点的图,n-1条边就是连通图,退出
    }
}
int main()
{
    
    
    cin>>n>>m;
    for(int i=0; i<m; ++i)
	{
    
    
        int a,b,c;
        cin >> a >> b >> c;
        node[a].push_back(Node{
    
    b, c});
        node[b].push_back(Node{
    
    a, c});
    }
    cout<<prim();
    return 0;
}

猜你喜欢

转载自blog.csdn.net/pfl_327/article/details/104623664