Operations on Tree — Solution in Python, Java & C++
Operations on Tree solution explained: step-by-step approach, complexity and code in Python, Java, C++ and JavaScript. A medium problem.
Operations on Tree is a medium coding problem on arrays, hash table and depth-first search.
- Difficulty: Medium
- Topics: Arrays, Hash Table, Depth-First Search, Breadth-First Search, Trees
- Time limit: 2 s
- Memory limit: 256 MB
- Languages: JavaScript, TypeScript, Python, Java, C++, C, C#, Go, Kotlin, Swift, Rust, PHP and Ruby
Problem statement
A tree of n nodes, numbered 0 to n - 1, is given as a parent array: parent[i] is the parent of node i, and the root 0 has parent[0] == -1. Users lock and unlock its nodes under these rules:
- Lock — a node can be locked by a user only if it is currently unlocked. It then belongs to that user.
- Unlock — a node can be unlocked only by the user who locked it.
- Upgrade — a user may lock a node and at the same time unlock all of its descendants, but only if all three hold: the node is unlocked, it has at least one locked descendant (locked by anyone), and none of its ancestors is locked.
Write the class LockingTree:
LockingTree(int[] parent)builds the tree, all nodes unlocked.boolean lock(int num, int user)locks nodenumforuserif allowed; returns whether it did.boolean unlock(int num, int user)unlocks nodenumifuserholds it; returns whether it did.boolean upgrade(int num, int user)performs the upgrade onnumforuserif allowed; returns whether it did.
The judge calls the methods in the order listed in the first input line, with the arguments in the second; the expected output holds each call's result, null for the constructor and for every method that returns nothing.
Example 1
Input: ["LockingTree","lock","unlock","unlock","lock","upgrade","lock"]
[[[-1,0,0,1,1,2,2]],[2,2],[2,3],[2,2],[4,5],[0,1],[0,1]]
Output: [null,true,false,true,true,true,false]
Explanation: Node 2 is locked by user 2; user 3 cannot unlock it, user 2 can. After user 5 locks node 4, user 1 upgrades the root: it is unlocked, has the locked descendant 4 and no ancestors, so node 4 is unlocked and the root locked for user 1. Locking the root again then fails.
Example 2
Input: ["LockingTree","lock","upgrade","upgrade","lock","upgrade"]
[[[-1,0,1,2]],[3,7],[2,4],[0,9],[1,5],[2,6]]
Output: [null,true,true,true,true,false]
Explanation: The tree is the chain 0-1-2-3 and node 3 is locked. Upgrading 2 succeeds and unlocks 3. Upgrading 0 then succeeds too — 2 is its locked descendant, and is unlocked by it. Once node 1 is locked, upgrading 2 fails: its ancestors 1 and 0 are locked.
Constraints
n == parent.length2 <= n <= 20000 <= parent[i] <= n - 1 for i != 0, and parent[0] == -1parent describes a valid tree0 <= num <= n - 11 <= user <= 10^4At most 2000 calls are made to lock, unlock and upgrade in total
Hints for Operations on Tree
Hint 1
Keep, for each node, the user who holds it (0 for unlocked). Lock and unlock are then single checks.
Hint 2
For upgrade, the ancestor condition is a walk up the parent pointers. The descendant condition needs to walk down — build each node's list of children once in the constructor.
Hint 3
Check the node and its ancestors first. Then traverse the subtree below
num(DFS or BFS), unlocking every locked node you meet and noting whether there was one; if there was none, nothing changed and the answer is false, otherwise locknumfor the user.
How to solve Operations on Tree
Store the locking user per node and each node's children. Upgrade is a walk up (ancestors) followed by a traversal down (descendants), both bounded by the tree's size.
Approach
- Constructor:
lockedBy[v] = 0for every node;children[parent[i]].append(i)fori >= 1. lock(num, user): iflockedBy[num] != 0return false; else set it touserand return true.unlock(num, user): iflockedBy[num] != userreturn false; else set it to 0 and return true.upgrade(num, user): return false ifnumis locked or any node on the pathparent[num], parent[parent[num]], …is locked.- Traverse the subtree under
numwith a stack: every locked node found is unlocked and counted. If the count is 0 return false; otherwise setlockedBy[num] = userand return true.
Why it works
The three upgrade conditions are checked exactly as stated: the first two reads leave the tree unchanged when they fail, and the traversal unlocks descendants only — which is harmless when none are locked, since then nothing is modified before the false is returned. Unlocking during the same traversal that searches for a locked descendant saves a second pass.
Complexity
- Time —
O(1) for lock and unlock, O(n) for upgrade - Space —
O(n)
Pitfalls
- Unlock must compare against the user who locked the node; any other user fails even if the node is locked.
- Upgrade needs a locked descendant; a locked node elsewhere in the tree does not count, and neither does
numitself. - Check the ancestors before unlocking anything — otherwise a failing upgrade would already have changed descendants.
Reference solution
Python
class LockingTree:
def __init__(self, parent: List[int]):
self.parent = parent
self.children = [[] for _ in parent]
for v in range(1, len(parent)):
self.children[parent[v]].append(v)
self.locked_by = [0] * len(parent) # 0 = unlocked
def lock(self, num: int, user: int) -> bool:
if self.locked_by[num]:
return False
self.locked_by[num] = user
return True
def unlock(self, num: int, user: int) -> bool:
if self.locked_by[num] != user:
return False
self.locked_by[num] = 0
return True
def upgrade(self, num: int, user: int) -> bool:
lb = self.locked_by
if lb[num]:
return False
a = self.parent[num]
while a != -1: # no locked ancestor
if lb[a]:
return False
a = self.parent[a]
found = False
stack = list(self.children[num])
while stack: # unlock every locked descendant
v = stack.pop()
if lb[v]:
lb[v] = 0
found = True
stack.extend(self.children[v])
if not found:
return False
lb[num] = user
return TrueJava
class LockingTree {
private int[] parent, lockedBy; // lockedBy 0 = unlocked
private List<List<Integer>> children = new ArrayList<>();
public LockingTree(int[] parent) {
this.parent = parent;
lockedBy = new int[parent.length];
for (int v = 0; v < parent.length; v++) children.add(new ArrayList<>());
for (int v = 1; v < parent.length; v++) children.get(parent[v]).add(v);
}
public boolean lock(int num, int user) {
if (lockedBy[num] != 0) return false;
lockedBy[num] = user;
return true;
}
public boolean unlock(int num, int user) {
if (lockedBy[num] != user) return false;
lockedBy[num] = 0;
return true;
}
public boolean upgrade(int num, int user) {
if (lockedBy[num] != 0) return false;
for (int a = parent[num]; a != -1; a = parent[a]) if (lockedBy[a] != 0) return false; // no locked ancestor
boolean found = false;
Deque<Integer> stack = new ArrayDeque<>(children.get(num));
while (!stack.isEmpty()) { // unlock every locked descendant
int v = stack.pop();
if (lockedBy[v] != 0) { lockedBy[v] = 0; found = true; }
for (int c : children.get(v)) stack.push(c);
}
if (!found) return false;
lockedBy[num] = user;
return true;
}
}C++
class LockingTree {
vector<int> parent, lockedBy; // lockedBy 0 = unlocked
vector<vector<int>> children;
public:
LockingTree(vector<int>& parent) : parent(parent), lockedBy(parent.size(), 0), children(parent.size()) {
for (int v = 1; v < (int)parent.size(); v++) children[parent[v]].push_back(v);
}
bool lock(int num, int user) {
if (lockedBy[num]) return false;
lockedBy[num] = user;
return true;
}
bool unlock(int num, int user) {
if (lockedBy[num] != user) return false;
lockedBy[num] = 0;
return true;
}
bool upgrade(int num, int user) {
if (lockedBy[num]) return false;
for (int a = parent[num]; a != -1; a = parent[a]) if (lockedBy[a]) return false; // no locked ancestor
bool found = false;
vector<int> stack(children[num].begin(), children[num].end());
while (!stack.empty()) { // unlock every locked descendant
int v = stack.back();
stack.pop_back();
if (lockedBy[v]) { lockedBy[v] = 0; found = true; }
for (int c : children[v]) stack.push_back(c);
}
if (!found) return false;
lockedBy[num] = user;
return true;
}
};JavaScript
var LockingTree = function(parent) {
this.parent = parent;
this.children = parent.map(function() { return []; });
for (var v = 1; v < parent.length; v++) this.children[parent[v]].push(v);
this.lockedBy = new Array(parent.length).fill(0); // 0 = unlocked
};
LockingTree.prototype.lock = function(num, user) {
if (this.lockedBy[num]) return false;
this.lockedBy[num] = user;
return true;
};
LockingTree.prototype.unlock = function(num, user) {
if (this.lockedBy[num] !== user) return false;
this.lockedBy[num] = 0;
return true;
};
LockingTree.prototype.upgrade = function(num, user) {
var lb = this.lockedBy;
if (lb[num]) return false;
for (var a = this.parent[num]; a !== -1; a = this.parent[a]) if (lb[a]) return false; // no locked ancestor
var found = false, stack = this.children[num].slice();
while (stack.length) { // unlock every locked descendant
var v = stack.pop();
if (lb[v]) { lb[v] = 0; found = true; }
for (var i = 0; i < this.children[v].length; i++) stack.push(this.children[v][i]);
}
if (!found) return false;
lb[num] = user;
return true;
};Also on the editorial tab: C, C#, Go, Kotlin, PHP, Ruby, Rust, Swift, TypeScript.
All 1199 arrays problems · the whole catalogue
Learn the technique: Arrays · Hashing: Hash Maps and Hash Sets