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. Implement the LockingTree
class with lock
, unlock
, and upgrade
methods. Here's how they should function:
Lock(int num, int user)
: Locks the given node for the given user and prevents other users from locking the same node. You may only lock a node if it's unlocked.Unlock(int num, int user)
: Unlocks the given node for the given user. You may only unlock a node if it's currently locked by the same user.Upgrade(int num, int user)
: Locks the given node for the given user and unlocks all of its descendants, regardless of who locked them. You may only upgrade a node if all three of these conditions are met:
For example, given parent = [-1, 0, 0, 1, 1, 2, 2]
, demonstrate how the LockingTree
class would function:
LockingTree lockingTree = new LockingTree([-1, 0, 0, 1, 1, 2, 2]);
lockingTree.lock(2, 2); // return true, node 2 locked by user 2
lockingTree.unlock(2, 3); // return false, user 3 can't unlock node 2
lockingTree.unlock(2, 2); // return true, node 2 unlocked
lockingTree.lock(4, 5); // return true, node 4 locked by user 5
lockingTree.upgrade(0, 1); // return true, node 0 locked by user 1, node 4 unlocked
lockingTree.lock(0, 1); // return false, node 0 already locked
class LockingTree:
def __init__(self, parent: List[int]):
self.parent = parent
self.n = len(parent)
self.locked = [0] * self.n # 0: unlocked, user_id: locked by user_id
self.children = [[] for _ in range(self.n)]
for i in range(1, self.n):
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
p = self.parent[num]
while p != -1:
if self.locked[p] != 0:
return False
p = self.parent[p]
# Check for locked descendants and unlock them
has_locked_descendant = False
locked_descendants = []
def dfs(node):
nonlocal has_locked_descendant
if self.locked[node] != 0:
has_locked_descendant = True
locked_descendants.append(node)
for child in self.children[node]:
dfs(child)
dfs(num)
if not has_locked_descendant:
return False
# Unlock descendants
for node in locked_descendants:
self.locked[node] = 0
# Lock the current node
self.locked[num] = user
return True
# Your LockingTree object will be instantiated and called as such:
# obj = LockingTree(parent)
# param_1 = obj.lock(num,user)
# param_2 = obj.unlock(num,user)
# param_3 = obj.upgrade(num,user)
A naive solution would involve directly implementing the given functions lock
, unlock
, and upgrade
as described in the problem statement. For the upgrade
function, one could perform a depth-first search (DFS) to identify and unlock all descendants. The time complexity of the naive solution could be significant, especially for the upgrade
operation, as it might involve traversing a large portion of the tree.
The provided Python code represents an optimized solution for the problem. It uses the following strategies:
__init__(self, parent)
: Initializes the tree structure using the parent
array. It also precomputes the children for each node to facilitate efficient traversal during the upgrade operation. A locked
array is used to keep track of the lock status of each node.lock(self, num, user)
: Checks if the node num
is unlocked. If it is, it locks the node for the given user
and returns True
. Otherwise, it returns False
.unlock(self, num, user)
: Checks if the node num
is locked by the given user
. If it is, it unlocks the node and returns True
. Otherwise, it returns False
.upgrade(self, num, user)
: This is the most complex operation. It first checks if the node num
is unlocked and has no locked ancestors. Then, it performs a DFS to find all locked descendants. If such descendants exist, it unlocks them and locks the node num
for the given user
. The use of DFS ensures that all descendants are visited to check for locked nodes.__init__(self, parent)
: O(n), where n is the number of nodes, because it iterates through the parent array to build the children list.lock(self, num, user)
: O(1), as it only involves checking and updating the locked
array at the given index.unlock(self, num, user)
: O(1), similar to the lock
function, it only involves checking and updating the locked
array.upgrade(self, num, user)
: O(n) in the worst case, where n is the number of nodes. This is because, in the worst-case scenario, the DFS might have to visit all the nodes in the tree to find locked descendants. The ancestor check also takes O(h) time where h is the height of the tree, which can be O(n) in worst case.__init__(self, parent)
: O(n), where n is the number of nodes. This is because the locked
array and the children
list both store information for each node in the tree.lock(self, num, user)
: O(1), as it doesn't use any additional space.unlock(self, num, user)
: O(1), similar to the lock
function, it doesn't use any additional space.upgrade(self, num, user)
: O(n) in the worst case due to the recursion stack of the DFS and the locked_descendants
list. In the worst case, the recursion stack can grow up to the height of the tree, and the locked_descendants
list can contain all nodes.upgrade
function might enter an infinite loop. Input validation should be performed to detect cycles.