某地区经过对城镇交通状况的调查,得到现有城镇间快速道路的统计数据,并提出“畅通工程”的目标:使整个地区任何两个城镇间都可以实现快速交通(但不一定有直接的快速道路相连,只要互相间接通过快速路可达即可)。现得到城镇道路统计表,表中列出了任意两城镇间修建快速路的费用,以及该道路是否已经修通的状态。现请你编写程序,计算出全地区畅通需要的最低成本。
输入格式:
输入的第一行给出村庄数目N (1≤N≤100);随后的行对应村庄间道路的成本及修建状态:每行给出4个正整数,分别是两个村庄的编号(从1编号到N),此两村庄间道路的成本,以及修建状态-1表示已建,0表示未建。
输出格式:
输出全省畅通需要的最低成本。
输入样例:
4
1 2 1 1
1 3 4 0
1 4 1 1
2 3 3 0
2 4 2 1
3 4 5 0
输出样例:
3
Ω
初看这题的分数时,我虎躯一震…竟然有35分。回想自己以往被虐的难题最多也只有30分,莫非是什么绝世难题。
不过…看这题目长度并没有难题的气势。
读完题目,常规之中透着一丝不羁。第一感觉是一道求最小生成树的题目,不过又有所不同,因为部分边已经选定了,我们需要在题目选定部分边的情况下选取成本(权值之和)最小并能使所有点连通的边。既然如此,那就把已经连通的点们看作一个大点,然后求大点们的最小生成树就完事了。参照处理最小生成树的Alg.Kruskal思想,我们可以每次选取一条两个顶点不在同一个连通集内且权值最小的边,直至所有点均连通。
对于连通集的处理,那就不得不说一说并查集。下面简单介绍一下并查集。
Wikipedia:”并查集是一种用于处理一些不交集的合并及查询问题的数据结构。” 我们需要对一些元素进行划分,这里可以认为元素中有若干个帮派,帮派里的元素也有等级制度(诸如头头,大哥,小弟,etc)。那么事实上我们只要让每个元素记住自己上一级是谁即可,即父节点,而等级最高元素的父节点是他自己,把这种层次形象化后其实就是个树的数据结构。那么对任意一个小弟,通过不断访问父节点的父节点就能找到最大的头头。其最大的特点就是每个元素只存自己的父节点。对于两个帮派交汇,若想要元素较少的帮派归附于另一帮派,只要让该帮派头头的父节点指向(这里的指向并不是说父节点是指针,也可以是数组)另一帮派的头头即可。以上所说的这种呢是毫无优化最基础的并查集,因为很显然在特殊情况下可能会退化成链表。我们当然也可以直接让所有帮派成员的父节点都指向最大元素,有兴趣进一步了解可以移步至算法学习笔记(1) : 并查集。
那么本题中的村庄很显然就是我们要划分的元素,因此我们可以先定义一个存储各村庄父节点的vector
,一开始均初始化为自己:
vector<int> belong(n);
iota(belong.begin(), belong.end(), 0);
//belong->[0,1,2,...,n-1]
同时我们需要一个size
向量来存储每个帮派现有的元素个数,好在帮派交汇时决定谁并入谁,这是一种优化策略:
vector<int> size(n, 1);
对于已经建成的公路,公路两端的村庄是连通的,需要将这两个帮派合并,那么就根据少数并入多数的策略进行合并:
int max_idx = (size[belong[x]] > size[belong[y]]) ? belong[x] : belong[y],
min_idx = (size[belong[x]] > size[belong[y]]) ? belong[y] : belong[x];
belong[min_idx] = max_idx;
size[max_idx] += size[min_idx];
由于我想要知道初始情况下连通集的个数,因此我必须让所有成员均指向其帮派头目,才能得知有几个帮派头目。但是上述代码会发生一个帮派头头的父节点指向了另一个帮派头头,但其手下成员的父节点并未更新,因此结束上述操作后还需找到每个元素真正的头头:
for (auto &v: belong)
while (v != belong[v])
v = belong[v];
此时belong
中都是头头的编号了,我们只需将其放入set
容器中即可得知连通集的个数。显然,每次找一条边只能使连通集个数-1,那么我们需要找的边数即是连通集的个数-1。
话说回来,每条公路的成本该怎么存呢?其实初看题目的时候我就纳闷已经修通的公路为啥还要告诉我成本。如果不给的话,数据倒是不规整了…估计是程序员那该死的强迫症作祟吧。也就是说,其实需要的数据也不是很多…本来最大也就的成本矩阵,现在没这个念头了。本来想用map
和pair
结合的,不过由于后面需要对成本进行排序…sort
和map
天性不搭,全剧终。那就找map
他爸pair
:
vector<pair<pair<int, int>, int> cost;
/*pair<pair<village1,village2>,cost>*/
最后,只要将cost
按从小到大的顺序进行sort
,然后按顺序判断每个公路的两个村庄是否位于两个连通集,若是则计入其成本直至有(连通集的个数-1)条符合条件的公路为止。另外这个过程需要动态更新连通集的关系,因为连通集的个数一直在变少。
C☺DE
#include <iostream>
#include <vector>
#include <set>
#include <numeric>
#include <algorithm>
using namespace std;
typedef pair<pair<int, int>, int> info;
int main()
{
int n, x, y, c;
bool flag;
cin >> n;
vector<int> belong(n), size(n, 1);
vector<info> cost;
iota(belong.begin(), belong.end(), 0);
for (int i = 0; i < n * (n - 1) / 2; ++i)
{
cin >> x >> y >> c >> flag;
x -= 1; y -= 1;
if (!flag)
cost.push_back({ {x, y}, c});
else
{
int max_idx = (size[belong[x]] > size[belong[y]]) ? belong[x] : belong[y],
min_idx = (size[belong[x]] > size[belong[y]]) ? belong[y] : belong[x];
belong[min_idx] = max_idx;
size[max_idx] += size[min_idx];
}
}
for (auto &v: belong)
while (v != belong[v])
v = belong[v];
set<int> div(belong.begin(), belong.end());
int all_cost = 0, idx = 0;
sort(cost.begin(), cost.end(), [](info &a, info &b) { return a.second < b.second; });
for (int i = 1; i < div.size(); ++i)
{
int v1, v2;
// 优化版
do
{
v1 = cost[idx].first.first, v2 = cost[idx].first.second;
while (belong[v1] != belong[belong[v1]])
belong[v1] = belong[belong[v1]];
while (belong[v2] != belong[belong[v2]])
belong[v2] = belong[belong[v2]];
++idx;
} while (belong[v1] == belong[v2]);
int max_idx = (size[belong[v1]] > size[belong[v2]]) ? belong[v1] : belong[v2],
min_idx = (size[belong[v1]] > size[belong[v2]]) ? belong[v2] : belong[v1];
belong[min_idx] = max_idx;
size[max_idx] += size[min_idx];
/* 简化版
do
{
v1 = cost[idx].first.first, v2 = cost[idx].first.second;
++idx;
} while (belong[v1] == belong[v2]);
belong[belong[v1]] = belong[v2];
*/
all_cost += cost[idx - 1].second;
}
cout << all_cost;
}
Σ 几点说明
-
map
的本质是pair
,但pair
的两部分是平等的,不能通过其一访问另一个 -
iota
函数在指定初值后可以使vector
按增序排列:#include <numeric> vector<int> v(num); //需要先确定向量大小 iota(v.begin(),v.end(),start_num); //v=[start_num , start_num+1 , … , start_num+num-1]
-
经过几次实验,读入
bool
变量,只能输入0和1.若输入其他数字可能不会影响bool
变量的读入,但会影响后面变量的读入. -
优化版代码在查找符合条件的公路时顺便更新父节点,并依旧按照小并大原则进行合并,这在数据较大时较有优势;而简化版则简短精炼,合并时不管三七二十一均将
v1
并入v2
,也不更新父节点. 以下是两者的时间比较:☜优化版简化版☞
δ 彩蛋
然鹅简化版真的是对的吗?虽然简化版确实通过了所有测试点,但我一直觉的简化版不够严谨,因此尝试着枚举一个反例出来。随便构造了一个:
5
1 2 2 0
1 3 10 1
1 4 10 0
1 5 3 0
2 3 1 0
2 4 10 1
2 5 10 0
3 4 10 0
3 5 10 0
4 5 10 0
这个测试用例优化版的结果是4,而简化版的结果是3。显然,简化版中枪了,我构造的思路就是着眼于简化版不动态更新各个元素的父节点,不更新也就算了,在检查的时候还不访问元素父节点的父节点,这样在一定的条件下就必然会导致选中的一条公路,其端点两个村庄的父节点不同,然而在之前的选取过程中这两个父节点已经合并了,也就是说,选取了一条处于一个连通集中的公路,花了没必要的造路成本。
其实简化版是我在提交完优化版后乱删而成的。当时就想着能不能简化代码,但自己改改太累了,不妨先试着删亿点然后提交看看,如果通过了那就再思考思考为什么可以删掉,于是乎就有了这个彩蛋。我已经将这个问题以邮件的形式发给PTA了。
那么怎么改正简化版呢,其实不改do-while
部分就好了:
do
{
v1 = cost[idx].first.first, v2 = cost[idx].first.second;
while (belong[v1] != belong[belong[v1]])
belong[v1] = belong[belong[v1]];
while (belong[v2] != belong[belong[v2]])
belong[v2] = belong[belong[v2]];
++idx;
} while (belong[v1] == belong[v2]);
belong[belong[v1]] = belong[v2];