You are given a tree with n
nodes numbered from 0
to n - 1
in the form of a parent array parent
where parent[i]
is the parent of the i<sup>th</sup>
node. The root of the tree is node 0
, so parent[0] = -1
since it has no parent. You want to design a data structure that allows users to lock, unlock, and upgrade nodes in the tree.
The data structure should support the following functions:
Implement the LockingTree
class:
LockingTree(int[] parent)
initializes the data structure with the parent array.lock(int num, int user)
returns true
if it is possible for the user with id user
to lock the node num
, or false
otherwise. If it is possible, the node num
will become locked by the user with id user
.unlock(int num, int user)
returns true
if it is possible for the user with id user
to unlock the node num
, or false
otherwise. If it is possible, the node num
will become unlocked.upgrade(int num, int user)
returns true
if it is possible for the user with id user
to upgrade the node num
, or false
otherwise. If it is possible, the node num
will be upgraded.For example, consider the following sequence of operations:
LockingTree lockingTree = new LockingTree([-1, 0, 0, 1, 1, 2, 2]);
lockingTree.lock(2, 2); // return true
lockingTree.unlock(2, 3); // return false
lockingTree.unlock(2, 2); // return true
lockingTree.lock(4, 5); // return true
lockingTree.upgrade(0, 1); // return true
lockingTree.lock(0, 1); // return false
How would you implement this LockingTree
class, and what would be the time and space complexity of each operation?
A naive approach to solving this problem would be to directly implement the given functions lock
, unlock
, and upgrade
as described. This would involve:
lock
and unlock
functions by simply checking the lock status and updating it if the conditions are met.upgrade
function by checking the specified conditions (unlocked node, at least one locked descendant, no locked ancestors) and then performing the lock and unlock operations.class LockingTree:
def __init__(self, parent: List[int]):
self.parent = parent
self.locked = [0] * len(parent) # 0: unlocked, user_id: locked by user_id
self.children = [[] for _ in range(len(parent))]
for i in range(1, len(parent)):
self.children[parent[i]].append(i)
def lock(self, num: int, user: int) -> bool:
if self.locked[num] == 0:
self.locked[num] = user
return True
return False
def unlock(self, num: int, user: int) -> bool:
if self.locked[num] == user:
self.locked[num] = 0
return True
return False
def upgrade(self, num: int, user: int) -> bool:
if self.locked[num] != 0:
return False
# Check for locked ancestors
curr = self.parent[num]
while curr != -1:
if self.locked[curr] != 0:
return False
curr = self.parent[curr]
# Check for locked descendants and unlock them
locked_descendants = []
def find_locked_descendants(node):
if self.locked[node] != 0:
locked_descendants.append(node)
for child in self.children[node]:
find_locked_descendants(child)
find_locked_descendants(num)
if not locked_descendants:
return False
# Unlock all locked descendants
for node in locked_descendants:
self.locked[node] = 0
# Lock the current node
self.locked[num] = user
return True
lock()
: O(1)unlock()
: O(1)upgrade()
: O(N) in the worst case, where N is the number of nodes (due to ancestor and descendant checks).parent
array, locked
array, and children
list.The optimal solution focuses on improving the upgrade
function's efficiency. The key improvement involves pre-calculating and storing the descendants of each node to avoid repeatedly traversing the tree.
i
contains a list of its children.lock
and unlock
functions: Remain the same, with O(1) complexity.upgrade
Function: The primary optimization will be in checking descendants quickly using the precomputed children.class LockingTree:
def __init__(self, parent: List[int]):
self.parent = parent
self.locked = [0] * len(parent)
self.children = [[] for _ in range(len(parent))]
for i in range(1, len(parent)):
self.children[parent[i]].append(i)
def lock(self, num: int, user: int) -> bool:
if self.locked[num] == 0:
self.locked[num] = user
return True
return False
def unlock(self, num: int, user: int) -> bool:
if self.locked[num] == user:
self.locked[num] = 0
return True
return False
def upgrade(self, num: int, user: int) -> bool:
if self.locked[num] != 0:
return False
# Check for locked ancestors
curr = self.parent[num]
while curr != -1:
if self.locked[curr] != 0:
return False
curr = self.parent[curr]
# Check for locked descendants and unlock them
locked_descendants = []
def find_locked_descendants(node):
if self.locked[node] != 0:
locked_descendants.append(node)
for child in self.children[node]:
find_locked_descendants(child)
find_locked_descendants(num)
if not locked_descendants:
return False
# Unlock all locked descendants
for node in locked_descendants:
self.locked[node] = 0
# Lock the current node
self.locked[num] = user
return True
lock()
: O(1)unlock()
: O(1)upgrade()
: O(N) in the worst case, where N is the number of nodes. While precomputing children helps, we still need to traverse potential descendants and ancestors.parent
array, locked
array, and children
list.parent[0] == -1
). This needs to be handled correctly when checking for locked ancestors in the upgrade
function.upgrade
function should return false
if there are no locked descendants.lock
function should return false
if the node is already locked.unlock
function should return false
if the node is locked by a different user.num
, but in a real-world scenario, you might want to add checks to ensure the node number is within the valid range.