207 lines
9.6 KiB
YAML
207 lines
9.6 KiB
YAML
title: Jump Game VII
|
|
slug: jump-game-vii
|
|
difficulty: medium
|
|
leetcode_id: 1871
|
|
leetcode_url: https://leetcode.com/problems/jump-game-vii/
|
|
categories:
|
|
- strings
|
|
- dynamic-programming
|
|
patterns:
|
|
- bfs
|
|
- sliding-window
|
|
- dynamic-programming
|
|
|
|
description: |
|
|
You are given a **0-indexed** binary string `s` and two integers `minJump` and `maxJump`. In the beginning, you are standing at index `0`, which is equal to `'0'`. You can move from index `i` to index `j` if the following conditions are fulfilled:
|
|
|
|
- `i + minJump <= j <= min(i + maxJump, s.length - 1)`, and
|
|
- `s[j] == '0'`.
|
|
|
|
Return `true` *if you can reach index* `s.length - 1` *in* `s`*, or* `false` *otherwise*.
|
|
|
|
constraints: |
|
|
- `2 <= s.length <= 10^5`
|
|
- `s[i]` is either `'0'` or `'1'`
|
|
- `s[0] == '0'`
|
|
- `1 <= minJump <= maxJump < s.length`
|
|
|
|
examples:
|
|
- input: 's = "011010", minJump = 2, maxJump = 3'
|
|
output: "true"
|
|
explanation: "In the first step, move from index 0 to index 3. In the second step, move from index 3 to index 5."
|
|
- input: 's = "01101110", minJump = 2, maxJump = 3'
|
|
output: "false"
|
|
explanation: "There is no way to reach the last index starting from index 0."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're hopping across stepping stones in a river, where `'0'` represents a safe stone and `'1'` represents water. From any stone, you can only jump forward between `minJump` and `maxJump` steps.
|
|
|
|
The naive approach would be to try every possible jump from every reachable position — but with up to 10^5 characters and a jump range that could span thousands of indices, this becomes extremely slow.
|
|
|
|
The key insight is that we need to efficiently track **which positions are reachable** and then, for each new position, check if **any** reachable position can jump to it. Instead of checking every source position individually, we can use a **sliding window** or **prefix sum** to answer "is there any reachable position in the valid jump range?" in O(1) time.
|
|
|
|
Think of it like this: as you scan left to right, you maintain a count of how many reachable positions fall within the "jump window" for the current index. If that count is positive and the current character is `'0'`, you can reach it.
|
|
|
|
approach: |
|
|
We solve this using **Dynamic Programming with Prefix Sum optimization**:
|
|
|
|
**Step 1: Set up the DP array**
|
|
|
|
- Create a boolean array `dp` where `dp[i]` indicates whether index `i` is reachable
|
|
- Set `dp[0] = True` since we start at index 0
|
|
- Initialize a counter `reachable` to track reachable positions in the current window
|
|
|
|
|
|
|
|
**Step 2: Iterate through the string**
|
|
|
|
- For each index `i` from 1 to `n-1`:
|
|
- First, check if a new position has entered our "from" window: if `i >= minJump` and `dp[i - minJump]` is true, increment `reachable`
|
|
- Then, check if a position has left the window: if `i > maxJump` and `dp[i - maxJump - 1]` is true, decrement `reachable`
|
|
- If `reachable > 0` and `s[i] == '0'`, mark `dp[i] = True`
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- Return `dp[n - 1]` — whether the last index is reachable
|
|
|
|
|
|
|
|
This approach works because the sliding window maintains a count of all reachable positions that could potentially jump to the current index. We add positions as they enter the jump range and remove them as they exit.
|
|
|
|
common_pitfalls:
|
|
- title: The BFS/DFS Timeout
|
|
description: |
|
|
A natural approach is to use BFS or DFS to explore all reachable positions. However, with a string of length `10^5` and a jump range that could span thousands of indices, this approach degenerates to O(n * (maxJump - minJump)) which is potentially O(n^2).
|
|
|
|
For example, with `n = 100000`, `minJump = 1`, and `maxJump = 50000`, each position could have 50,000 neighbors to explore!
|
|
wrong_approach: "Plain BFS/DFS exploring all neighbors in jump range"
|
|
correct_approach: "BFS with visited tracking and early termination, or DP with sliding window"
|
|
|
|
- title: Checking Every Source Position
|
|
description: |
|
|
For each index `i`, you might think to loop through all `j` from `i - maxJump` to `i - minJump` to check if any `dp[j]` is true. This is O(n * range) which is too slow.
|
|
|
|
The sliding window/prefix sum optimization reduces this to O(1) per index by maintaining a running count of reachable positions in the valid range.
|
|
wrong_approach: "For each i, loop through all j in jump range"
|
|
correct_approach: "Maintain sliding window count of reachable positions"
|
|
|
|
- title: Off-by-One Errors in Window Bounds
|
|
description: |
|
|
The jump constraints are `i + minJump <= j <= i + maxJump`. When working backwards (asking "can I reach index j?"), the valid source range is `j - maxJump <= i <= j - minJump`.
|
|
|
|
Be careful when a position enters and exits the window:
|
|
- Position `j - minJump` enters the window when we reach index `j`
|
|
- Position `j - maxJump - 1` exits the window when we reach index `j`
|
|
wrong_approach: "Incorrect window boundary calculations"
|
|
correct_approach: "Carefully track when positions enter (at i - minJump) and exit (at i - maxJump - 1)"
|
|
|
|
key_takeaways:
|
|
- "**Sliding window for range queries**: When you need to check if *any* value in a range satisfies a condition, maintain a running count as the window slides"
|
|
- "**Prefix sum for cumulative queries**: This pattern appears frequently — counting elements in ranges can be done in O(1) with preprocessing"
|
|
- "**Optimizing DP transitions**: When DP transitions involve checking a range of previous states, look for ways to avoid the inner loop"
|
|
- "**Jump Game series**: This problem extends the classic Jump Game pattern — earlier versions use greedy, this one requires DP with optimization"
|
|
|
|
time_complexity: "O(n). We iterate through the string once, and each position enters and exits the sliding window exactly once."
|
|
space_complexity: "O(n). We use a DP array of size `n` to track reachability of each position."
|
|
|
|
solutions:
|
|
- approach_name: DP with Sliding Window
|
|
is_optimal: true
|
|
code: |
|
|
def can_reach(s: str, min_jump: int, max_jump: int) -> bool:
|
|
n = len(s)
|
|
# dp[i] = True if we can reach index i
|
|
dp = [False] * n
|
|
dp[0] = True # Start at index 0
|
|
|
|
# Count of reachable positions in the current jump window
|
|
reachable = 0
|
|
|
|
for i in range(1, n):
|
|
# Position (i - min_jump) just entered our "can jump from" window
|
|
if i >= min_jump and dp[i - min_jump]:
|
|
reachable += 1
|
|
|
|
# Position (i - max_jump - 1) just left the window
|
|
if i > max_jump and dp[i - max_jump - 1]:
|
|
reachable -= 1
|
|
|
|
# If any reachable position can jump here and it's a '0', mark reachable
|
|
if reachable > 0 and s[i] == '0':
|
|
dp[i] = True
|
|
|
|
return dp[n - 1]
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the string with O(1) work per index.
|
|
|
|
**Space Complexity:** O(n) — DP array to track reachability.
|
|
|
|
The sliding window maintains a count of positions that could jump to the current index. As we move right, positions enter the window when they're exactly `minJump` away, and exit when they're more than `maxJump` away.
|
|
|
|
- approach_name: BFS with Optimization
|
|
is_optimal: false
|
|
code: |
|
|
from collections import deque
|
|
|
|
def can_reach(s: str, min_jump: int, max_jump: int) -> bool:
|
|
n = len(s)
|
|
if s[n - 1] == '1':
|
|
return False
|
|
|
|
queue = deque([0])
|
|
# Track the farthest index we've added to avoid duplicates
|
|
farthest = 0
|
|
|
|
while queue:
|
|
i = queue.popleft()
|
|
|
|
# Start of jump range: don't re-explore already visited indices
|
|
start = max(i + min_jump, farthest + 1)
|
|
end = min(i + max_jump, n - 1)
|
|
|
|
for j in range(start, end + 1):
|
|
if s[j] == '0':
|
|
if j == n - 1:
|
|
return True
|
|
queue.append(j)
|
|
|
|
# Update farthest to avoid revisiting
|
|
farthest = max(farthest, i + max_jump)
|
|
|
|
return False
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each index is added to the queue at most once due to the `farthest` optimization.
|
|
|
|
**Space Complexity:** O(n) — Queue can hold up to n positions in the worst case.
|
|
|
|
This BFS approach uses a `farthest` pointer to avoid re-exploring indices. When processing position `i`, we only explore indices beyond what we've already added. This ensures each index is visited at most once, giving linear time.
|
|
|
|
- approach_name: Brute Force DP
|
|
is_optimal: false
|
|
code: |
|
|
def can_reach(s: str, min_jump: int, max_jump: int) -> bool:
|
|
n = len(s)
|
|
dp = [False] * n
|
|
dp[0] = True
|
|
|
|
for i in range(1, n):
|
|
if s[i] == '1':
|
|
continue
|
|
|
|
# Check all positions that could jump to i
|
|
for j in range(max(0, i - max_jump), i - min_jump + 1):
|
|
if dp[j]:
|
|
dp[i] = True
|
|
break
|
|
|
|
return dp[n - 1]
|
|
explanation: |
|
|
**Time Complexity:** O(n * (maxJump - minJump)) — For each position, we check up to `maxJump - minJump + 1` previous positions.
|
|
|
|
**Space Complexity:** O(n) — DP array.
|
|
|
|
This approach is correct but too slow for large inputs. With `n = 10^5` and a large jump range, this becomes O(n^2) and will TLE. Included to illustrate why the sliding window optimization is necessary.
|