|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1036. 逃离大迷宫](https://leetcode-cn.com/problems/escape-a-large-maze/solution/gong-shui-san-xie-bfs-gei-ding-zhang-ai-8w63o/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「几何」、「BFS」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +在一个 $10^6$ x $10^6$ 的网格中,每个网格上方格的坐标为 $(x, y)$ 。 |
| 10 | + |
| 11 | +现在从源方格 $source = [s_x, s_y]$ 开始出发,意图赶往目标方格 $target = [t_x, t_y]$ 。 |
| 12 | + |
| 13 | +数组 $blocked$ 是封锁的方格列表,其中每个 $blocked[i] = [x_i, y_i]$ 表示坐标为 $(x_i, y_i)$ 的方格是禁止通行的。 |
| 14 | + |
| 15 | +每次移动,都可以走到网格中在四个方向上相邻的方格,只要该方格 不 在给出的封锁列表 $blocked$ 上。同时,不允许走出网格。 |
| 16 | + |
| 17 | +只有在可以通过一系列的移动从源方格 $source$ 到达目标方格 $target$ 时才返回 $true$。否则,返回 $false$。 |
| 18 | + |
| 19 | +示例 1: |
| 20 | +``` |
| 21 | +输入:blocked = [[0,1],[1,0]], source = [0,0], target = [0,2] |
| 22 | +
|
| 23 | +输出:false |
| 24 | +
|
| 25 | +解释: |
| 26 | +从源方格无法到达目标方格,因为我们无法在网格中移动。 |
| 27 | +无法向北或者向东移动是因为方格禁止通行。 |
| 28 | +无法向南或者向西移动是因为不能走出网格。 |
| 29 | +``` |
| 30 | +示例 2: |
| 31 | +``` |
| 32 | +输入:blocked = [], source = [0,0], target = [999999,999999] |
| 33 | +
|
| 34 | +输出:true |
| 35 | +
|
| 36 | +解释: |
| 37 | +因为没有方格被封锁,所以一定可以到达目标方格。 |
| 38 | +``` |
| 39 | + |
| 40 | +提示: |
| 41 | +* $0 <= blocked.length <= 200$ |
| 42 | +* $blocked[i].length == 2$ |
| 43 | +* $0 <= xi, yi < 10^6$ |
| 44 | +* $source.length == target.length == 2$ |
| 45 | +* $0 <= sx, sy, tx, ty < 10^6$ |
| 46 | +* $source != target$ |
| 47 | +* 题目数据保证 $source$ 和 $target$ 不在封锁列表内 |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +### BFS + 给定障碍物所能围成的最大面积 |
| 52 | + |
| 53 | +为了方便,我们用 $s$ 代指 $source$,用 $t$ 代指 $target$,用 $n$ 来代指 $blocked$ 大小。 |
| 54 | + |
| 55 | +整理题意为:在一个足够大的空间里,有少数的障碍物,问两点是否连通。 |
| 56 | + |
| 57 | +当两点相隔较远时,常规的 `BFS` 做法可能会搜完整个棋盘,而棋盘大小为 $10^6 * 10^6$,会 `TLE`。 |
| 58 | + |
| 59 | +考虑什么情况下两点会不连通? |
| 60 | + |
| 61 | +当两个点中的任意一点被障碍物围住时,两点将无法连通。 |
| 62 | + |
| 63 | +一个很容易想到的思路是:**从 $s$ 跑一遍 `BFS`,然后从 $t$ 跑一遍 `BFS`,同时设定一个最大访问点数量 `MAX`,若从两者出发能够访问的点数量都能超过 `MAX`,说明两点均没有被围住,最终必然会联通。** |
| 64 | + |
| 65 | +考虑如何敲定 `MAX` 的取值范围?直观感受,`MAX` 应该是一个与 $blocked$ 大小相关的数。 |
| 66 | + |
| 67 | +但第一反应还是想从单秒计算量上界进行反推,两边 `BFS` 的复杂度均为 $O(\max)$,因此直接设定 `MAX = 1e5` 应该是比较合适的。 |
| 68 | + |
| 69 | +更小的 `MAX` 需要证明:**在给定数量障碍物的前提下,障碍物所能围成的最大面积为多少。** |
| 70 | + |
| 71 | +首先,容易想到:**任何一条封闭图形的直边都可以通过调整为斜边来围成更大的面积:** |
| 72 | + |
| 73 | + |
| 74 | + |
| 75 | +**即组成封闭图形的边不可能有直边,同时由于是封闭图形,因此斜边直接必然是单点衔接,而不可能是平行(无法封闭)。** |
| 76 | + |
| 77 | +同时,**想要达到最大面积,应当尽可能利用边界作为围成图形的某些边。** |
| 78 | + |
| 79 | +利用边界所能围成的最大封面图形 **可以是**「由边界提供两边,障碍物提供一边的三角形」。 |
| 80 | + |
| 81 | +如果不是该形状,则可以通过调整障碍物的直边为一条完整的斜边,来组成封闭三角形,围成面积不会变小: |
| 82 | + |
| 83 | + |
| 84 | + |
| 85 | +即给定 $n$ 的情况下,根据「等差数列求和」可知,最大所能围成的面积为 $1 + 2 + ... + n - 1 = \frac{n * (n - 1)}{2}$。 |
| 86 | + |
| 87 | +因此如果从 $s$ 和 $t$ 出发,能够访问的点数超过 $\frac{n * (n - 1)}{2}$ 个,那么两点并没有被围住,必然联通。 |
| 88 | + |
| 89 | +最后,为了在 `BFS` 过程中记录某些点被访问过,可以通过计算某个位置哈希值(数值)来实现。 |
| 90 | + |
| 91 | +**代码(感谢 [@🍭可乐可乐吗QAQ](/u/littletime_cc/) 和 [@Benhao](/u/himymben/) 同学提供的其他语言版本):** |
| 92 | +```Java |
| 93 | +class Solution { |
| 94 | + int EDGE = (int)1e6, MAX = (int)1e5; |
| 95 | + long BASE = 131L; |
| 96 | + Set<Long> set = new HashSet<>(); |
| 97 | + int[][] dir = new int[][]{{1,0},{-1,0},{0,1},{0,-1}}; |
| 98 | + public boolean isEscapePossible(int[][] blocked, int[] s, int[] t) { |
| 99 | + for (int[] p : blocked) set.add(p[0] * BASE + p[1]); |
| 100 | + int n = blocked.length; |
| 101 | + MAX = n * (n - 1) / 2; // 可直接使用 1e5 |
| 102 | + return check(s, t) && check(t, s); |
| 103 | + } |
| 104 | + boolean check(int[] a, int[] b) { |
| 105 | + Set<Long> vis = new HashSet<>(); |
| 106 | + Deque<int[]> d = new ArrayDeque<>(); |
| 107 | + d.addLast(a); |
| 108 | + vis.add(a[0] * BASE + a[1]); |
| 109 | + while (!d.isEmpty() && vis.size() <= MAX) { |
| 110 | + int[] poll = d.pollFirst(); |
| 111 | + int x = poll[0], y = poll[1]; |
| 112 | + if (x == b[0] && y == b[1]) return true; |
| 113 | + for (int[] di : dir) { |
| 114 | + int nx = x + di[0], ny = y + di[1]; |
| 115 | + if (nx < 0 || nx >= EDGE || ny < 0 || ny >= EDGE) continue; |
| 116 | + long hash = nx * BASE + ny; |
| 117 | + if (set.contains(hash)) continue; |
| 118 | + if (vis.contains(hash)) continue; |
| 119 | + d.addLast(new int[]{nx, ny}); |
| 120 | + vis.add(hash); |
| 121 | + } |
| 122 | + } |
| 123 | + return vis.size() > MAX; |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | + |
| 129 | +```C++ |
| 130 | +class Solution { |
| 131 | +public: |
| 132 | + int EDGE = 1e6, MAX = 1e5; |
| 133 | + long long BASE = 13331; |
| 134 | + unordered_set<long long> set; |
| 135 | + int dir[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} }; |
| 136 | + bool isEscapePossible(vector<vector<int>>& blocked, vector<int>& s, vector<int>& t) { |
| 137 | + for(auto& p : blocked) set.insert(p[0] * BASE + p[1]); |
| 138 | + int n = blocked.size(); |
| 139 | + MAX = n * (n - 1) / 2; // 可直接使用 1e5 |
| 140 | + return check(s, t) and check(t, s); |
| 141 | + } |
| 142 | + bool check(vector<int>& a, vector<int>& b){ |
| 143 | + unordered_set<long long> vis; |
| 144 | + queue< pair<int,int> > q; |
| 145 | + q.push( {a[0], a[1]}); |
| 146 | + vis.insert(a[0] * BASE + a[1]); |
| 147 | + while(q.size() and vis.size() <= MAX){ |
| 148 | + auto t = q.front(); |
| 149 | + q.pop(); |
| 150 | + int x = t.first, y = t.second; |
| 151 | + if(x == b[0] and y == b[1]) return true; |
| 152 | + for(int i = 0; i < 4; i++){ |
| 153 | + int nx = x + dir[i][0], ny = y + dir[i][1]; |
| 154 | + if(nx < 0 or nx >= EDGE or ny < 0 or ny >= EDGE) continue; |
| 155 | + if(set.count(nx * BASE + ny)) continue; |
| 156 | + if(vis.count(nx * BASE + ny)) continue; |
| 157 | + q.push( {nx, ny} ); |
| 158 | + vis.insert(nx * BASE + ny); |
| 159 | + } |
| 160 | + } |
| 161 | + return vis.size() > MAX; |
| 162 | + } |
| 163 | +}; |
| 164 | +``` |
| 165 | +
|
| 166 | +
|
| 167 | +```Python |
| 168 | +EDGE, MAX, BASE, DIR = int(1e6), int(1e5), 131, [(1, 0), (-1, 0), (0, 1), (0, -1)] |
| 169 | +class Solution: |
| 170 | + def isEscapePossible(self, blocked: List[List[int]], source: List[int], target: List[int]) -> bool: |
| 171 | + block = {p[0] * BASE + p[1] for p in blocked} |
| 172 | + n = len(blocked) |
| 173 | + MAX = n * (n-1)//2 # 可直接使用 1e5 |
| 174 | + def check(a, b): |
| 175 | + vis = {a[0] * BASE + a[1]} |
| 176 | + d = deque([a]) |
| 177 | + while len(d) and len(vis) <= MAX: |
| 178 | + x, y = d.popleft() |
| 179 | + if x == b[0] and y == b[1]: |
| 180 | + return True |
| 181 | + for dx, dy in DIR: |
| 182 | + nx, ny = x + dx, y + dy |
| 183 | + if nx < 0 or nx >= EDGE or ny < 0 or ny >= EDGE: |
| 184 | + continue |
| 185 | + h = nx * BASE + ny |
| 186 | + if h in block or h in vis: |
| 187 | + continue |
| 188 | + d.append((nx, ny)) |
| 189 | + vis.add(h) |
| 190 | + return len(vis) > MAX |
| 191 | + return check(source, target) and check(target, source) |
| 192 | +``` |
| 193 | +* 时间复杂度:令 $n$ 为 $blocked$ 大小,两次 `BFS` 的最大访问点数为 $\frac{n * (n - 1)}{2}$。整体复杂度为 $O(n^2)$ |
| 194 | +* 空间复杂度:两次 `BFS` 的最大访问点数为 $\frac{n * (n - 1)}{2}$。整体复杂度为 $O(n^2)$ |
| 195 | + |
| 196 | +--- |
| 197 | + |
| 198 | +### 最后 |
| 199 | + |
| 200 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1036` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 201 | + |
| 202 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 203 | + |
| 204 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 205 | + |
| 206 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 207 | + |
0 commit comments