LeetCode 1377 - Frog Position After T Seconds
This problem describes a frog moving through an undirected tree. A tree is a connected graph with no cycles, which means
Difficulty: 🔴 Hard
Topics: Tree, Depth-First Search, Breadth-First Search, Graph Theory
Solution
Problem Understanding
This problem describes a frog moving through an undirected tree. A tree is a connected graph with no cycles, which means there is exactly one path between any two vertices.
The frog always starts at node 1. Every second, the frog must jump to one of its unvisited neighboring nodes. If there are multiple valid choices, each one is chosen with equal probability. Once a node has been visited, the frog can never return to it because the movement rule only allows jumps to unvisited vertices.
If the frog reaches a node where all neighbors have already been visited, then it becomes stuck and remains on that node forever.
The input consists of:
n, the number of verticesedges, the list of undirected connections between verticest, the number of seconds that passtarget, the node whose probability we want to compute
The goal is to return the probability that after exactly t seconds, the frog is standing on target.
The constraints are relatively small:
n <= 100t <= 50
These limits tell us that we do not need advanced optimizations. A straightforward graph traversal such as DFS or BFS is sufficient.
Several edge cases are important:
- The target might be reached before
tseconds. - The target might still have unvisited neighbors when reached.
- The target might be a leaf node.
- The frog may become stuck before time runs out.
- The target may never be reachable at exactly time
t.
A common source of bugs is misunderstanding what happens when the frog reaches the target early. If the target still has unvisited neighbors, the frog must continue moving away from it in later seconds, so the probability becomes zero unless t exactly matches the arrival time. However, if the target is a leaf, the frog stays there forever once it arrives.
Approaches
Brute Force Approach
A brute force solution would simulate every possible path the frog can take.
Starting from node 1, at every second we recursively branch into all valid unvisited neighbors. For each path, we compute the probability contribution by multiplying by 1 / choices at every step. After exactly t seconds, we check whether the frog is at the target.
This approach is correct because it explicitly enumerates every valid movement sequence and accumulates probabilities accurately.
However, this method becomes inefficient because the number of possible paths grows exponentially with depth. Even though the constraints are not enormous, unnecessary recomputation makes this approach less elegant and harder to reason about.
Optimal Approach
The key observation is that the graph is a tree.
Because a tree has exactly one path between any two nodes, the frog can only reach the target through one unique route. We do not need to explore arbitrary cycles or revisit states.
We can perform a DFS or BFS traversal while tracking:
- Current node
- Current time
- Current probability
- Parent node, to avoid revisiting
At every node:
- Count how many unvisited neighbors exist.
- Divide the current probability equally among them.
- Continue traversal.
The important logic occurs when we reach the target:
- If we arrive exactly at time
t, return the current probability. - If we arrive before
tand the target is a leaf, the frog stays there forever, so return the probability. - If we arrive before
tbut the target has unvisited neighbors, the frog must leave, so return0.
Since each edge and node is visited only once, the algorithm is linear.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(branching^t) | O(t) | Explores every possible movement sequence |
| Optimal | O(n) | O(n) | DFS/BFS traversal using tree properties |
Algorithm Walkthrough
- Build an adjacency list from the edge list.
Since the graph is undirected, every edge [u, v] must be added in both directions. An adjacency list allows efficient neighbor lookup during traversal.
2. Start a DFS from node 1.
The DFS keeps track of:
- Current node
- Parent node
- Current time elapsed
- Current probability
The parent is necessary because the frog cannot revisit already visited nodes. 3. Count the number of valid next moves.
For the current node, valid moves are neighbors excluding the parent node. This represents all unvisited neighbors. 4. Check whether the current node is the target.
There are two valid situations where the frog can remain at the target:
- We arrive exactly at time
t - We arrive earlier than
t, but the target has no unvisited neighbors
Otherwise, the frog must continue moving away, so the probability becomes zero. 5. Distribute probability among children.
If the node has k valid next moves, then each child receives:
$$\text{next probability} = \frac{\text{current probability}}{k}$$
Continue DFS recursively for each child. 6. Return the probability once the target is found.
Because the graph is a tree, only one path can lead to the target.
Why it works
The algorithm works because trees contain exactly one simple path between nodes. The frog's movement is completely determined by the available unvisited neighbors at each step. By recursively distributing probability equally among all valid children, we model the random process exactly. The stopping condition correctly handles the special rule that the frog remains at a node forever once no unvisited neighbors exist.
Python Solution
from typing import List
from collections import defaultdict
class Solution:
def frogPosition(
self,
n: int,
edges: List[List[int]],
t: int,
target: int
) -> float:
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
def dfs(node: int, parent: int, time: int, probability: float) -> float:
children = 0
for neighbor in graph[node]:
if neighbor != parent:
children += 1
# If we reached the target
if node == target:
# Valid if:
# 1. exact time match
# 2. no further moves available
if time == t or (children == 0 and time < t):
return probability
return 0.0
# No more time left
if time == t:
return 0.0
next_probability = probability / children
for neighbor in graph[node]:
if neighbor != parent:
result = dfs(
neighbor,
node,
time + 1,
next_probability
)
if result > 0:
return result
return 0.0
return dfs(1, 0, 0, 1.0)
The implementation begins by constructing the adjacency list representation of the tree. Since the graph is undirected, every edge is inserted in both directions.
The DFS function is responsible for exploring the tree while tracking time and probability. The parent parameter prevents revisiting nodes, which matches the problem requirement that the frog cannot jump to already visited vertices.
For each node, we count the number of valid children. This value determines how probability is divided among possible moves.
The most important logic occurs when the current node equals the target. The code carefully checks whether the frog is allowed to remain there at the current time. If the target still has unvisited neighbors and time remains, the frog must continue moving, so the probability becomes zero.
The DFS eventually returns the probability of the unique valid path leading to the target.
Go Solution
package main
func frogPosition(n int, edges [][]int, t int, target int) float64 {
graph := make([][]int, n+1)
for _, edge := range edges {
u := edge[0]
v := edge[1]
graph[u] = append(graph[u], v)
graph[v] = append(graph[v], u)
}
var dfs func(node, parent, time int, probability float64) float64
dfs = func(node, parent, time int, probability float64) float64 {
children := 0
for _, neighbor := range graph[node] {
if neighbor != parent {
children++
}
}
// Reached target
if node == target {
if time == t || (children == 0 && time < t) {
return probability
}
return 0.0
}
// No time left
if time == t {
return 0.0
}
nextProbability := probability / float64(children)
for _, neighbor := range graph[node] {
if neighbor != parent {
result := dfs(
neighbor,
node,
time+1,
nextProbability,
)
if result > 0 {
return result
}
}
}
return 0.0
}
return dfs(1, 0, 0, 1.0)
}
The Go implementation closely mirrors the Python version. The adjacency list is implemented using a slice of integer slices.
One important difference is explicit type conversion when dividing probabilities. Since children is an integer, it must be converted to float64 before division.
Go also requires the recursive DFS function to be declared using a variable assignment before definition so the function can reference itself recursively.
Worked Examples
Example 1
Input:
n = 7
edges = [[1,2],[1,3],[1,7],[2,4],[2,6],[3,5]]
t = 2
target = 4
Constructed graph:
| Node | Neighbors |
|---|---|
| 1 | 2, 3, 7 |
| 2 | 1, 4, 6 |
| 3 | 1, 5 |
| 4 | 2 |
| 5 | 3 |
| 6 | 2 |
| 7 | 1 |
Initial state:
| Node | Time | Probability |
|---|---|---|
| 1 | 0 | 1.0 |
At node 1, there are 3 possible moves.
Each move gets probability:
$$\frac{1}{3}$$
Move to node 2:
| Node | Time | Probability |
|---|---|---|
| 2 | 1 | 1/3 |
At node 2, excluding parent 1, there are 2 children: 4 and 6.
Each receives:
$$\frac{1}{3} \times \frac{1}{2} = \frac{1}{6}$$
Move to node 4:
| Node | Time | Probability |
|---|---|---|
| 4 | 2 | 1/6 |
We reached the target exactly at time 2.
Final answer:
$$\frac{1}{6} = 0.16666666666666666$$
Example 2
Input:
n = 7
edges = [[1,2],[1,3],[1,7],[2,4],[2,6],[3,5]]
t = 1
target = 7
Initial state:
| Node | Time | Probability |
|---|---|---|
| 1 | 0 | 1.0 |
At node 1, there are 3 possible moves.
Probability for each:
$$\frac{1}{3}$$
Move to node 7:
| Node | Time | Probability |
|---|---|---|
| 7 | 1 | 1/3 |
We reached the target exactly at time 1.
Final answer:
$$\frac{1}{3} = 0.3333333333333333$$
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Every node and edge is visited at most once |
| Space | O(n) | Adjacency list and recursion stack |
The DFS traverses the tree once. Since a tree with n nodes contains exactly n - 1 edges, the total traversal cost is linear. The adjacency list stores all edges, and the recursion depth can reach at most n in a skewed tree.
Test Cases
from typing import List
class Solution:
def frogPosition(self, n: int, edges: List[List[int]], t: int, target: int) -> float:
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
def dfs(node, parent, time, probability):
children = 0
for neighbor in graph[node]:
if neighbor != parent:
children += 1
if node == target:
if time == t or (children == 0 and time < t):
return probability
return 0.0
if time == t:
return 0.0
next_probability = probability / children
for neighbor in graph[node]:
if neighbor != parent:
result = dfs(
neighbor,
node,
time + 1,
next_probability
)
if result > 0:
return result
return 0.0
return dfs(1, 0, 0, 1.0)
solution = Solution()
# Example 1
assert abs(
solution.frogPosition(
7,
[[1,2],[1,3],[1,7],[2,4],[2,6],[3,5]],
2,
4
) - 0.16666666666666666
) < 1e-5 # standard example
# Example 2
assert abs(
solution.frogPosition(
7,
[[1,2],[1,3],[1,7],[2,4],[2,6],[3,5]],
1,
7
) - 0.3333333333333333
) < 1e-5 # direct one-step jump
# Target is root and frog must move away
assert abs(
solution.frogPosition(
3,
[[1,2],[1,3]],
1,
1
) - 0.0
) < 1e-5 # frog cannot stay at root
# Target is leaf reached early, frog stays there
assert abs(
solution.frogPosition(
2,
[[1,2]],
5,
2
) - 1.0
) < 1e-5 # frog remains at leaf forever
# Target reached too early but must continue moving
assert abs(
solution.frogPosition(
3,
[[1,2],[2,3]],
1,
2
) - 1.0
) < 1e-5 # exact arrival time
assert abs(
solution.frogPosition(
3,
[[1,2],[2,3]],
2,
2
) - 0.0
) < 1e-5 # frog must leave node 2
# Single path tree
assert abs(
solution.frogPosition(
4,
[[1,2],[2,3],[3,4]],
3,
4
) - 1.0
) < 1e-5 # deterministic movement
# Unreachable within given time
assert abs(
solution.frogPosition(
4,
[[1,2],[1,3],[3,4]],
1,
4
) - 0.0
) < 1e-5 # target too far away
| Test | Why |
|---|---|
| Example 1 | Verifies multi-step probability splitting |
| Example 2 | Verifies direct jump probability |
| Target is root | Ensures frog cannot remain if moves exist |
| Leaf reached early | Ensures frog stays forever at leaf |
| Exact arrival time | Verifies valid timing logic |
| Must continue moving | Ensures early arrival does not incorrectly count |
| Single path tree | Verifies deterministic traversal |
| Unreachable target | Ensures impossible states return zero |
Edge Cases
One important edge case occurs when the target is reached before time t, but the target still has unvisited neighbors. In this situation, the frog is forced to continue moving away from the target. A buggy implementation might incorrectly return the probability immediately upon reaching the target. The solution avoids this by checking whether the target node has available children.
Another important case occurs when the target is a leaf node. Once the frog reaches a leaf, it remains there forever because no unvisited neighbors exist. The implementation correctly handles this by allowing the probability to remain valid even when time < t.
A third edge case is when the target is the starting node 1. If t > 0 and node 1 has neighbors, the frog must leave immediately. Therefore, the answer should be zero unless the graph consists of only one node. The DFS logic naturally handles this because the root's available children force movement away from node 1.
A final subtle case is a linear chain tree. In such trees, every move is deterministic because there is only one unvisited neighbor at each step. The probability should remain 1.0 throughout the traversal. Since the algorithm divides by the number of available children, and that value is always 1, the implementation handles this correctly.