Files
codetutor/backend/data/questions/jump-game-vii.yaml

232 lines
10 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:
- slug: bfs
is_optimal: false
- slug: sliding-window
is_optimal: true
- slug: dynamic-programming
is_optimal: false
function_signature: "def can_reach(s: str, min_jump: int, max_jump: int) -> bool:"
test_cases:
visible:
- input: { s: "011010", min_jump: 2, max_jump: 3 }
expected: true
- input: { s: "01101110", min_jump: 2, max_jump: 3 }
expected: false
hidden:
- input: { s: "00", min_jump: 1, max_jump: 1 }
expected: true
- input: { s: "01", min_jump: 1, max_jump: 1 }
expected: false
- input: { s: "0000", min_jump: 1, max_jump: 3 }
expected: true
- input: { s: "00000000", min_jump: 3, max_jump: 5 }
expected: true
- input: { s: "0100000", min_jump: 2, max_jump: 5 }
expected: true
- input: { s: "01111110", min_jump: 1, max_jump: 6 }
expected: false
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
&nbsp;
**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`
&nbsp;
**Step 3: Return the result**
- Return `dp[n - 1]` — whether the last index is reachable
&nbsp;
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.