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), set locked[num] = user and return true. Otherwise return false.
unlock(num, user): If locked[num] equals user, set it to 0 and return true. Otherwise return false.
upgrade(num, user):
Walk up the parent chain from num. If any ancestor is locked, return false.
Run dfs on the subtree rooted at num: count locked descendants and unlock them.
If at least one descendant was locked, lock num with user and return true. Otherwise return false.
type LockingTree struct{
parent []int
child [][]int
locked []int}funcConstructor(parent []int) LockingTree {
n :=len(parent)
child :=make([][]int, n)for i :=range child {
child[i]=[]int{}}for node :=1; node < n; node++{
child[parent[node]]=append(child[parent[node]], node)}return LockingTree{
parent: parent,
child: child,
locked:make([]int, n),}}func(this *LockingTree)Lock(num int, user int)bool{if this.locked[num]!=0{returnfalse}
this.locked[num]= user
returntrue}func(this *LockingTree)Unlock(num int, user int)bool{if this.locked[num]!= user {returnfalse}
this.locked[num]=0returntrue}func(this *LockingTree)Upgrade(num int, user int)bool{
node := num
for node !=-1{if this.locked[node]!=0{returnfalse}
node = this.parent[node]}var dfs func(node int)int
dfs =func(node int)int{
lockedCount :=0if this.locked[node]!=0{
lockedCount++
this.locked[node]=0}for_, nei :=range this.child[node]{
lockedCount +=dfs(nei)}return lockedCount
}ifdfs(num)>0{
this.locked[num]= user
returntrue}returnfalse}
classLockingTree(privateval parent: IntArray){privateval child: Array<MutableList<Int>>=Array(parent.size){mutableListOf()}privateval locked =IntArray(parent.size)init{for(node in1 until parent.size){
child[parent[node]].add(node)}}funlock(num: Int, user: Int): Boolean {if(locked[num]!=0){returnfalse}
locked[num]= user
returntrue}fununlock(num: Int, user: Int): Boolean {if(locked[num]!= user){returnfalse}
locked[num]=0returntrue}funupgrade(num: Int, user: Int): Boolean {var node = num
while(node !=-1){if(locked[node]!=0){returnfalse}
node = parent[node]}val lockedCount =dfs(num)if(lockedCount >0){
locked[num]= user
returntrue}returnfalse}privatefundfs(node: Int): Int {var lockedCount =0if(locked[node]!=0){
lockedCount++
locked[node]=0}for(nei in child[node]){
lockedCount +=dfs(nei)}return lockedCount
}}
classLockingTree{privatevar parent:[Int]privatevar child:[[Int]]privatevar locked:[Int]init(_ parent:[Int]){self.parent = parent
let n = parent.count
self.child =Array(repeating:[Int](), count: n)self.locked =Array(repeating:0, count: n)for node in1..<n {self.child[parent[node]].append(node)}}funclock(_ num:Int,_ user:Int)->Bool{if locked[num]!=0{returnfalse}
locked[num]= user
returntrue}funcunlock(_ num:Int,_ user:Int)->Bool{if locked[num]!= user {returnfalse}
locked[num]=0returntrue}funcupgrade(_ num:Int,_ user:Int)->Bool{var node = num
while node !=-1{if locked[node]!=0{returnfalse}
node = parent[node]}funcdfs(_ node:Int)->Int{var lockedCount =0if locked[node]!=0{
lockedCount +=1
locked[node]=0}for nei in child[node]{
lockedCount +=dfs(nei)}return lockedCount
}ifdfs(num)>0{
locked[num]= user
returntrue}returnfalse}}
Time & Space Complexity
Time complexity:
O(n) time for initialization.
O(1) time for each lock() and unlock() function call.
O(n) time for each upgrade() function call.
Space complexity: O(n)
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, lock num for user and return true.
classLockingTree{privatevar parent:[Int]privatevar child:[[Int]]privatevar locked:[Int]init(_ parent:[Int]){self.parent = parent
let n = parent.count
self.child =Array(repeating:[Int](), count: n)self.locked =Array(repeating:0, count: n)for node in1..<n {self.child[parent[node]].append(node)}}funclock(_ num:Int,_ user:Int)->Bool{if locked[num]!=0{returnfalse}
locked[num]= user
returntrue}funcunlock(_ num:Int,_ user:Int)->Bool{if locked[num]!= user {returnfalse}
locked[num]=0returntrue}funcupgrade(_ num:Int,_ user:Int)->Bool{var node = num
while node !=-1{if locked[node]!=0{returnfalse}
node = parent[node]}var lockedCount =0var q =[num]while!q.isEmpty {
node = q.removeFirst()if locked[node]!=0{
locked[node]=0
lockedCount +=1}
q.append(contentsOf: child[node])}if lockedCount >0{
locked[num]= user
returntrue}returnfalse}}
Time & Space Complexity
Time complexity:
O(n) time for initialization.
O(1) time for each lock() and unlock() function call.
O(n) time for each upgrade() function call.
Space complexity: O(n)
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 dfs on descendants.
Pop nodes, check if locked, unlock if so, and push children.
If any descendants were locked, lock num and return true.
classLockingTree{privatevar parent:[Int]privatevar child:[[Int]]privatevar locked:[Int]init(_ parent:[Int]){self.parent = parent
let n = parent.count
self.child =Array(repeating:[Int](), count: n)self.locked =Array(repeating:0, count: n)for node in1..<n {self.child[parent[node]].append(node)}}funclock(_ num:Int,_ user:Int)->Bool{if locked[num]!=0{returnfalse}
locked[num]= user
returntrue}funcunlock(_ num:Int,_ user:Int)->Bool{if locked[num]!= user {returnfalse}
locked[num]=0returntrue}funcupgrade(_ num:Int,_ user:Int)->Bool{var node = num
while node !=-1{if locked[node]!=0{returnfalse}
node = parent[node]}var lockedCount =0var stack =[num]while!stack.isEmpty {
node = stack.removeLast()if locked[node]!=0{
locked[node]=0
lockedCount +=1}
stack.append(contentsOf: child[node])}if lockedCount >0{
locked[num]= user
returntrue}returnfalse}}
Time & Space Complexity
Time complexity:
O(n) time for initialization.
O(1) time for each lock() and unlock() function call.
O(n) time for each upgrade() function call.
Space complexity: O(n)
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.