- BFS
- Dijkstra
- Best-First
- A*
- Floyd
- Bellman-Ford
- SPFA
广度优先遍历
由 Edward F. Moore 在 1950 年发表,起初被用于在迷宫中寻找最短路径。在 Prim 最小生成树算法和 Dijkstra 单源最短路径算法中,都采用了与广度优先搜索类似的思想。BFS是最短路径算法里面最简单的,也是后面其他的基础。它是在所有方向上均等地探索。这是一个非常有用的算法,不仅适用于常规寻路,还适用于程序地图生成、流场寻路、距离地图和其他类型的地图分析。
原理
广度优先遍历就是你找一个点,然后匀速的向各个方向扩展
时间复杂度O(V+E),V 为顶点个数,E 为边的条数
空间复杂度O(V),V 为顶点个数,也就是我们每次遍历需要储存下一次要遍历的点
代码
一般情况下,我们使用广度优先遍历的时候都会用到队列,每次遍历一遍就把下一次可以遍历的点加入队尾
为了防止当前取出元素的时候取到下一次的元素(因为每次都往队尾加元素,如果不需要统计广度的次数就可以不理),那么我们可以在进行一次扩散时先获取当前需要遍历的元素个数,也就是当前队列里面剩下的元素个数
733. 图像渲染
class Solution {
int[] xn = {-1, 1, 0, 0};
int[] yn = {0, 0, -1, 1};
public int[][] floodFill(int[][] image, int sr, int sc, int color) {
// 队列,用于广度遍历的时候记录每一次扩展到的点
Deque<int[]> deque = new LinkedList<>();
deque.add(new int[]{sr, sc});
// 记录遍历过的点
boolean[][] visited = new boolean[image.length][image[0].length];
visited[sr][sc] = true;
// 起始点
int start = image[sr][sc];
image[sr][sc] = color;
// 开始广度遍历
while (!deque.isEmpty()){
int size = deque.size();
for (int i = 0; i < size; i++) {
int[] poll = deque.poll();
int x = poll[0];
int y = poll[1];
for (int j = 0; j < 4; j++) {
// 判断---如果不在图内、不等于起始值、访问过,都需要排除
if (x+xn[j] >= 0 && x+xn[j] < image.length && y+yn[j] >= 0 && y+yn[j] < image[0].length && image[x+xn[j]][y+yn[j]] == start && !visited[x+xn[j]][y+yn[j]]){
image[x+xn[j]][y+yn[j]] = color;
visited[x+xn[j]][y+yn[j]] = true;
deque.add(new int[]{x+xn[j], y+yn[j]});
}
}
}
}
return image;
}
}
Dijkstra
上面的广度优先遍历有点贪心的味道,不管三七二十一就是一股脑的往外蔓延,但是如果每一条路都有价值,并且我们需要找到两个点之间最低价值的路径呢?这个时候广度似乎就没办法解决了,所以我们引入了Dijkstra算法
Dijkstra算法是由计算机科学家Edsger W. Dijkstra在1956年提出的
Dijkstra算法用来寻找图形中节点之间的最短路径
原理
在大学我们就学过这个算法,想像一下,你现在需要求出点0到各个点的最短路径,你是不是要考虑到他们的路径价值,而Dijkstra算法就是解决这种问题的 --- 单点最短路径问题
本质其实就是贪心,每次取距离0最短,并且没有遍历过的点,从0开始
- 每次我们取距离0最近并且没有取过的点(后面就知道,取过的点已经是求出了最短距离)
- 取出这个点后,我们更新和这个点相连的点的距离,比如取出的是1,0到1的距离是2,3又和1相连,并且距离是5,那么0到3的距离就是7,现在1已经被选择过了,我们假设现在满足条件的是3,他和1相连,那么0进过3到1的距离肯定比之前长,因为3是从1更新过来的,而且取1的时候他是距离0最小,并且没有被遍历过的(也就是后续取的点肯定比当前0到1的距离长,这里不考虑负数距离),这就是为什么被选中过的点一定已经确定了0到它的最短距离
- 重复1、2这两个过程,直到所有点都被取过,那么就完成了整个算法的过程(如果只需要判断0到某一个点的最短距离/路径,那么只要那个点被取出过后就不需要继续往下走了)
不考虑堆优化的情况下(邻接表实现)
时间复杂度为O(n^2+m)
空间复杂度为O(m)
考虑堆优化的情况下(当图为稠密图时,堆优化反而时间更差,主要是边太多,那么放进优先队列里面重复的点就越多(有时候不得不放,因为你要考虑相同的点可能更优秀,距离更短))
时间复杂度为O((v+e)*loge)
空间复杂度为O(v+e)
推荐看一下这个网站的图文介绍,介绍了整个过程,很生动形象
www.freecodecamp.org/chinese/new…
743. 网络延迟时间
代码
class Solution {
class Node{
int value;
int power;
public Node(int value, int power) {
this.value = value;
this.power = power;
}
}
Map<Integer, List<Node>> map = new HashMap<>();
public int networkDelayTime(int[][] times, int n, int k) {
for (int[] time : times) {
map.computeIfAbsent(time[0]-1, c->new ArrayList<>()).add(new Node(time[1]-1, time[2]));
}
int[] paths = Dijkstra(k - 1, n);
int res = -1;
for (int i = 0; i < n; i++) {
res = Math.max(res, paths[i]);
}
return res == Integer.MAX_VALUE ? -1 : res;
}
public int[] Dijkstra(int source, int n){
int[] paths = new int[n];
Arrays.fill(paths, Integer.MAX_VALUE);
paths[source] = 0;
PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(a -> a.power));
queue.add(new Node(source, 0));
boolean[] visited = new boolean[n];
while (!queue.isEmpty()){
Node poll = queue.poll();
if (visited[poll.value]) continue;
visited[poll.value] = true;
List<Node> list = map.getOrDefault(poll.value, new ArrayList<>());
for (Node node : list) {
if (visited[node.value]) continue;
if (node.power + poll.power < paths[node.value]){
queue.add(new Node(node.value, node.power+poll.power));
paths[node.value] = node.power+poll.power;
}
}
}
return paths;
}
}
Best-First
最佳优先算法,意思就是把Dijkstra中选点的方法改成选取距离终点最近的点(曼哈顿距离)
原理
默认选择距离终点直线最近的点,这种算法也是贪心的一种。
这种说法在有障碍物的情况下是不能用的,因为他给出的答案往往不是最短距离,比如用Dijkstra和他对比
很明显图一才是最佳的路径
这里很明显只能用于无障碍物,所以
时间复杂度:O(2n)
空间复杂度:O(1)
代码
无
A*
A*搜索算法发明者是Nils John Nilsson博士,它是一种启发式搜索算法,可以用来找到 A 点到 B 点之间的最短路径。常用于游戏中的角色的移动,或线上游戏中BOT的移动。可以找到一条最短路径,也可以像宽度搜索算法一样,进行启发式的搜索。
原理
A*算法就是Dijkstra的扩展,选择哪一个点时是使用估计函数 f(n) = g(n)+h(n)
- f(n):其中f(n)越小,优先级越高
- g(n):g(n)是从起点走到这个点消耗了多少步
- h(n):h(n)是当前点到终点的预计代价,也叫做启发函数,他是当前点到终点的距离函数,可以类比为当前点到终点还需要多少步,但是由于可能会有障碍物什么的,而且我们无法准确的估计出当前点到终点还需要多少步,所以这个函数如果选择得好,在特定的情景下,算法的速度可能会有很大的提升
这是Dijkstra、Best-First、A算法的比较,我们可以发现,A算法在准确度和时间上都是最快的,这里结合了其他两个算法的优点,首先如果 h(n) 始终为0,我们发现我们就只考虑起点走到当前点的最短距离,这和Dijkstra是一样的,但是如果我们考虑h(n),也就是当前点到终点的预估距离,那么选择当前点的时候我们就可以选择更加优秀的点,比如当前 h(n) 选择的算法是曼哈顿距离,也就是两点之间的直线距离,这也是 Best-First 使用的算法,当然我们也可以选择其他的,这就要看使用者能不能根据具体情况选择最佳 h(n) 函数了
推荐一个很好玩的网站,他使用了可拖动的动画展示了上面几个算法的过程,最终总结了A*算法
www.redblobgames.com/pathfinding…
代码
773. 滑动谜题
class Solution {
int n;
int m;
int[] xn = {-1, 1, 0, 0};
int[] yn = {0, 0, -1, 1};
int[][] sites = {{1, 2}, {0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}};
class Node{
String str;
int x = 0;
int y = 0;
int value;
public Node(String str, int x, int y, int value) {
this.str = str;
this.x = x;
this.y = y;
this.value = value;
}
}
public int slidingPuzzle(int[][] board) {
StringBuilder source = new StringBuilder("");
String target = "123450";
int tx = 0;
int ty = 0;
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (board[i][j] == 0) {
tx = i;
ty = j;
}
source.append(board[i][j]);
}
}
if (!check(source)) return -1;
Map<String, Integer> map = new HashMap<>();
n = board.length;
m = board[0].length;
PriorityQueue<Node> queue = new PriorityQueue<>((a, b) -> a.value-b.value);
queue.add(new Node(source.toString(), tx, ty,calculate(source)));
map.put(source.toString(), 0);
while (!queue.isEmpty()){
Node poll = queue.poll();
int step = map.getOrDefault(poll.str, 0);
if (poll.str.equals(target)) return step;
for (int i = 0; i < 4; i++) {
int newX = poll.x+xn[i];
int newY = poll.y+yn[i];
if (newX >= 0 && newX < n && newY >= 0 && newY < m){
StringBuilder stringBuilder = new StringBuilder(poll.str);
update(stringBuilder, poll.x*m+poll.y, newX*m+newY);
if (map.containsKey(stringBuilder.toString())) continue;
String s = stringBuilder.toString();
queue.add(new Node(s, newX, newY, calculate(stringBuilder)+step));
map.put(s, step+1);
}
}
}
return -1;
}
public int calculate(StringBuilder str){
int sum = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '0') continue;
sum += Math.abs(i/3-sites[str.charAt(i)-'0'][0])+Math.abs(i%3-sites[str.charAt(i)-'0'][1]);
}
return sum;
}
public void update(StringBuilder str, int x, int y){
char c1 = str.charAt(x);
char c2 = str.charAt(y);
str.setCharAt(x, c2);
str.setCharAt(y, c1);
}
boolean check(StringBuilder cs) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < n * m; i++) {
if (cs.charAt(i) != '0') list.add(cs.charAt(i), - '0');
}
int cnt = 0;
for (int i = 0; i < list.size(); i++) {
for (int j = i + 1; j < list.size(); j++) {
if (list.get(i) > list.get(j)) cnt++;
}
}
return cnt % 2 == 0;
}
}
Floyd
该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名,这个算法主要就是寻找每一个点到其他所有点的最短路径
原理
对于每个顶点v,和任一项顶点对 (i, j), i != j, v != j, 如果 A[i][j] > A[i][v] + A[v][j],则将 A[i][j] 更新为 A[i][v] + A[v][j] 的值,并且将 Path[i][j] 改为v
这句话主要就是找到中间点,然后判断两个点之间如果经过这个中间点会不会有更短的路径
只需要遵循这句话写代码即可,代码非常简单,三个for循环
- 第一个for循环,遍历0~n-1,得到 v(选取中间值)
- 第二个for循环,遍历0~n-1,得到 i,i != v
- 第三个for循环,遍历0~n-1,得到 j,j != v,判断是否满足 A[i][j] > A[i][v] + A[v][j],如果满足则更新 A[i][j] = A[i][v] + A[v][j]
时间复杂度为O(n3)
空间复杂度为O(n2)
参考:www.bilibili.com/video/BV1LE…
代码
743. 网络延迟时间
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
int[][] distance = new int[n][n];
int[][] path = new int[n][n];
for (int i = 0; i < n; i++) {
Arrays.fill(distance[i], 1000000);
Arrays.fill(path[i], -1);
}
for (int i = 0; i < n; i++) {
distance[i][i] = 0;
}
for (int[] time : times) {
distance[time[0]-1][time[1]-1] = time[2];
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j) continue;
for (int l = 0; l < n; l++) {
if (i == l) continue;
if (distance[j][l] > distance[j][i] + distance[i][l]){
distance[j][l] = distance[j][i] + distance[i][l];
path[j][l] = i;
}
}
}
}
int max = -1;
for (int j = 0; j < n; j++) {
if (distance[k-1][j] == 1000000){
return -1;
}
max = Math.max(max, distance[k-1][j]);
}
return max;
}
}
Bellman-Ford
贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼(Richard Bellman) 和莱斯特·福特创立的,求解单源最短路径问题的一种算法,该算法允许有负权的存在
原理
这个算法有一个叫做松弛操作,它是针对边而言的,本意就是 (0) --7--> (∞),那么对这条边进行松弛,则无穷会被更新为7,因为源节点到上一个节点的距离是0,这条边又需要7的距离,那么源节点到下一个节点的距离就是7,那么结果就是(0) --7-->(7)
我们需要遍历每一条边,并更新这条边中的目标点,既如果(target) > (边长)+(source),那么(target) = (边长)+(source),遍历的次数为V次,既节点个数,为什么呢?
我们可以考虑这样一种极端情况,每次我们都从最后一条边开始更新,这样就必须更新 n-1 次才可以完成所有节点的更新
上面遍历完后,我们可以考虑在单独进行一次松弛操作,如果可以进行,说明存在负环,对于这个环,我们无法找到最短路径
时间复杂度O(V*E)
空间复杂度O(V*V)
推荐一个动画演示的视频
代码
这个代码多进行一次松弛操作,因为这道题不存在负权,当然要进行也行,也就是把上面循环的操作在复制一份在下面进行一次即可(不需要循环执行,只需要循环一遍)
743. 网络延迟时间
class Solution {
class Node{
int source;
int target;
int value;
public Node(int source, int target, int value) {
this.source = source;
this.target = target;
this.value = value;
}
}
public int networkDelayTime(int[][] times, int n, int k) {
int[] path = new int[n];
ArrayList<Node> list = new ArrayList<>();
for (int[] time : times) {
list.add(new Node(time[0]-1, time[1]-1, time[2]));
}
Arrays.fill(path, 1000000);
path[k-1] = 0;
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < list.size(); j++) {
if (path[list.get(j).target] > path[list.get(j).source]+list.get(j).value){
path[list.get(j).target] = path[list.get(j).source]+list.get(j).value;
}
}
}
int max = -1;
for (int i = 0; i < n; i++) {
if (path[i] == 1000000){
return -1;
}
max = Math.max(max, path[i]);
}
return max;
}
}
SPFA
针对Bellman-Ford算法最坏的情况,我们可以发现时间复杂度很高O(V*E),主要是没办法直接找到可以松弛的最合适的点,这里我们可以使用队列优化(不需要优先队列)
原理
在Bellman-Ford算法中,我们把for循环改成队列的方式
初始时,我们把源点放入队列中,然后遍历这个队列的边,并且进行松弛操作,如果可以松弛,那么我们就把它放入到队尾,这样每次我们取到的点绝对是之前松弛过过的点,大概率它连接的边也是可以松弛其他点的
比如还是这个图,我们0先入队,然后取出来它,遍历它的边,更新0 --1-->∞为0 --1-->1,然后这个1的点入队,这样整体下来只需要遍历一次。
当然上面这个例子是最好的情况,如果存在负环,那么他可能会退变为Bellman-Ford,但是总体来说还是优于它的,毕竟Bellman-Ford无论最好还是最坏,每次都要判断全部边,因为他不知道自己是否找到了所有可更新的边
代码
743. 网络延迟时间
class Solution {
class Node{
int target;
int value;
public Node(int target, int value) {
this.target = target;
this.value = value;
}
}
public int networkDelayTime(int[][] times, int n, int k) {
int[] path = new int[n];
Map<Integer, List<Node>> map = new HashMap<>();
for (int[] time : times) {
map.computeIfAbsent(time[0]-1,t->new ArrayList<>()).add(new Node(time[1]-1, time[2]));
}
Arrays.fill(path, 1000000);
path[k-1] = 0;
Deque<Integer> deque = new LinkedList<>();
deque.add(k-1);
while (!deque.isEmpty()){
Integer poll = deque.poll();
List<Node> list = map.getOrDefault(poll, new ArrayList<>());
for (Node node : list) {
if (path[node.target] > path[poll]+node.value){
path[node.target] = path[poll]+node.value;
deque.add(node.target);
}
}
}
int max = -1;
for (int i = 0; i < n; i++) {
if (path[i] == 1000000){
return -1;
}
max = Math.max(max, path[i]);
}
return max;
}
}