参考文章:
pat顶级1001 Battle Over Cities - Hard Version (35 point(s))_日沉云起的博客-CSDN博客
题述
解题思路
首先判断是一个图论的题目。因为最终的目的是保持整个图(除去被攻占的城市)连通,可以想到关于连通性的数据结构,也就是并查集。而主要操作要求花费最小的代价使得整张图连通,可以想到最小生成树。虽然和最小生成树不同,题目中本来已经有道路保持连通,相当于“花费最小的代价,使得子图连通”;但是根据Kruskal最小生成树算法的原理,算法基本没有改变。
策略:按序号遍历每一个点,分别假设将每一个点以及与它相连的道路“抹去”,计算该节点被破坏后维持连通所造成的花费,将花费存于数组,并更新最大花费。最终将话费达到最大值的序号依次输出。
对于单个节点被抹去之后的代价计算,首先要建立当前的联通情况。
将并查集初始化,然后用可用的道路中不与被毁节点相连的道路建立并查集(即遍历所有输入中得到的可用道路,要加上判断该道路的某一端是不是被毁城市)。这样,就建立了当前的联通情况。
然后,我们需要在status=0的道路中找到数条,使得修复这些路能使得整张图除了被毁城市都联通,并且代价最小。
遍历开始时得到的status=0的道路(开始时存在一个数组中,并事先按照代价升序排序),遍历时要加条件跳过与被毁城市相连的可修复道路(因为城市被攻占了,不能够建立通向这座城市的道路)。采用Kruskal的贪心策略,如果当前可修复道路连接的是分属两个子图的城市(用并查集判断),则修复该道路。如果连接的是同一子图的两个城市,就跳过。遍历之后,就得到所需的最优解。
贪心策略的正确性可以这样理解:假如一条道路连接的是子图内部的两个城市,那么选择它一定是浪费的,因为如果后续加入的一条道路使得这个子图与外部联通了,那么把这条内部连接的路去掉也不会有任何问题,所有的点依然都连通。而如果一条道路连接的是两个子图的不同城市,那么不选择它一定是浪费的,因为数组按照升序排列,即使后续通过其他的道路实现了相同的联通作用,代价也一定比选择这条道路更大。而且,子图之间的联通能且仅能通过连接这两个子图的两个节点得到。因此,贪心策略就是最优解。
最后,在获取最优解之后,存储并更新最大值之前,还有一步操作:检查在这样的操作之后,整个图是否联通。因为Kruskal的最坏结果也就是把所有能修的道路都修了,如果整张图仍然不能联通,说明失去这座城市的代价是无穷大,用宏定义为INF。这样之后才可以更新最优解。
程序结构
设计结构体保存道路的所有信息,并在输入的时候对道路进行判别,可用的存为一个数组,不可用的存为另一个数组;建立并查集需要前者,获取最优解需要后者。其他的包括并查集的所有函数。代码如下。
#include <iostream>
#include <algorithm>
#define MAXN 500
#define max(a,b) ((a)>(b)?(a):(b))
#define INF 1e7
//道路的存储结构
struct connection{
int city1;
int city2;
int cost;
bool status;
};
//并查集函数
int pre[MAXN+1];
void init(int n){
for (int i=1;i<=n;i++){
pre[i]=i;
}
}
int find(int x){
if (pre[x]==x) return x;
else return pre[x]=find(pre[x]);
}
int check(int x,int y){
return find(x) == find(y);
}
bool join(int x,int y){
int xroot=find(x),yroot=find(y);
if (xroot==yroot) return false;
else pre[yroot]=xroot;
return true;
}
//用于sort函数的compare
bool compare(connection a,connection b){
return a.cost<b.cost;
}
int main(){
//输入处理
int n,m;
scanf("%d%d",&n,&m);
connection connections[m+1];
int connections_p=0;
connection toberepair[m+1];
int repair_p=0;
connection tmp;
for (int i=1;i<=m;i++){
std::cin>>tmp.city1>>tmp.city2>>tmp.cost>>tmp.status;
if (tmp.status==1){
connections[++connections_p]=tmp;
}
else{
toberepair[++repair_p]=tmp;
}
}
if (repair_p) std::sort(toberepair+1,toberepair+repair_p,compare);
int cost[MAXN+1]={0};
int maxcost=0;
//遍历节点
for (int i=1;i<=n;i++){
init(n);
//建立并查集
for (int j=1;j<=connections_p;j++){
if (connections[j].city1!=i and connections[j].city2!=i) join(connections[j].city1,connections[j].city2);
}
//选择最优解
for (int j=1;j<=repair_p;j++){
if (toberepair[j].city1!=i and toberepair[j].city2!=i){
if (!check(toberepair[j].city1,toberepair[j].city2)){
join(toberepair[j].city1,toberepair[j].city2);
cost[i]+=toberepair[j].cost;
}
}
}
//判别是否整张图都联通
int flag=0;
for (int j=1;j<=n;j++){
if (pre[j]==j && j!=i){
if (flag==0) flag=1;
else if (flag==1){
flag=2;break;
}
}
}
if (flag==2) cost[i]=INF;
maxcost=max(maxcost,cost[i]);
}
//输出
//如果本来就是联通的
if (maxcost==0){
printf("0");
return 0;
}
//平凡情况输出
int ans[50];
int ans_p=0;
for (int i=1;i<=n;i++){
if (cost[i]==maxcost){
ans[++ans_p]=i;
}
}
for (int i=1;i<=ans_p;i++){
printf("%d%c",ans[i],(i==ans_p)?'\0':' ');
}
return 0;
}
遇到的问题
格式错误。对格式的判断是严格的,结尾不能有空格。小问题
TLE。本来Kruskal的排序用的是自己写的快排,结果有一个点就tle了。想着也没什么东西好砍时间,不会是排序太慢的问题吧,结果换成内置的sort直接就过了。
(好家伙)
输入的特判。考虑边界情况,有可能一条道路都没有,也有可能只有一条道路。在这种情况下,sort会出问题:
if (repair_p) std::sort(toberepair+1,toberepair+repair_p,compare);
如果没有if,就会发生repair_p=0报SF的情况。反正一条路和没有路不需要排序,过滤掉就得了。
)个人感想:内置函数要是能用还是用内置的。不然都不知道自己写出来的东西有多菜(