1 今日打卡
Floyd算法 97. 小明逛公园
A*算法 127. 骑士的攻击
2 Floyd算法
2.1 思路
核心原理:对于任意两个节点 i 和 j,尝试以节点 k 作为中间节点,更新 i 到 j 的最短路径,即 i -> j 的最短路径 = min (原 i->j 路径,i->k->j 路径)。
三层循环逻辑:
外层循环 k:枚举所有可能的中间节点(从 1 到 n);
中层循环 i:枚举所有起点;
内层循环 j:枚举所有终点;
初始化:
自身到自身的距离为 0(代码中未显式写,但可补充);
直接相连的节点距离为输入的边权;
无直接相连的节点距离设为一个极大值(表示不可达)。
2.2 实现代码
java
import java.util.*;
public class Main {
public static void main(String[] args) {
// 1. 输入处理:读取节点数n和边数m
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), m = sc.nextInt(); // n:节点总数,m:边的数量
// 2. 初始化三维数组:grid[i][j][k] 表示经过前k个中间节点后i到j的最短路径
// 维度说明:i(起点)、j(终点)、k(中间节点上限),范围1~n(方便节点编号对应)
int[][][] grid = new int[n + 1][n + 1][n + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
// 初始化所有路径为极大值(10001,需大于可能的最大路径和),表示初始不可达
Arrays.fill(grid[i][j], 10001);
// 补充:自身到自身的距离为0(原代码遗漏,建议添加)
if (i == j) {
grid[i][j][0] = 0;
}
}
}
// 3. 读取边的信息,初始化直接相连的节点路径(k=0表示无中间节点)
for (int i = 0; i < m; i++) {
int src = sc.nextInt(); // 起点
int dst = sc.nextInt(); // 终点
int val = sc.nextInt(); // 边权(路径长度)
// 无向图:双向路径都要赋值
grid[src][dst][0] = val;
grid[dst][src][0] = val;
}
// 4. Floyd核心:动态规划更新最短路径
// k:枚举中间节点(第k个中间节点)
for (int k = 1; k <= n; k++) {
// i:枚举所有起点
for (int i = 1; i <= n; i++) {
// j:枚举所有终点
for (int j = 1; j <= n; j++) {
// 状态转移方程:
// 经过前k个中间节点的i->j路径 = min(不经过k的路径, 经过k的路径(i->k + k->j))
grid[i][j][k] = Math.min(grid[i][j][k - 1], grid[i][k][k - 1] + grid[k][j][k - 1]);
}
}
}
// 5. 处理查询:输出指定起点到终点的最短路径
int q = sc.nextInt(); // 查询次数
while (q > 0) {
int start = sc.nextInt(); // 查询的起点
int end = sc.nextInt(); // 查询的终点
// grid[start][end][n] 表示经过所有n个中间节点后,start到end的最短路径
if (grid[start][end][n] == 10001) {
System.out.println(-1); // 不可达输出-1
} else {
System.out.println(grid[start][end][n]); // 输出最短路径长度
}
q--;
}
sc.close(); // 关闭输入流
}
}
3 A*算法
3.1 思路
A*(A-Star)算法是一种启发式搜索算法,核心是结合「实际代价(G)」和「预估代价(H)」来引导搜索方向,相比普通 BFS 能更快找到最优路径,适用于最短路径问题(如本题的骑士走棋盘):
核心公式:F = G + H
G:从起点到当前节点的实际移动代价(本题中骑士每走一步,G 增加 5,对应 "马走日" 的欧氏距离平方:12+22=5);
H:从当前节点到终点的预估代价(启发函数,本题用欧氏距离平方,避免开根号提升计算效率);
F:总代价,算法优先选择 F 最小的节点扩展,能快速逼近终点。
核心流程:
用优先队列(最小堆)存储待扩展的节点,按 F 值从小到大排序;
每次取出 F 最小的节点,扩展其所有合法邻接节点(骑士的 8 种走法);
记录每个节点的实际移动步数,直到找到终点。
3.2 实现代码
java
import java.util.PriorityQueue;
import java.util.Arrays;
import java.util.Scanner;
// 骑士节点类:存储坐标、G/H/F值
class Knight implements Comparable<Knight> {
int x, y; // 坐标
int g, h, f;// G(实际代价)、H(预估代价)、F(总代价)
// 构造方法
public Knight(int x, int y, int g, int h) {
this.x = x;
this.y = y;
this.g = g;
this.h = h;
this.f = g + h; // F = G + H
}
// 重写比较器:优先队列按F值升序排列(F小的先出队)
@Override
public int compareTo(Knight other) {
return Integer.compare(this.f, other.f);
}
}
public class Main {
// 骑士的8个移动方向(马走日)
private static final int[][] dir = {{-2, -1}, {-2, 1}, {-1, 2}, {1, 2},
{2, 1}, {2, -1}, {1, -2}, {-1, -2}};
// 记录每个位置的移动步数(初始为0表示未访问)
private static int[][] moves;
// 终点坐标(全局变量,方便启发函数调用)
private static int targetX, targetY;
// 启发函数:计算当前节点到终点的欧氏距离平方(不开根号提升效率)
private static int heuristic(int x, int y) {
int dx = x - targetX;
int dy = y - targetY;
return dx * dx + dy * dy;
}
// A* 核心搜索方法
private static void aStar(int startX, int startY) {
// 初始化优先队列(最小堆),按F值升序排列
PriorityQueue<Knight> queue = new PriorityQueue<>();
// 起点入队:G=0(无移动代价),H为起点到终点的预估代价
queue.add(new Knight(startX, startY, 0, heuristic(startX, startY)));
// 起点步数记为0
moves[startX][startY] = 0;
// 开始搜索
while (!queue.isEmpty()) {
// 取出F值最小的节点(当前最优节点)
Knight cur = queue.poll();
int curX = cur.x;
int curY = cur.y;
// 到达终点,直接退出(A*找到的第一条路径就是最短路径)
if (curX == targetX && curY == targetY) {
return;
}
// 遍历8个移动方向
for (int[] d : dir) {
int nextX = curX + d[0];
int nextY = curY + d[1];
// 边界检查:棋盘范围1~1000
if (nextX < 1 || nextX > 1000 || nextY < 1 || nextY > 1000) {
continue;
}
// 未访问过的节点(moves为0表示未访问)
if (moves[nextX][nextY] == 0) {
// 记录步数:当前步数+1(每走一步步数+1)
moves[nextX][nextY] = moves[curX][curY] + 1;
// 计算新节点的G、H、F:G=当前G+5(马走日的距离平方)
int newG = cur.g + 5;
int newH = heuristic(nextX, nextY);
// 新节点入队
queue.add(new Knight(nextX, nextY, newG, newH));
}
}
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(); // 测试用例数
while (n-- > 0) {
int a1 = scanner.nextInt(); // 起点x
int a2 = scanner.nextInt(); // 起点y
targetX = scanner.nextInt();// 终点x
targetY = scanner.nextInt();// 终点y
// 初始化步数数组:1001*1001(适配1~1000的坐标),每次用例重置
moves = new int[1001][1001];
// 若起点=终点,直接输出0(避免不必要的搜索)
if (a1 == targetX && a2 == targetY) {
System.out.println(0);
continue;
}
// 执行A*搜索
aStar(a1, a2);
// 输出终点的步数
System.out.println(moves[targetX][targetY]);
}
scanner.close();
}
}