题目描述
阿甘是一名乡下学校的校车司机,每天放学都需要送学生回家,送完所有学生后,还需要把车开回学校。
由于是乡下,因此学校建在了乡村的中心位置,可以保证每个学生家都有路线直达学校。但是不同学生的家之间不一定有直达路线。
现在你已经统计好了每个学生家直达学校的路线距离,以及有直达路线关联的两个学生家之间的距离。
请你找出一条最短行车路线,这条路线起点和终点都要求是学校,并且中间需要经过所有学生的家。(假设学校的编号为0,学生的编号为1~x)。
输入描述
第一行输入学生数量 x。学生数量不会高于校车核载规定的15人。
第二行输入 x 个正整数,第 i 个正整数表示编号 i 的学生家到学校直达路线的距离,该距离不大于100000米,以空格分割。i 从 1 开始编号。
第三行输入多个三元组,三元组含义是:学生编号a,学生编号b,学生a家到学生b家的距离c。三元组内以逗号分隔,三元组之间以空格分隔。
输出描述
输出一个最短行车路线,编号之间以 "->" 分隔
用例
输入 | 3 100 300 500 1,2,50 2,3,1000 |
输出 | 0->1->2->1->0->3->0 |
说明 | 最短路线长度为1300。 |
题目解析
本题需要我们帮助司机找到一条最短路径,该最短路径需要满足:
- 从学校(编号0)出发,最终返回学校
- 经过所有学生家位置,每个学生家位置可以经过不止一次
首先,这题需要我们经过所有学生家位置,因此我们对 1~x 学生编号求解全排列,每一个全排列即代表一种策略路径,比如x=3,则有以下策略路径:
- 1->2->3
- 1->3->2
- 2->1->3
- 2->3->1
- 3->1->2
- 3->2->1
我们在这些全排列首尾加上0,即可得所有策略路径:
- 0->1->2->3->0
- 0->1->3->2->0
- 0->2->1->3->0
- 0->2->3->1->0
- 0->3->1->2->0
- 0->3->2->1->0
由于本题的 1 ≤ x ≤ 15 ,因此全排列求解不会超时。
需要注意的是:上面策略路径中,比如:0->2->3->1->0,只是关键点路径 keyPath,并非实际路线 fullPath,比如 3->1 并非 3 直达 1,而是可能需要 3 经过一些中转点后到 1,才能实现最短路。
求解图中任意两点间的最短距离,最佳策略是使用floyd算法。
floyd算法:最短路径算法全套(floyed+dijstra+Bellman+SPFA)_哔哩哔哩_bilibili
JAVA实现
import java.util.*;
public class Main {
static int x;
static int[][] dist;
static int[][] path;
static int minDis = Integer.MAX_VALUE; // minDis记录经过所有点后回到出发点的最短距离
static ArrayList<Integer> keyPath = new ArrayList<>();
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
x = sc.nextInt();
// floyd算法需要基于dist和path矩阵求解
// dist[i][j] 用于记录点 i->j 的最短距离,初始时等价于邻接矩阵
dist = new int[x + 1][x + 1];
// path[i][j] 用于记录点 i->j 最短距离情况下需要经过的中转点,初始时默认任意两点间无中转点,即默认path[i][j] = -1
path = new int[x + 1][x + 1];
for (int i = 0; i < x + 1; i++) {
for (int j = 0; j < x + 1; j++) {
// 初始时默认i,j不相连,即i,j之间距离无穷大
if (i != j) {
dist[i][j] = Integer.MAX_VALUE;
}
path[i][j] = -1;
}
}
for (int i = 1; i <= x; i++) {
dist[0][i] = sc.nextInt();
dist[i][0] = dist[0][i];
}
while (sc.hasNext()) {
int[] tmp = Arrays.stream(sc.next().split(",")).mapToInt(Integer::parseInt).toArray();
int a = tmp[0];
int b = tmp[1];
int c = tmp[2];
dist[a][b] = c;
dist[b][a] = c;
}
// floyd算法调用
floyd();
// 全排列模拟经过所有点的路径
dfs(0, 0, new boolean[x + 1], new LinkedList<>());
keyPath.add(0, 0);
keyPath.add(0);
StringJoiner fullPath = new StringJoiner("->");
for (int i = 1; i < keyPath.size(); i++) {
int start = keyPath.get(i - 1);
int end = keyPath.get(i);
while (true) {
fullPath.add(start + "");
if (path[start][end] == -1) break;
start = path[start][end];
}
}
fullPath.add("0");
System.out.println(fullPath);
}
// floyd算法求解图中任意两点之间的最短路径
public static void floyd() {
for (int k = 0; k < x + 1; k++) {
for (int i = 0; i < x + 1; i++) {
for (int j = 0; j < x + 1; j++) {
// newDist是经过k后,i->j的距离
int newDist = dist[i][k] + dist[k][j];
// 如果newDist是i->j的更短路径
if (newDist < dist[i][j]) {
// 则更新i->j的最短距离
dist[i][j] = newDist;
// 且此更短距离需要经过k, path[i][j]即记录 i->j 最短距离下需要经过点 k
path[i][j] = k;
}
}
}
}
}
/**
* 找一条经过所有点的最短路径,我们可以求解所有点形成的全排列,每一个全排列都对应一条经过所有点的路径,只是经过点的先后顺序不同 //
* 求某个全排列过程中,可以通过dist数组,累计上一个点i到下一个点j的最短路径dist[i][j]
*
* @param pre 上一个点, 初始为0,表示从快递站出发
* @param sum 当前全排列路径累计的路径权重
* @param used 全排列used数组,用于标记哪些点已使用过
* @param permutation 1~x的全排列
*/
public static void dfs(int pre, int sum, boolean[] used, LinkedList<Integer> permutation) {
if (permutation.size() == x) {
// 此时pre是最后一个学生编号,送完最后一个学生后,司机需要回到学校,因此最终累计路径权重为 sum + dist[pre][0]
// 我们保留最小权重路径
int dis = sum + dist[pre][0];
if (dis < minDis) {
minDis = dis;
keyPath = new ArrayList<>(permutation);
}
return;
}
for (int i = 1; i <= x; i++) {
if (used[i]) continue;
used[i] = true;
permutation.add(i);
dfs(i, sum + dist[pre][i], used, permutation);
permutation.removeLast();
used[i] = false;
}
}
}