LeetCode 1091. Shortest Path in Binary Matrix【BFS,A星】中等

news2024/11/27 4:02:51

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你一个 n x n 的二进制矩阵 grid 中,返回矩阵中最短 畅通路径 的长度。如果不存在这样的路径,返回 -1 。

二进制矩阵中的 畅通路径 是一条从 左上角 单元格(即,(0, 0))到 右下角 单元格(即,(n - 1, n - 1))的路径,该路径同时满足下述要求:

  • 路径途经的所有单元格的值都是 0 。
  • 路径中所有相邻的单元格应当在 8 个方向之一 上连通(即,相邻两单元之间彼此不同且共享一条边或者一个角)。

畅通路径的长度 是该路径途经的单元格总数。

示例 1:

输入:grid = [[0,1],[1,0]]
输出:2

示例 2:

输入:grid = [[0,0,0],[1,1,0],[1,1,0]]
输出:4

示例 3:

输入:grid = [[1,0,0],[1,1,0],[1,1,0]]
输出:-1

提示:

  • n == grid.length
  • n == grid[i].length
  • 1 <= n <= 100
  • grid[i][j] 为 0 或 1

解法1 BFS

使用宽度优先算法,对八个方向一层层的搜索,从出发点开始,第一次遍历到终点时经过的那条路径就是最短的路径。因为这条路径没有多绕一个不相关节点,所以它是最短的,也符合题目最短畅通路径。

要注意的是,出发点和目的地都可能是 1 1 1 ,这时直接返回 − 1 -1 1 表示不可通过即可。

class Solution {
    public int shortestPathBinaryMatrix(int[][] grid) {
        int n = grid.length;
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return -1; // 不可能出发或到达
        if (n <= 2) return n; // 1x1 2x2
        var q = new ArrayDeque<Integer>();
        int[][] d = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}}; 
        q.offer(0);
        grid[0][0] = 1;
        int step = 1;
        while (!q.isEmpty()) {
            int size = q.size();
            for (int i = 0; i < size; ++i) {
                int u = q.poll();
                int x = u / n, y = u % n;
                if (x == n - 1 && y == n - 1) return step;
                for (int j = 0; j < 8; ++j) {
                    int tx = x + d[j][0], ty = y + d[j][1];
                    if (tx >= 0 && tx < n && ty >= 0 && ty < n && grid[tx][ty] == 0) {
                        q.offer(tx * n + ty);  
                        grid[tx][ty] = 1;
                    }
                }
            }
            ++step;
        }
        return -1;
    }
}

复杂度分析:

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,为二进制矩阵的大小。
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2)

解法2 A* Search启发式搜索

在说明 A* 算法前,先将上述代码改造成「可以延伸出 A* 算法」的形式:

class Solution {
    static class Node {
        public int x, y;
        public int step;
        public Node(int start, int end, int step) {
            this.x = start;
            this.y = end;
            this.step = step;
        }
    };
    private int[][] d = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
    public int shortestPathBinaryMatrix(int[][] grid) {
        int n = grid.length;
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return -1; // 不可能出发或到达
        if (n <= 2) return n; // 1x1 2x2
        Node node = new Node(0, 0, 2);
        Deque<Node> q = new ArrayDeque();
        q.addLast(node);
        while (!q.isEmpty()) {
            Node cur = q.removeFirst();
            int x = cur.x;
            int y = cur.y;
            int step = cur.step;
            for (int i = 0; i < 8; ++i) {
                int tx = x + d[i][0];
                int ty = y + d[i][1];
                if (tx >= 0 && tx < n && ty >= 0 && ty < n && grid[tx][ty] == 0) {
                    // 找到终点
                    if (tx == n - 1 && ty == n - 1) return step;
                    q.addLast(new Node(tx, ty, step + 1));
                    grid[tx][ty] = 1; // 标记遍历过,避免重复
                }
            } 
        }
        return -1;
    }
}

在这一代码的基础上,稍作修改。A* Search中不再使用简单的队列,而是改用优先队列,将最有前途的点优先弹出查找。相比普通队列按顺序依次弹出,要智能很多,而这个算法的启发式函数很重要,如果没有选好、速度反而会变慢。

这里顺便加了个路径记录,有些题目输出路径可能需要:顺着终点方向,找到它的父亲,再找到父亲的父亲……,如此依次回溯,就能找到从起点到终点的一条最佳路径了。本题可以去掉。

下面使用的启发式函数是欧几里得距离

class Solution {
    static class Node {
        public int x, y;
        public int step;
        public double cost; // 用于启发式函数评估
        public Node(int start, int end, int step) {
            this.x = start;
            this.y = end;
            this.step = step;
        }
        public Node(int start, int end, int step, double cost) {
            this.x = start;
            this.y = end;
            this.step = step;
            this.cost = cost;
        }
        // 用于输出路径
        public Node parent = null;
        public Node getParent() { return parent; }
        public void setParent(Node parent) { this.parent = parent; }
        // 用于优先队列
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Node node = (Node) o;
            return x == node.x && y == node.y;
        }
        @Override
        public int hashCode() { return Objects.hash(x, y); }
    }; 
    private int pathLength(Node node) { // 输出路径的方法
        if (node == null) return 0;
        int pathLength = 1;
        while (node.getParent() != null) {
            node = node.getParent();
            pathLength++;
        }
        return pathLength;
    }
    private int[][] d = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
    public int shortestPathBinaryMatrix(int[][] grid) {
        int n = grid.length;
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return -1; // 不可能出发或到达
        if (n <= 2) return n; // 1x1 2x2
        PriorityQueue<Node> q = new PriorityQueue<>(10, (i, j) -> Double.compare(i.cost, j.cost));
        q.add(new Node(0, 0, 1, 0));
        grid[0][0] = 1;
        while (!q.isEmpty()) {
            Node cur = q.poll();
            int x = cur.x;
            int y = cur.y;
            int step = cur.step;
            if (x == n - 1 && y == n - 1) return step;
            for (int i = 0; i < 8; ++i) {
                int tx = x + d[i][0];
                int ty = y + d[i][1];
                if (tx >= 0 && tx < n && ty >= 0 && ty < n && grid[tx][ty] == 0) {
                    double cost = cur.cost + 1 + distance(tx, ty, x, y); // 之前的距离+1+现在新增的距离
                    Node next = new Node(tx, ty, step + 1, cost);
                    if (q.contains(next)) continue; // 不能重复入队
                    next.setParent(cur); // 保存路径方便后续打印
                    q.add(next);
                    grid[tx][ty] = 1; // 标记遍历过,避免重复
                }
            } 
        }
        return -1;
    }
    public double distance(int x, int y, int tx, int ty) { // 启发式函数,使用两点距离坐标公式
        return Math.sqrt(Math.pow(tx - x, 2) + Math.pow(ty - y, 2));
    } 
}

下面将启发式函数改为切比雪夫距离。 距离计算方法有很多,选择合适的启发式函数有利于速度的提升。这题可以用好几种启发式函数:

  • 曼哈顿距离 Manhattan Distance : 一般只能在四个方向上移动时用(右、左、上、下)

  • 对角线距离 Diagonal Distance : 当我们只允许向八个方向移动时用(国际象棋中的王移动方式那种)

  • 欧几里得距离 Euclidean Distance : 不受限制,允许向任何方向移动时。

  • 切比雪夫距离:对于平面上的两个点 x = ( x 0 , x 1 ) x = (x_0, x_1) x=(x0,x1) y = ( y 0 , y 1 ) y = (y_0, y_1) y=(y0,y1) ,设它们横坐标距离之差为 d x = ∣ x 0 − y 0 ∣ dx = |x_0 - y_0| dx=x0y0 ,纵坐标距离之差为 d y = ∣ x 1 − y 1 ∣ dy = |x_1 - y_1| dy=x1y1 ,对于以下三种情况,我们可以分别计算出从 x x x 移动到 y y y 的最少次数:

    • d x < d y d_x < d_y dx<dy :沿对角线移动 d x d_x dx 次,再竖直移动 d y − d x d_y - d_x dydx 次,总计 d x + ( d y − d x ) = d y d_x + (d_y - d_x) = d_y dx+(dydx)=dy 次;
    • d x = = d y d_x == d_y dx==dy :沿对角线移动 d x d_x dx 次;
    • d x > d y d_x > d_y dx>dy :沿对角线移动 d y d_y dy 次,再水平移动 d x − d y d_x - d_y dxdy 次,总计 d y + ( d x − d y ) = d x d_y + (d_x - d_y) = d_x dy+(dxdy)=dx 次。

    可以发现,对于任意一种情况, x x x 移动到 y y y 的最少次数为 d x d_x dx d y d_y dy 中的较大值 max ⁡ ( d x , d y ) \max(d_x, d_y) max(dx,dy) ,这也被称作 x x x y y y 之间的 切比雪夫距离

class Solution {
    int n = 0;
    public class Node {
        public int x, y; 
        public int cost; // 用于启发式函数评估
        public Node(int start, int end, int step) {
            this.x = start;
            this.y = end;
            this.cost = distance(x, y, step);
        } 
        public int distance(int x, int y, int step) {
            return step + Math.max(n - 1 - x, n - 1 - y); // 已走距离+到目标的切比雪夫距离
        }
    };  
    private int[][] d = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
    public int shortestPathBinaryMatrix(int[][] grid) {
        n = grid.length;
        if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return -1; // 不可能出发或到达
        if (n <= 2) return n; // 1x1 2x2
        PriorityQueue<Node> q = new PriorityQueue<>(Comparator.comparingInt(v -> v.cost)); // 小顶堆 // (a, b) -> a.cost - b.cost
        q.add(new Node(0, 0, 1));
        grid[0][0] = 1;
        while (!q.isEmpty()) {
            Node cur = q.poll();
            int x = cur.x;
            int y = cur.y; 
            if (x == n - 1 && y == n - 1) return grid[x][y];
            for (int i = 0; i < 8; ++i) {
                int tx = x + d[i][0];
                int ty = y + d[i][1]; // 注意判断 grid[tx][ty] > grid[x][y] + 1
                if (tx >= 0 && tx < n && ty >= 0 && ty < n && (
                    grid[tx][ty] == 0 || grid[tx][ty] > grid[x][y] + 1)) {
                    grid[tx][ty] = grid[x][y] + 1; // 标记遍历过,避免重复
                    q.add(new Node(tx, ty, grid[tx][ty]));
                }
            } 
        }
        return -1;
    } 
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/573434.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 代码整洁之道

NOTICE: 这篇文章的框架条目来自《C代码整洁之道&#xff1a;C17可持续软件开发模式实践》&#xff0c;作者: [德] 斯提芬罗特。书籍原名"Clean C: Sustainable Software Development Patterns and Best Practices with C 17"。 文章目录 编码基本原则保持简单和直接…

Unity | HDRP高清渲染管线学习笔记:示例场景解析

目录 一、HDRP入门 1.HDRP设置 1.1 HDRP配置文件中的全部设置项 1.1.1 Rendering下的Lit Shader Mode 1.1.2 Lighting 下的Volumetrics&#xff08;体积光&#xff09;和Screen Space Reflection&#xff08;屏幕空间反射&#xff09; 2.离线渲染VS实时渲染 3.Volume组件 …

文字gif闪图怎么做?高效的gif闪图制作方法

相信不少新媒体行业的小伙伴&#xff0c;一定都见过那种闪动文字效果的gif动图吧。效果非常的炫酷还很吸引人们的眼球&#xff0c;但是作为设计小白这种闪烁gif图要怎么制作呢&#xff1f;有没有那种小白也能轻松上手的工具呢&#xff1f; 一、什么样的工具能够在线生成gif动态…

《Spring Guides系列学习》guide35 - guide40

要想全面快速学习Spring的内容&#xff0c;最好的方法肯定是先去Spring官网去查阅文档&#xff0c;在Spring官网中找到了适合新手了解的官网Guides&#xff0c;一共68篇&#xff0c;打算全部过一遍&#xff0c;能尽量全面的了解Spring框架的每个特性和功能。 接着上篇看过的gu…

项目管理自动化 工作效率顶呱呱

项目管理&#xff0c;是职场人进阶发展的必备高阶能力&#xff0c;需要在复杂的环境中有效整合资源、高效助力团队实现整体的项目目标。 一个好的项目管理者&#xff0c;需要合理规划项目进展&#xff0c;实时同步需求、及时沟通进展&#xff0c;合理判断项目风险&预警&am…

记一次用户反馈app在后台收不到push问题跟踪

我们的应该大范围推广后&#xff0c;今日用户群好多用户反馈安卓手机app在后台时收不到app的push消息&#xff0c;只有app处于前台时才能收到push消息。但是ios手机可以正常接收push消息。 拿到问题&#xff0c;首先想到从下面几个方便尝试定位&#xff1a; 1.用户手机app通知权…

财报解读:毛利持续改善,金山云正在“弯道超车”?

一季度&#xff0c;云巨头们的表现持续稳健&#xff0c;依旧稳坐前排&#xff0c;而作为中小云代表的金山云也在5月23日发布了2023年一季度财报&#xff0c;盈利能力持续改善成为通篇最亮眼的一笔。 随着AI大模型打开了新的“潘多拉魔盒”&#xff0c;云市场也在发生着巨变。 …

picoctf_2018_rop chain

小白垃圾笔记&#xff0c;不建议阅读。 这道题目其实我是瞎做的. 本地调试需要写一个文件名为flag.txt的文件。 先检查下保护&#xff1a;&#xff08;我把文件名改成pwn了&#xff09;&#xff0c;32位仅仅开启了nx 然后放到32位ida里&#xff1a; main函数如下&#xff1a…

〖Web全栈开发⑤〗— CSS基础

〖Web全栈开发⑤〗— CSS基础 (一)CSS基础1.1CSS介绍1.2CSS样式1.3CSS 格式 &#xff08;二&#xff09;CSS 选择器2.1标签选择器2.2类选择器2.3层级选择器2.4id选择器2.5组选择器2.6伪类选择器2.7通配符选择器 &#xff08;三&#xff09;样式表引入3.1外部样式表3.2内部样式表…

WPF入门实例 WPF完整例子 WPF DEMO WPF学习完整例子 WPF实战例子 WPF sql实例应用

WPF 和 WinForms 都是用于创建 Windows 桌面应用程序的开发框架&#xff0c;它们有一些相似之处&#xff0c;但也有很多不同之处。 在开发速度方面&#xff0c;这取决于具体情况。如果您熟悉 WinForms 开发并且正在开发简单的界面应用程序&#xff0c;则可能会比使用 WPF 更快…

《Spring Guides系列学习》guide41 - guide45

要想全面快速学习Spring的内容&#xff0c;最好的方法肯定是先去Spring官网去查阅文档&#xff0c;在Spring官网中找到了适合新手了解的官网Guides&#xff0c;一共68篇&#xff0c;打算全部过一遍&#xff0c;能尽量全面的了解Spring框架的每个特性和功能。 接着上篇看过的gu…

kaggle官方书籍推荐:The-Kaggle-Book

今天介绍一本kaggle出版的竞赛书籍。 这本书结合真实的kaggle竞赛题目&#xff0c;以及它们的冠军团队方案&#xff0c;介绍了参与机器学习竞赛的一些基础知识、经验技巧等。 内容涵盖Kaggle的介绍、建模问题以及技巧、如何利用Kaggle的经历来丰富简历等等。 书籍简介 参加 …

银河麒麟v4.0.2安装

银河麒麟v4.0.2安装 一、下载银河麒麟系统二、制作USB的启动镜像三、安装银河麒麟系统1、设置要被安装的机器bios启动模式为USB启动后&#xff0c;选择第一项&#xff1a;图形安装银河麒麟服务器操作系统2、设置用户和密码&#xff0c;右下角有继续&#xff0c;点击继续下一步3…

Axure教程—水平方向多色图(中继器)

本文将教大家如何用AXURE制作动态水平方向多色图 一、效果介绍 如图&#xff1a; 预览地址&#xff1a;https://l83ucp.axshare.com 下载地址&#xff1a;https://download.csdn.net/download/weixin_43516258/87822666 二、功能介绍 简单填写中继器内容即可生成动态水平多色…

操作系统层面下——进程状态讲解

目录 一.进程的状态&#xff1a;运行态 1.什么是运行状态&#xff1f; 2.进程进入内存的详细图解&#xff1a; 总结&#xff1a; 二.进程的状态&#xff1a;阻塞态 1.什么是阻塞状态&#xff1f; 三.进程的状态&#xff1a;挂起态 1.什么是挂起态&#xff1f; 2.阻塞与挂起的…

xss跨站之代码及http only绕过

什么是http only&#xff0c;在cookie中设置了http only属性&#xff0c;那么通过js代码无法获取cookie&#xff0c;并不能防止xss漏洞&#xff0c;在上一节的靶场网站源代码里面&#xff0c;写上这一串代码就是启动http only 再加上带去cookie的代码 然后我们再去访问网站的后…

线程池各参数学习

线程池学习_alutimo的博客-CSDN博客尚硅谷java并发包JUC线程池部分学习总结https://blog.csdn.net/qq_41228145/article/details/125650075老生常谈 线程池的参数ThreadPoolExecutor.java 的构造器 /*** Creates a new {code ThreadPoolExecutor} with the given initial* par…

PLC【西门子】几种常见的连接口和通讯协议简介

S7-200 PLC支持的几种通讯协议 一、PPI通讯 是西门子公司专为s7-200系列plc开发的通讯协议。内置于s7-200CPU中。PPI协议物理上基于RS-485口,通过屏蔽双绞线就可以实现PPI通讯。PPI协议是一种主-从协议。主站设备发送要求到从站设备,从站设备响应,从站不能主动发出信息。主…

简述什么是微前端 微前端几种框架的区别

微前端就是将各个模块分成不同项目 方便多个团队一起开发互相不影响 例如&#xff1a;a团队维护较老的项目使用angular&#xff0c;b团队开发react&#xff0c;c团队开发vue 。按道理说abc三个项目并没有关联&#xff0c;但是他们又都是公司内部管理的系统。需要集成在一起 &…

智能排班系统 【管理系统功能、操作说明——中篇】

文章目录 页面与功能展示企业管理角色管理用户管理系统管理员身份使用企业管理员身份使用门店管理员身份使用 门店管理职位管理排班规则设置节日管理消息管理 页面与功能展示 企业管理 企业管理页面如图 34所示&#xff0c;在企业管理页面&#xff0c;系统管理员可以查询所注…