|
| 1 | +## 题目地址(1871. 跳跃游戏 VII) |
| 2 | + |
| 3 | +https://leetcode-cn.com/problems/jump-game-vii/ |
| 4 | + |
| 5 | +## 题目描述 |
| 6 | + |
| 7 | +``` |
| 8 | +给你一个下标从 0 开始的二进制字符串 s 和两个整数 minJump 和 maxJump 。一开始,你在下标 0 处,且该位置的值一定为 '0' 。当同时满足如下条件时,你可以从下标 i 移动到下标 j 处: |
| 9 | +
|
| 10 | +i + minJump <= j <= min(i + maxJump, s.length - 1) 且 |
| 11 | +s[j] == '0'. |
| 12 | +
|
| 13 | +如果你可以到达 s 的下标 s.length - 1 处,请你返回 true ,否则返回 false 。 |
| 14 | +
|
| 15 | + |
| 16 | +
|
| 17 | +示例 1: |
| 18 | +
|
| 19 | +输入:s = "011010", minJump = 2, maxJump = 3 |
| 20 | +输出:true |
| 21 | +解释: |
| 22 | +第一步,从下标 0 移动到下标 3 。 |
| 23 | +第二步,从下标 3 移动到下标 5 。 |
| 24 | +
|
| 25 | +
|
| 26 | +示例 2: |
| 27 | +
|
| 28 | +输入:s = "01101110", minJump = 2, maxJump = 3 |
| 29 | +输出:false |
| 30 | +
|
| 31 | +
|
| 32 | + |
| 33 | +
|
| 34 | +提示: |
| 35 | +
|
| 36 | +2 <= s.length <= 105 |
| 37 | +s[i] 要么是 '0' ,要么是 '1' |
| 38 | +s[0] == '0' |
| 39 | +1 <= minJump <= maxJump < s.length |
| 40 | +``` |
| 41 | + |
| 42 | +## 前置知识 |
| 43 | + |
| 44 | +- BFS |
| 45 | +- 动态规划 |
| 46 | +- 前缀和 |
| 47 | + |
| 48 | +## 公司 |
| 49 | + |
| 50 | +- 暂无 |
| 51 | + |
| 52 | +## BFS(超时) |
| 53 | + |
| 54 | +### 思路 |
| 55 | + |
| 56 | +我们可以对问题进行抽象。将 0 抽象为图中的点,将每一个 0 与**其能够到达的比它索引大的 0**抽象为边。那么问题转化为从索引为 0 的点与索引为 n - 1 的点**是否联通**。我们有很多方法能够解决这个问题,不妨使用 BFS。 |
| 57 | + |
| 58 | +### 关键点 |
| 59 | + |
| 60 | +- 将题目抽象为图的联通问题 |
| 61 | + |
| 62 | +### 代码 |
| 63 | + |
| 64 | +- 语言支持:Python3 |
| 65 | + |
| 66 | +Python3 Code: |
| 67 | + |
| 68 | +```python |
| 69 | + |
| 70 | +class Solution: |
| 71 | + def canReach(self, s: str, minJump: int, maxJump: int) -> bool: |
| 72 | + if s[-1] == '1': return False |
| 73 | + zeroes = set([i for i in range(len(s)) if s[i] == '0']) |
| 74 | + q = set([0]) |
| 75 | + while q: |
| 76 | + cur = q.pop() |
| 77 | + if cur == len(s) - 1: return True |
| 78 | + for nxt in range(cur + minJump, min(cur + maxJump, len(s)) + 1): |
| 79 | + if nxt in zeroes and nxt not in q: |
| 80 | + q.add(nxt) |
| 81 | + return False |
| 82 | + |
| 83 | +``` |
| 84 | + |
| 85 | +**复杂度分析** |
| 86 | + |
| 87 | +令 n 为数组长度。 |
| 88 | + |
| 89 | +- 时间复杂度:$O(n^2)$ |
| 90 | +- 空间复杂度:$O(n)$ |
| 91 | + |
| 92 | +## 动态规划 |
| 93 | + |
| 94 | +### 思路 |
| 95 | + |
| 96 | +定义 dp(i) 为从索引为 0 的点是否能够到达索引为 i 的点。显然 dp(i) 只有在满足以下两个条件才为 true: |
| 97 | + |
| 98 | +- s[i] == '0' |
| 99 | +- s[j] == '0' 其中 max(0, i - maxJump) <= j <= max(0, i - minJump) |
| 100 | + |
| 101 | +于是,我们可以枚举所有满足条件的 j ,并观察其是否满足上述条件即可。 |
| 102 | + |
| 103 | +代码: |
| 104 | + |
| 105 | +```py |
| 106 | +class Solution: |
| 107 | + def canReach(self, s: str, minJump: int, maxJump: int) -> bool: |
| 108 | + def dp(pos): |
| 109 | + if pos == len(s) - 1: return True |
| 110 | + return s[pos] == '0' and any([dp(i) for i in range(pos + minJump, min(len(s), pos + maxJump + 1))]) |
| 111 | + if s[-1] == '1': return False |
| 112 | + return dp(0) |
| 113 | +``` |
| 114 | + |
| 115 | +由于枚举 i 和 j 的复杂度为都为 $O(n)$,因此总的时间复杂度为 $O(n^2)$,代入题目会超时。 |
| 116 | + |
| 117 | +我们需要对其进行优化。我们发现算法条件在于寻找满足条件的 j,而满足条件的 j 实际上就是区间[max(0,i-maxJump), max(0, i-minJump)] **中可以从 0 点到达的点**。 |
| 118 | + |
| 119 | +换句话说就是 **dp 数组的区间 [max(0,i-maxJump), max(0, i-minJump)] 中是否存在一个 true**。 |
| 120 | + |
| 121 | +那么这该如何求呢?我举个例子你就懂了。 |
| 122 | + |
| 123 | +比如一个数组 [false, true, false, false, true],我想知道区间 [2,3] 是否有 true。 |
| 124 | + |
| 125 | +朴素的做法是遍历: |
| 126 | + |
| 127 | +```js |
| 128 | +bools = [false, true, false, false, true]; |
| 129 | +bools[2] || bools[3]; |
| 130 | +``` |
| 131 | + |
| 132 | +如果我想知道任意合法区间 [s,e] 是否有 true。 |
| 133 | + |
| 134 | +则可以: |
| 135 | + |
| 136 | +```js |
| 137 | +bools = [false, true, false, false, true] |
| 138 | +for(let i = s; i < min(e,len(bools)); i++) { |
| 139 | + if bools[i]: return true |
| 140 | +} |
| 141 | +return false |
| 142 | + |
| 143 | +``` |
| 144 | + |
| 145 | +实际上,我们有可以将 bools 映射到整数,其中 false 映射为 0,true 映射为 1,并对 bools 求前缀和。这样就可以通过前缀和在 $O(1)$ 时间获取到任意区间的 true 的个数。 |
| 146 | + |
| 147 | +代码: |
| 148 | + |
| 149 | +```js |
| 150 | +bools = [false, true, false, false, true]; |
| 151 | +// bools 映射为 [0,1,0,0,1] |
| 152 | +// pres 为 [0,1,1,1,2] |
| 153 | +return pres[e] - s == 0 ? 0 : pres[s - 1]; |
| 154 | +``` |
| 155 | + |
| 156 | +### 关键点 |
| 157 | + |
| 158 | +- 对 DP 数组本身求前缀和,而不是原数组 |
| 159 | + |
| 160 | +### 代码 |
| 161 | + |
| 162 | +- 语言支持:Python3 |
| 163 | + |
| 164 | +Python3 Code: |
| 165 | + |
| 166 | +```python |
| 167 | + |
| 168 | + |
| 169 | +class Solution: |
| 170 | + def canReach(self, s: str, minJump: int, maxJump: int) -> bool: |
| 171 | + n = len(s) |
| 172 | + pres = [0] * n |
| 173 | + dp = [0] * n |
| 174 | + dp[0] = pres[0] = 1 |
| 175 | + for i in range(1, n): |
| 176 | + l = i - maxJump - 1 |
| 177 | + r = i - minJump |
| 178 | + dp[i] = s[i] == '0' and (0 if r < 0 else pres[r]) - (0 if l < 0 else pres[l]) > 0 |
| 179 | + pres[i] = pres[i-1] + dp[i] |
| 180 | + return dp[-1] |
| 181 | + |
| 182 | +``` |
| 183 | + |
| 184 | +**复杂度分析** |
| 185 | + |
| 186 | +令 n 为数组长度。 |
| 187 | + |
| 188 | +- 时间复杂度:$O(n)$ |
| 189 | +- 空间复杂度:$O(n)$ |
| 190 | + |
| 191 | +## 平衡二叉树 |
| 192 | + |
| 193 | +### 思路 |
| 194 | + |
| 195 | +我们可以将所有的 0 的索引按照升序顺序存起来,不妨令这个存储 0 索引的数据结构名为 zeros。 |
| 196 | + |
| 197 | +然后继续使用前面提到的动态规划思路,即 dp(i) 表示是否可从索引为 0 的点到达索引为 1 的点。唯一不同的是,这里不使用前缀和加速。 |
| 198 | + |
| 199 | +对于每一个可以到达的点(初始为索引为 0 的点),我们都执行一下判断: |
| 200 | + |
| 201 | +1. 当前点 i 可以到跳的点为 j。其中 j 属于区间[i + minJump, i + maxJump]。判断 j 是否存在于 zeros。 (操作 1) |
| 202 | +2. 如果存在 zero[k] == j 。则令 dp[j] = true(操作 2) |
| 203 | + |
| 204 | +最后返回最后 dp[n] 即可。 |
| 205 | + |
| 206 | +这种做法时间复杂度取决于 zeros 的数据结构。由于 zero 需要支持区间查找(操作 1),并且 zeros 是升序的,因此使用数组来存储就没问题。区间查找使用二分即可。 |
| 207 | + |
| 208 | +不过由于 zeros 最差的情况下可以到达 n 的数据规模,而此时操作 2 复杂度可以到达 $O(n)$,因此对于全为 0 的情况,时间复杂度为 $O(n^2)$。 |
| 209 | + |
| 210 | +我们可以使用平衡二叉树,并在每次操作 2 结束后将 v 从 zeros 移除来进行加速(平衡二叉树删除只需要 logn 时间) |
| 211 | + |
| 212 | +并且由于 zeros 中的值都最多只会被访问一次,因此时间复杂度为 $O(n)$。但是我们使用二分查找时间复杂度是 $logn$,因此总的时间不大于 $nlogn$。之所以说不大于,是因为 zeros 在不断变小,因此每次二分时间也在不断缩减。 |
| 213 | + |
| 214 | +### 关键点 |
| 215 | + |
| 216 | +- 使用平衡二叉树不断执行删除以降低复杂度 |
| 217 | + |
| 218 | +### 代码 |
| 219 | + |
| 220 | +- 语言支持:Python3 |
| 221 | + |
| 222 | +Python3 Code: |
| 223 | + |
| 224 | +```python |
| 225 | + |
| 226 | +from sortedcontainers import SortedList |
| 227 | +class Solution: |
| 228 | + def canReach(self, s: str, minJump: int, maxJump: int) -> bool: |
| 229 | + if s[-1] == '1': return False |
| 230 | + zeroes = SortedList([i for i in range(len(s)) if s[i] == '0']) |
| 231 | + |
| 232 | + dp = [False] * len(s) |
| 233 | + dp[0] = True |
| 234 | + |
| 235 | + for i in range(len(s)): |
| 236 | + if dp[i]: |
| 237 | + l = zeroes.bisect_left(i + minJump) |
| 238 | + r = zeroes.bisect_right(i + maxJump) |
| 239 | + for v in [zeroes[i] for i in range(l, r)]: |
| 240 | + dp[v] = True |
| 241 | + zeroes.remove(v) |
| 242 | + return dp[-1] |
| 243 | + |
| 244 | +``` |
| 245 | + |
| 246 | +**复杂度分析** |
| 247 | + |
| 248 | +令 n 为数组长度。 |
| 249 | + |
| 250 | +- 时间复杂度:$O(nlogn)$ |
| 251 | +- 空间复杂度:$O(n)$ |
| 252 | + |
| 253 | +> 此题解由 [力扣刷题插件](https://leetcode-pp.github.io/leetcode-cheat/?tab=solution-template) 自动生成。 |
| 254 | +
|
| 255 | +力扣的小伙伴可以[关注我](https://leetcode-cn.com/u/fe-lucifer/),这样就会第一时间收到我的动态啦~ |
| 256 | + |
| 257 | +以上就是本文的全部内容了。大家对此有何看法,欢迎给我留言,我有时间都会一一查看回答。更多算法套路可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 40K star 啦。大家也可以关注我的公众号《力扣加加》带你啃下算法这块硬骨头。 |
| 258 | + |
| 259 | +关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 |
| 260 | + |
| 261 | + |
0 commit comments