【力扣时间】【1036】【困难】逃离大迷宫

困难题来了!

1、读题

题目

今天的困难题属于从来没有接触过的类型。
虽然解法还是基于bfs的,但思路上则是完全木有的船新体验。

2、审题

题目属于比较简洁的,而且类型属于图的遍历。
让你在两点之间寻找是否能连通的题,之前貌似也接触过。
辣么难点在哪呢?

一个 106 x 106 的网格中
这个图太大了。
在这个范围内无限制地使用dfsbfs遍历无疑问都是超时的。

辣么怎么判断两个点是否能联通呢?
不急,先说说题目给的些许信息吧。

  1. 首先自然是图的边界:106,这个量级对于图来说不可谓不大了。
  2. 0 <= blocked.length <= 200,看似平平无奇的条件,却是此题的突破点。
  3. 起点source或是终点target都不会出现在blocked
  4. 路径仅能从上下左右四个方向延伸。然后当然,不能越界。

3、思路

在整个思路历程中,我的思路大概变了三次。 并且,我也很惊讶我能想到最后也是最终我做出来的这种思路。

首先,当然是朴素的暴力搜索了,由于范围过大,我企图用dfs+优化剪枝来实现。
但最终,全都是超时。

当暴力的搜索毫无建树的时候,我开始注意到了blocked的长度:0 <= blocked.length <= 200
就算是最长的200个点,在106 x 106的图中也显得过于渺小了。
你很容易就能发现,就算是200个点完全相连形成一条直线,也无法横贯整个图。
这意味着若想要在图中阻隔两个点,辣么这条封锁线,将其中一点成一个圈,或是依靠边界成一个圈。

但是,就算判断一些点能否收尾相连很容易,我也不清楚如何判断起点和终点是否在这个包围圈内。
于是这个思路也很快夭折了。

但新的思路又很快在此至上诞生。

即是围棋

稍微接触过一些围棋的人都知道,一个孤零零的棋子有四个,即是其上下左右。当其上下左右都被包围时,这个子就气绝了。
而相连的同色棋子是共用气的,棋子连得越多,气也会相对增加。
于是我想到,若blocked形成的圈,能包围住某一个点,则必定会限制住这个点延伸出来的一系列点的
而完美情况下,blocked的长度,就是这一系列点的的数量。
反过来想,如果某一点延伸出来的一系列点的的数量超过了blocked的长度,辣么blocked就绝对无法限制这个点向外延伸,也无法限制它连通到终点了。

思路成形,动手吧。

4、开工!

class Solution {

    private static Integer BOARD = 1000000;
    
    public boolean isEscapePossible(int[][] blocked, int[] source, int[] target) {
        Set<Point> walls = new HashSet<>();

        for (int[] p : blocked) {
            walls.add(new Point(p[0], p[1]));
        }

        //无任何阻隔时,一定可以到达
        if (walls.isEmpty()) {
            return true;
        }

        Point s = new Point(source[0], source[1]);
        Point t = new Point(target[0], target[1]);

        return !isSurrounded(walls, blocked.length, s, t)
                && !isSurrounded(walls, blocked.length, t, s);
    }

    private boolean isSurrounded(Set<Point> blocked, int blockSize, Point point, Point target) {
        Set<Point> visited = new HashSet<>();

        Deque<Point> air = new ArrayDeque<>();
        air.add(point);

        while (!air.isEmpty() && air.size() <= blockSize) {
            //取出一个气,并判断是否能继续延伸
            Point current = air.poll();

            //标记当前点已访问
            visited.add(current);

            //抵达目标点时
            if (current.equals(target)) {
                return false;
            }

            //向四周延伸气,但是要判断是否可以算入气内
            Point left = new Point(current.x - 1, current.y);
            if (!air.contains(left) && canSpread(blocked, left, visited)) {
                air.add(left);
            }
            Point right = new Point(current.x + 1, current.y);
            if (!air.contains(right) && canSpread(blocked, right, visited)) {
                air.add(right);
            }
            Point top = new Point(current.x, current.y + 1);
            if (!air.contains(top) && canSpread(blocked, top, visited)) {
                air.add(top);
            }
            Point bottom = new Point(current.x, current.y - 1);
            if (!air.contains(bottom) && canSpread(blocked, bottom, visited)) {
                air.add(bottom);
            }
        }

        //判断气的大小是否超过blocked的长度
        return air.size() <= blockSize;
    }

    private boolean canSpread(Set<Point> blocked, Point point, Set<Point> visit) {
        //已访问
        if (visit.contains(point)) {
            return false;
        }
        //越界
        if (point.x < 0 || point.x >= BOARD
                || point.y < 0 || point.y >= BOARD) {
            return false;
        }
        //碰壁
        if (blocked.contains(point)) {
            return false;
        }
        return true;
    }

    class Point {

        int x;

        int y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            } else if (!(o instanceof Solution.Point)) {
                return false;
            } else {
                Solution.Point other = (Solution.Point) o;
                if (!other.canEqual(this)) {
                    return false;
                } else if (this.x != other.x) {
                    return false;
                } else {
                    return this.y == other.y;
                }
            }
        }

        protected boolean canEqual(Object other) {
            return other instanceof Solution.Point;
        }

        @Override
        public int hashCode() {
            int resultx = 1;
            int result = resultx * 59 + this.x;
            result = result * 59 + this.y;
            return result;
        }
    }

5、解读

大家可以发现我实现了一个叫做Point的类,其作用是为了记录每一个点是否有访问,也方便记录延伸出去的。为此,我还专门有请lambok 实现了它的equals()和hashCode()方法……
在结合了大牛们的做法后,我发现这确实多此一举了。

这次,除了主函数外我还实现了另外两个方法。
其一是canSpread(),其作用是判断某一点能否访问。
限制无非是是否越界是否已访问是否遇到blocked

private boolean canSpread(Set<Point> blocked, Point point, Set<Point> visit) {
        //已访问
        if (visit.contains(point)) {
            return false;
        }
        //越界
        if (point.x < 0 || point.x >= BOARD
                || point.y < 0 || point.y >= BOARD) {
            return false;
        }
        //碰壁
        if (blocked.contains(point)) {
            return false;
        }
        return true;
    }

然后是isSurrounded()方法,其作用是从某一点出发作bfs遍历,并判断其延伸的的数量是否超过了blocked点的数量。

队列的数量即为的数量,而延伸的起点也是队列中的点。
在围棋中想连接一系列点,则会占用其一个气,但同时,自身也将气延续了出去。

private boolean isSurrounded(Set<Point> blocked, int blockSize, Point point, Point target) {
        Set<Point> visited = new HashSet<>();

        Deque<Point> air = new ArrayDeque<>();
        air.add(point);

        while (!air.isEmpty() && air.size() <= blockSize) {
            //取出一个气,并判断是否能继续延伸
            Point current = air.poll();

            //标记当前点已访问
            visited.add(current);

            //抵达目标点时
            if (current.equals(target)) {
                return false;
            }

            //向四周延伸气,但是要判断是否可以算入气内
            Point left = new Point(current.x - 1, current.y);
            if (!air.contains(left) && canSpread(blocked, left, visited)) {
                air.add(left);
            }
            Point right = new Point(current.x + 1, current.y);
            if (!air.contains(right) && canSpread(blocked, right, visited)) {
                air.add(right);
            }
            Point top = new Point(current.x, current.y + 1);
            if (!air.contains(top) && canSpread(blocked, top, visited)) {
                air.add(top);
            }
            Point bottom = new Point(current.x, current.y - 1);
            if (!air.contains(bottom) && canSpread(blocked, bottom, visited)) {
                air.add(bottom);
            }
        }

        //判断气的大小是否超过blocked的长度
        return air.size() <= blockSize;
    }

当然,还有一种情况是sourcetarget都在包围圈内,在bfs遍历中便相遇后,则可以快速剪枝了。
这就是为什么我在isSurrounded()方法中将两个点都传入了。

//抵达目标点时
            if (current.equals(target)) {
                return false;
            }

最后是主函数,其逻辑反而没有isSurrounded()复杂。
主要是将blocked数组转化为所有的点。然后从sourcetarget两个点分别发起bfs遍历。
由于blocked只要将sourcetarget中任一一点包围,则两点必然无法连通,所以得全都进行一次bfs。

当然 ,如果两点都在一个圈内的话,后续的遍历就属于多余的了。 但我没有进行这段剪枝,懒得了。不想破坏这么完整的逻辑。

6、提交

在这里插入图片描述
困难题,做出来就好,不多求了……

时间复杂度和空间复杂度都不会分析,直接看大牛的吧……

7、学习大牛们

我的思路与这位大牛的一致。
不过就我之前说的,看了大牛们将二维转化为一维的方法后,我通过定义类来实现的做法就真的显得弱智了。

以及,这位大牛提供了java中耗时排名100%的解法,但老实说……让我融会贯通还需要些时间。

8、总结

虽然我的解法并不算优质,但在整个题解过程中的思路变化,确认让我自己也有些惊讶。
就方法是超越了自我一样的进化,这就是这些天来刷题的收获吗?终于能有些反馈了吗?

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值