题目描述
As an emergency rescue team leader of a city, you are given a special map of your country. The map shows several scattered cities connected by some roads. Amount of rescue teams in each city and the length of each road between any pair of cities are marked on the map. When there is an emergency call to you from some other city, your job is to lead your men to the place as quickly as possible, and at the mean time, call up as many hands on the way as possible.
明显的与图相关的问题。有权图,既有边权值(路径长度)又有点权值(救援队个数)。要求你找出最短的路径,而且问最短的路径总共有几条;同时,找出这些最短路径中,沿途能聚集到最多的救援队的路径。
简而言之,就是无向有权图中,找出单源路径中,边权最少,点权最多的一条。
输入描述
N为城市编号(即顶点ID,从0到N-1编号。在PAT考试里,要特别注意,id是从1到N编号,还是0到N-1,还是与N无关的),M是边的个数,C1、C2分别是起点和终点。
接下来的N个值以编号0到N-1,给出每个城市救援队个数(点权值)
接下来的M行以 顶点1 顶点2 距离(边权) 格式给出所有边
输入样例
5 6 0 2
1 2 1 5 3
0 1 1
0 2 2
0 3 1
1 2 1
2 4 1
3 4 1
输出描述
For each test case, print in one line two numbers: the number of different shortest paths between C1 and C2, and the maximum amount of rescue teams you can possibly gather. All the numbers in a line must be separated by exactly one space, and there is no extra space allowed at the end of a line.
要求输出最短路径的条数和最短路径中,能聚集到最多救援队的个数。
这意味着,我们无需花心思去明确路径具体经历了哪些顶点,只需要求出个数,和点权值之和就可以了。
输出样例
2 4
分析
有权图单源最短路径,第一个想到的是优选Dijkstra算法。然而,Dijkstra算法只能找到一条最短的路径而且不关心点权值。所以一定要对Dijkstra做一点改良。
除此以外,深度优先搜索也可以搞定。
“增强版Dijkstra”
Dijkstra算法的核心步骤是,在顶点v访问之后,会检查与v邻接的顶点,对于一个与v邻接的顶点w,如果从v到w的距离(cvw)+v到起点的距离(dist[v])小于w到起点的距离(dist[w]),就把w的前往起点的路径更新到v(path[w] = v)。如果是等于或大于则不做任何处理。
所谓增强版Dijkstra算法(我自己起的名 ),即是:path中不仅仅存放当前顶点前往起点的下一跳,而改存一整条路径。而且,如果dist[v]+cvw==dist[w],不清空原有的路径,而是将新的路径也存入path中。如果dist[v]+cvw<dist[w],则清空w原有的所有路径,将v的每条路径拷贝一份,再加上v,作为w的路径。
注意:初始时,每个顶点(除起点外)的path都不为空,而是有一条默认的“src -> v”的路径,dist=INF(无穷大)。
举个🌰:
设起点为src,有若干顶点src,0,1,2,3,……,v,w……
某一步后,顶点v被选中,即known[v] = true;
此时
w已有路径:
src -> 1 -> 2 -> 3
src -> 2 -> 4 -> 5
src -> 6 -> 7
dist[w] = 9,即这些路径长度都为9
v已有路径:
src -> 0 -> 1
src -> 2 -> 9 -> 8
dist[v] = 7,即这些路径长度都为7
这时我们考虑可能出现的三种情况:
①v和w之间的距离为1,即 dist[v]+cvw < dist[w]:
这个时候w原来的路径不能再要了,首先清空w的所有路径,再把v的路径全部拷贝过来,再每个加上v作为w的路径:
w现有路径:
src -> 0 -> 1 -> v
src -> 2 -> 9 -> 8 -> v
②v和w之间的距离为2,即dist[v]+cvw==dist[w]:
这时,w的原来路径要保留,还要把v的路径拷贝过来并加上v,追加在w的路径中:
w现有路径:
src -> 1 -> 2 -> 3
src -> 2 -> 4 -> 5
src -> 6 -> 7
src -> 0 -> 1 -> v
src -> 2 -> 9 -> 8 -> v
③v和w之间的距离为3或者更大,即dist[v]+cvw>dist[w]:
不做任何改变。
其他的步骤按正常的dijkstra算法运行即可。由于只需要找到C2的路径,可以在循环中添加一个额外的判断,当C2被访问之后,就可以跳出循环,无需把全部顶点都走完。
之后的工作就相当简单了——最短路径条数就是path[C2]的大小,遍历path[C2]中的每条路径也可以找到聚集救援队最多的一条。
使用Java实现如下:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
public class Emergency_1003 {
static final int BUFFER_SIZE = 8192*25;
static BufferedReader br;
static StringTokenizer tokenizer;
static void initInput(InputStream in) throws Exception {
br = new BufferedReader(new InputStreamReader(in), BUFFER_SIZE);
tokenizer = new StringTokenizer("");
}
static String next() throws Exception{
while(!tokenizer.hasMoreTokens()) {
tokenizer = new StringTokenizer(br.readLine());
}
return tokenizer.nextToken();
}
static int nextInt() throws Exception {
return Integer.parseInt(next());
}
static double nextDouble() throws Exception{
return Double.parseDouble(next());
}
static PrintWriter pw;
public static void main(String[] args) throws Exception {
initInput(System.in);
pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out), BUFFER_SIZE));
int V = nextInt();
int E = nextInt();
int bNum = nextInt();
int eNum = nextInt();
List<Vertex> vList = new ArrayList<>();
for (int i = 0; i < V; i++) {
Vertex v = new Vertex(nextInt(), i);
if (i == bNum)
v.dist = 0;
vList.add(v);
}
for (int i = 0; i < vList.size(); i++) {
if (i != bNum)
vList.get(i).path.get(0).add(vList.get(bNum));
}
for (int i = 0; i < E; i++) {
int v1 = nextInt();
int v2 = nextInt();
Edge edge = new Edge(vList.get(v1), vList.get(v2), nextInt());
vList.get(v1).adj.add(edge);
vList.get(v2).adj.add(edge);
}
while (true) {
int isShut = 1;
Vertex v = null;
for (int i = 0; i < vList.size(); i++) {
Vertex t = vList.get(i);
if (!t.known) {
isShut = 0;
if (v == null || t.dist < v.dist)
v = t;
}
}
if (isShut == 1)
break;
v.known = true;
for (Edge edge : v.adj) {
Vertex w = null;
if (v == edge.begin)
w = edge.end;
else
w = edge.begin;
if (!w.known) {
int cvw = edge.dist;
if (v.dist + cvw <= w.dist) {
if (v.dist + cvw < w.dist)
w.path.clear();//清空w原有路径
for (List<Vertex> each : v.path) {
List<Vertex> nList = new ArrayList<>();
for (int i = 0; i < each.size(); i++) {
nList.add(each.get(i));
}
nList.add(v);//将v的路径拷贝后加上v,作为w的新路径
w.path.add(nList);
}
w.dist = v.dist + cvw;
}
}
}
}
Vertex endV = vList.get(eNum);
pw.print(endV.path.size() + " ");
int vCount = 0;
for (List<Vertex> each : endV.path) {
int temp = 0;
for (int i = 0; i < each.size(); i++)
temp += each.get(i).count;
temp += endV.count;
if (temp > vCount)
vCount = temp;
}
pw.println(vCount);
pw.flush();
}
static class Vertex {
public int id;
public List<Edge> adj;
public boolean known;
public int dist;
public int count;
public List<List<Vertex>> path;
public Vertex(int count, int id) {
this.id = id;
this.adj = new ArrayList<>();
this.dist = Integer.MAX_VALUE;
this.known = false;
this.count = count;
path = new ArrayList<>();
path.add(new ArrayList<>());
}
}
static class Edge {
public Vertex begin;
public Vertex end;
public int dist;
public Edge(Vertex begin, Vertex end, int dist) {
this.begin = begin;
this.end = end;
this.dist = dist;
}
}
}
耗时情况如下:
Java实现是早写的,那时候还比较“年轻”,很多地方写的太冗杂了。这次二刷+写题解,用C++又实现了一次:
#include <iostream>
#include <vector>
using namespace std;
const int INF = 2147483647;
int main() {
int N, M, C1, C2;
scanf("%d %d %d %d", &N, &M, &C1, &C2);
/*为避免创建结构体或类,用很多数组来存储每个顶点的信息*/
/*采用邻接表存储,pair的前部是与邻接点的id,后部是这条边的权值*/
vector<vector<pair<int, int> > > adjv(N);
/*存储每个顶点有多少救援队(点权值)*/
vector<int> teamv(N);
vector<bool> known(N, false);
/*初始时给所有顶点添加一条只包含起点的默认路径*/
vector<vector<vector<int> > > path(N, vector<vector<int> >(1, vector<int>(1, C1)));
vector<int> dist(N, INF);
/*读取顶点和边的信息*/
for (int i = 0; i < N; i++) scanf("%d", &teamv[i]);
for (int i = 0; i < M; i++) {
int one, ano, dist;
scanf("%d %d %d", &one, &ano, &dist);
adjv[one].push_back(make_pair(ano, dist));
adjv[ano].push_back(make_pair(one, dist));
}
/*初始化起点的信息*/
dist[C1] = 0;
/*注意,此句非常重要,将初始点的路径置为空,即起点有且仅有一条到自己的空路径
此句不可省去,否则运行Dijkstra之后其他顶点的路径在起始位置会有一个重复的C1,即 C1->C1->...->va
此句也也不可写作 path[C1].clear() ,否则其他顶点不会拥有任何路径
*/
path[C1][0].clear();
/*Dijkstra算法*/
while (true) {
/*在未知顶点中寻找一个dist最小的顶点*/
int minDist = INF, v = -1;
for (int i = 0; i < N; i++) {
if (!known[i] && dist[i] < minDist) {
minDist = dist[i];
v = i;
}
}
/*如果终点的dist已经最小,跳出循环,而无需走完整个Dijkstra算法*/
if (v == C2) break;
known[v] = true;
for (int i = 0; i < adjv[v].size(); i++) {
int w = adjv[v][i].first, cvw = adjv[v][i].second;
if (!known[w] && dist[v] + cvw <= dist[w]) {
/*如果是小于,就先清空w原有的所有路径*/
if (dist[v] + cvw < dist[w]) {
path[w].clear();
}
dist[w] = dist[v] + cvw;
/*把v的路径拷贝并加上v,作为w的新路径*/
for (int j = 0; j < path[v].size(); j++) {
vector<int> tmp = path[v][j];
tmp.push_back(v);
path[w].push_back(tmp);
}
}
}
}
/*找到最多能聚集队伍的个数*/
int maxTeamCnt = -1;
for (int i = 0; i < path[C2].size(); i++) {
int tmpTeamCnt = 0;
for (int j = 0; j < path[C2][i].size(); j++)
tmpTeamCnt += teamv[path[C2][i][j]];
/*path中默认不包含终点,不要忘了加上终点的救援队个数*/
tmpTeamCnt += teamv[C2];
if (tmpTeamCnt > maxTeamCnt) maxTeamCnt = tmpTeamCnt;
}
printf("%d %d", path[C2].size(), maxTeamCnt);
return 0;
}
耗时情况如下:
值得一提的是,这种方法对于这道题仍然有可以改进的地方。因为结果不要求存下整个路径,因此每次更新path时,只要把路径长度、点权值之和更新了就行。和这个程序只有微小的差别。
但是不管怎么优化,根据考试时能少写就少写的做题原则,这道题目的代码量还是太大了些。之所以把这个算法写出来,是因为具有“普适性”——后面的PAT题目中,不管与图相关的题目如何变,只要是让求 “路径最短+其他条件” 的题目,都可以用这种方法,求出所有最短路径,再筛选。
下面的方法是比较友好的办法
DFS
思路明确,深度优先搜索,找最短路径,顺便找最多的队伍个数。
#include <iostream>
#include <vector>
using namespace std;
int N, M, C1, C2, pathCnt = 0, teamCnt = 0, minDist = 2147483647;
vector<int> teamv;
vector<vector<pair<int, int> > > adjv;
vector<bool> known;
/*id是顶点编号,dist是当前走过的距离,team是当前已经聚集的队伍*/
void dfs(int id, int dist, int team) {
/*id==C2意味着到达终点,这时更新最短路径的长度、最短路径的数量和最多队伍的数量*/
if (id == C2) {
if (dist < minDist) {
minDist = dist;
pathCnt = 1;
teamCnt = team;
}
else if (dist == minDist) {
pathCnt++;
if (team > teamCnt) teamCnt = team;
}
return;
}
for (int i = 0; i < adjv[id].size(); i++) {
int w = adjv[id][i].first, cvw = adjv[id][i].second;
/*if中"dist + cvw <= minDist"相当于剪枝,如果当前的距离已经大于已知的最短距离,
就不用再往下走了,即便达到终点,也比一定不符合要求*/
if (!known[w] && dist + cvw <= minDist) {
known[id] = true;
dfs(w, dist + cvw, team + teamv[w]);
known[id] = false;
}
}
}
int main() {
scanf("%d %d %d %d", &N, &M, &C1, &C2);
teamv.resize(N);
adjv.resize(N);
known.resize(N, false);
for (int i = 0; i < N; i++) scanf("%d", &teamv[i]);
for (int i = 0; i < M; i++) {
int one, ano, dist;
scanf("%d %d %d", &one, &ano, &dist);
adjv[one].push_back(make_pair(ano, dist));
adjv[ano].push_back(make_pair(one, dist));
}
dfs(C1, 0, teamv[C1]);
printf("%d %d", pathCnt, teamCnt);
return 0;
}
耗时情况如下:
BFS也可以做,然鹅个人感觉没有DFS好写,就没有实现。
这里有一个经验,。如果能在上一层中剪枝,就不要递归到下一层发现不符合条件再返回,数据量大的时候,这两个看似相同的操作,带来的开销差异是巨大的。
另,虽然这道题目数据量不大(N<=500),不剪枝也能通过。但是在PAT考试里,要时时刻刻考虑算法的高效性。如果题目的限时不是400ms(更多或更少),说明本题有不止一种解法,但出题人有意让你选择尽可能高效的算法。