1993. Operations On Tree - Explanation
Description
Implement LockingTree for a rooted tree described by parent. Node 0 is the root and has parent -1; every other entry identifies that node's parent. All nodes start unlocked.
lock(num, user)locksnumforuserif that node is currently unlocked. It does not check ancestors or descendants.unlock(num, user)unlocksnumonly if that same user currently owns its lock.upgrade(num, user)succeeds only whennumis unlocked, every ancestor ofnumis unlocked, and at least one proper descendant is locked. On success, unlock every descendant regardless of its owner and locknumforuser.
Each operation returns true when it succeeds and false otherwise. A failed operation leaves every lock unchanged. A proper descendant excludes the node itself.
Example 1:
Input: ["LockingTree","lock","unlock","upgrade","unlock","unlock"]
[[[-1,0,0]],[1,7],[1,8],[0,4],[1,7],[0,4]]
Output: [null,true,false,true,false,true]Explanation: User 7 locks node 1; user 8 cannot unlock it. Upgrading root 0 succeeds because it has a locked descendant and no locked ancestor. This clears node 1 and locks node 0 for user 4, so only the final unlock succeeds.
Example 2:
Input: ["LockingTree","lock","lock","upgrade","unlock","upgrade"]
[[[-1,0,1]],[0,1],[2,2],[1,3],[0,1],[1,3]]
Output: [null,true,true,false,true,true]Explanation: Both ordinary lock calls succeed, even though node 0 is an ancestor of node 2. Upgrading node 1 initially fails because node 0 is locked. After unlocking node 0, the upgrade succeeds and clears node 2.
Example 3:
Input: ["LockingTree","upgrade","lock","upgrade","unlock","lock","upgrade","unlock"]
[[[-1,0,0]],[0,1],[0,1],[0,2],[0,1],[2,99],[1,3],[2,99]]
Output: [null,false,true,false,true,true,false,true]Explanation: An upgrade with no locked descendant fails. An already locked node also cannot be upgraded. Finally, locked node 2 is a sibling of node 1, so upgrading node 1 fails and leaves node 2 locked; its owner can still unlock it.
Constraints:
2 <= parent.length <= 2000parent[0] == -1- Every other parent is a valid node index, and
parentdescribes a valid tree rooted at node 0. 0 <= num < parent.length1 <= user <= 10000- At most
2000calls are made tolock,unlock, andupgradecombined.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Tree Data Structure - Understanding parent-child relationships and tree traversal patterns
- Depth-First Search (DFS) - Recursive and iterative traversal of tree nodes to visit all descendants
- Breadth-First Search (BFS) - Level-by-level tree traversal using a queue
- Building Adjacency Lists - Converting parent array representation to child adjacency lists for efficient traversal
1. Depth First Search
Intuition
We need to simulate a locking system on a tree structure. The key insight is that lock and unlock are simple O(1) operations since they only check and modify a single node. The upgrade operation is more complex: it requires checking all ancestors (no locks allowed) and all descendants (at least one lock required, then unlock all).
For the ancestor check, we traverse up the parent chain. For the descendant check and unlock, we use dfs to visit all nodes in the subtree, counting and clearing any locks.
Algorithm
- Initialization: Build a child adjacency list from the parent array and create a locked array to track which user (if any) has locked each node.
- lock(num, user): If the node is unlocked (
locked[num] == 0), setlocked[num] = userand returntrue. Otherwise returnfalse. - unlock(num, user): If
locked[num]equalsuser, set it to0and returntrue. Otherwise returnfalse. - upgrade(num, user):
- Walk up the parent chain from
num. If any ancestor is locked, returnfalse. - Run
dfson the subtree rooted atnum: count locked descendants and unlock them. - If at least one descendant was locked, lock
numwithuserand returntrue. Otherwise returnfalse.
- Walk up the parent chain from
class LockingTree:
def __init__(self, parent: List[int]):
self.parent = parent
self.child = [[] for _ in range(len(parent))]
self.locked = [0] * len(parent)
for node in range(1, len(parent)):
self.child[parent[node]].append(node)
def lock(self, num: int, user: int) -> bool:
if self.locked[num]:
return False
self.locked[num] = user
return True
def unlock(self, num: int, user: int) -> bool:
if self.locked[num] != user:
return False
self.locked[num] = 0
return True
def upgrade(self, num: int, user: int) -> bool:
node = num
while node != -1:
if self.locked[node]:
return False
node = self.parent[node]
def dfs(node):
lockedCount = 0
if self.locked[node]:
lockedCount += 1
self.locked[node] = 0
for nei in self.child[node]:
lockedCount += dfs(nei)
return lockedCount
if dfs(num) > 0:
self.locked[num] = user
return True
return FalseTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each and function call.
- time for each function call.
- Space complexity:
2. Breadth First Search
Intuition
This solution replaces the recursive dfs for the descendant traversal with an iterative BFS using a queue. The logic remains identical: lock and unlock are O(1) operations, while upgrade requires ancestor and descendant checks.
BFS processes nodes level by level, which can be more cache-friendly in some cases and avoids potential stack overflow issues with very deep trees.
Algorithm
- Initialization: Same as
dfs, build child lists and initialize the locked array. - lock and unlock: Same O(1) checks and updates.
- upgrade(num, user):
- Check ancestors by walking up the parent chain.
- Use a queue to traverse all descendants, counting and unlocking any locked nodes.
- If
lockedCount > 0, locknumforuserand returntrue.
class LockingTree:
def __init__(self, parent: List[int]):
self.parent = parent
self.child = [[] for _ in range(len(parent))]
self.locked = [0] * len(parent)
for node in range(1, len(parent)):
self.child[parent[node]].append(node)
def lock(self, num: int, user: int) -> bool:
if self.locked[num]:
return False
self.locked[num] = user
return True
def unlock(self, num: int, user: int) -> bool:
if self.locked[num] != user:
return False
self.locked[num] = 0
return True
def upgrade(self, num: int, user: int) -> bool:
node = num
while node != -1:
if self.locked[node]:
return False
node = self.parent[node]
lockedCount, q = 0, deque([num])
while q:
node = q.popleft()
if self.locked[node]:
self.locked[node] = 0
lockedCount += 1
q.extend(self.child[node])
if lockedCount:
self.locked[num] = user
return True
return FalseTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each and function call.
- time for each function call.
- Space complexity:
3. Iterative DFS
Intuition
This approach converts the recursive dfs into an iterative version using an explicit stack. This is useful when you want to avoid recursion overhead or potential stack overflow on very large trees, while maintaining the depth-first traversal order.
The core logic for all three operations remains unchanged from the recursive dfs solution.
Algorithm
- Initialization: Build child adjacency lists and initialize the locked array.
- lock and unlock: Same O(1) checks.
- upgrade(num, user):
- Traverse ancestors to ensure none are locked.
- Use a stack to perform iterative
dfson descendants. - Pop nodes, check if locked, unlock if so, and push children.
- If any descendants were locked, lock
numand returntrue.
class LockingTree:
def __init__(self, parent: List[int]):
self.parent = parent
self.child = [[] for _ in range(len(parent))]
self.locked = [0] * len(parent)
for node in range(1, len(parent)):
self.child[parent[node]].append(node)
def lock(self, num: int, user: int) -> bool:
if self.locked[num]:
return False
self.locked[num] = user
return True
def unlock(self, num: int, user: int) -> bool:
if self.locked[num] != user:
return False
self.locked[num] = 0
return True
def upgrade(self, num: int, user: int) -> bool:
node = num
while node != -1:
if self.locked[node]:
return False
node = self.parent[node]
lockedCount, stack = 0, [num]
while stack:
node = stack.pop()
if self.locked[node]:
self.locked[node] = 0
lockedCount += 1
stack.extend(self.child[node])
if lockedCount:
self.locked[num] = user
return True
return FalseTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each and function call.
- time for each function call.
- Space complexity:
Common Pitfalls
Not Checking All Ancestors for Upgrade
The upgrade operation requires that no ancestor of the node is locked. Checking only the immediate parent is insufficient. You must traverse the entire path from the node to the root, verifying each ancestor is unlocked before proceeding.
Forgetting to Unlock Descendants Before Locking
When upgrade succeeds, all locked descendants must be unlocked before locking the current node. Some solutions lock the node first, which means when traversing descendants, the node itself gets incorrectly counted or unlocked. Process descendants completely before modifying the current node.
Returning True When No Descendants Are Locked
The upgrade operation requires at least one locked descendant. Even if all other conditions are met (node unlocked, ancestors unlocked), if no descendant is locked, the operation must return false. Always verify the locked descendant count is greater than zero before returning success.
Sign in to join the discussion