CF 106431C - Tao of Trees (search trees)
The problem asks us to maintain a binary search tree while a sequence of commands is executed. The tree starts empty.
CF 106431C - Tao of Trees (search trees)
Rating: -
Tags: -
Solve time: 37s
Verified: yes
Solution
Problem Understanding
The problem asks us to maintain a binary search tree while a sequence of commands is executed. The tree starts empty. An insertion command places a new key into the correct leaf position according to normal binary search tree rules, while ignoring the operation if the key is already present. A deletion command removes a key if it exists, using the standard BST deletion rule: replace a node with two children by its inorder predecessor. A print command asks us to output the current tree using a recursive parenthesized representation.
The input is a stream of operations rather than a single static tree. Each operation changes the structure that later operations observe. The output is the exact shape of the tree after every print request, so storing only the set of keys is not enough. The parent and child relationships must be preserved.
The constraints are designed around simulation. The number of operations can be large enough that rebuilding the whole tree after every command would be wasteful. If there are around 100000 operations, an approach that scans all existing nodes for every operation can reach about 10^10 operations in the worst case, which is far beyond a typical one second limit. The intended approach is to make each tree modification proportional to the current height of the tree. A binary search tree can become a chain, so the worst case height is O(n), but the total number of commands is the real limiting factor and we need to avoid extra full traversals.
The tricky cases are mostly around deletion and printing. For example, deleting a leaf must remove exactly that node.
ADD 5
ADD 3
DEL 3
PRINT
The output is:
5
A careless implementation that only marks nodes as deleted but does not disconnect the parent pointer can still print the old child.
Deleting a node with two children is another common mistake.
ADD 5
ADD 3
ADD 7
ADD 4
DEL 5
PRINT
The correct output is:
4(3,7)
The predecessor of 5 is 4, so 4 replaces 5. An implementation that chooses the successor instead would create a different tree.
An empty tree is also possible.
DEL 10
PRINT
The output is:
The deletion should do nothing because the key does not exist. Printing an empty tree should produce an empty line, not a special marker.
Approaches
The direct solution is to represent every node with its key and pointers to the left and right children. For an insertion, we start at the root and repeatedly compare the new key with the current node until we find an empty child position. This is exactly how a BST is defined, so the procedure is correct. For deletion, we search for the node and then handle the three cases: no child, one child, or two children. Printing is just a recursive traversal that follows the required format.
The brute force approach would be to store all keys in an array and rebuild the tree whenever a command changes it. This would still be correct, because inserting the same keys in the same order reconstructs the same BST. However, rebuilding after every command costs O(n) work. With up to 100000 operations, this can become roughly O(n²), which is too slow.
The key observation is that a BST operation only needs to inspect the path from the root to the affected key. The rest of the tree is already in a valid state and does not need to be touched. A node deletion also only changes a small number of links around the removed node. This reduces every update to a height traversal.
The remaining issue is printing. A print command has to visit every node because the whole structure must be output. That cost is unavoidable, since the output itself contains the entire tree.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | O(q²) in the worst case | O(n) | Too slow |
| Optimal | O(qh + total printed nodes) | O(n) | Accepted |
Here q is the number of commands and h is the current tree height.
Algorithm Walkthrough
- Store the tree using nodes that contain a key and references to their left and right children. Keep a reference to the root node. The tree structure itself is the information we need for future operations.
- For an ADD operation, start from the root and compare the new key with each visited node. Move left for a smaller key and right for a larger key. When the next child pointer is empty, attach the new node there. If the key is found during the search, stop because duplicates are ignored.
- For a DEL operation, search for the target key while remembering the parent node. If the key is missing, return immediately. If the node has no children, remove the corresponding parent pointer. If it has one child, connect the parent directly to that child.
- If the deleted node has two children, find the rightmost node in its left subtree. This node is the largest key smaller than the deleted key, so replacing the deleted node with it keeps the BST ordering valid. Remove the predecessor from its old location and reconnect the children.
- For a PRINT operation, recursively produce the representation. A missing node contributes an empty string. A leaf contributes only its key. Any other node contributes its key followed by the representations of its children.
Why it works: the invariant is that after every modification, every node still satisfies the binary search tree ordering rule. Insertions preserve it because the new node is placed exactly at the first valid empty position. Deletions preserve it because replacing a two child node with its inorder predecessor keeps all left subtree values smaller and all right subtree values larger. Since printing only reads the maintained structure, the output always describes the current tree.
Python Solution
import sys
input = sys.stdin.readline
sys.setrecursionlimit(1 << 25)
class Node:
__slots__ = ("key", "left", "right")
def __init__(self, key):
self.key = key
self.left = None
self.right = None
root = None
def add(key):
global root
if root is None:
root = Node(key)
return
cur = root
while True:
if key == cur.key:
return
if key < cur.key:
if cur.left is None:
cur.left = Node(key)
return
cur = cur.left
else:
if cur.right is None:
cur.right = Node(key)
return
cur = cur.right
def delete(key):
global root
parent = None
cur = root
while cur is not None and cur.key != key:
parent = cur
if key < cur.key:
cur = cur.left
else:
cur = cur.right
if cur is None:
return
if cur.left is None:
child = cur.right
if parent is None:
root = child
elif parent.left is cur:
parent.left = child
else:
parent.right = child
elif cur.right is None:
child = cur.left
if parent is None:
root = child
elif parent.left is cur:
parent.left = child
else:
parent.right = child
else:
pred_parent = cur
pred = cur.left
while pred.right is not None:
pred_parent = pred
pred = pred.right
cur.key = pred.key
if pred_parent.left is pred:
pred_parent.left = pred.left
else:
pred_parent.right = pred.left
def build(cur):
if cur is None:
return ""
if cur.left is None and cur.right is None:
return str(cur.key)
return str(cur.key) + "(" + build(cur.left) + "," + build(cur.right) + ")"
def solve(data):
global root
root = None
ans = []
for line in data.splitlines():
if not line:
continue
parts = line.split()
if parts[0] == "ADD":
add(int(parts[1]))
elif parts[0] == "DEL":
delete(int(parts[1]))
else:
ans.append(build(root))
return "\n".join(ans)
data = sys.stdin.read()
print(solve(data))
The Node class stores only the three pieces of information required for a BST. Using iterative insertion and deletion avoids recursion depth issues when the tree becomes a chain.
The insertion routine walks down the tree until it finds a missing pointer. The equality check is done before moving, which prevents duplicate keys from being inserted.
Deletion is the most delicate part. The search keeps the parent because changing the tree requires modifying the pointer that leads to the removed node. In the two child case, the predecessor is found by moving left once and then going as far right as possible. The predecessor cannot have a right child, so after copying its key into the deleted node, it can be removed like a zero or one child case.
The printing function is recursive because the requested format is naturally recursive. The empty string handling is what allows missing children inside a non leaf node to appear correctly, such as 5(3,).
Worked Examples
Consider this sequence:
ADD 5
ADD 3
ADD 7
PRINT
DEL 5
PRINT
The execution state is:
| Step | Operation | Root | Printed tree |
|---|---|---|---|
| 1 | ADD 5 | 5 | |
| 2 | ADD 3 | 5 | |
| 3 | ADD 7 | 5 | |
| 4 | 5 | 5(3,7) | |
| 5 | DEL 5 | 7 | |
| 6 | 7 | 3(,7) |
The deletion replaces 5 with its predecessor 3. The subtree ordering remains valid because everything left of 3 is smaller and everything right of 3 is larger.
A second example:
ADD 8
ADD 2
ADD 4
ADD 10
DEL 2
PRINT
DEL 100
PRINT
| Step | Operation | Root | Printed tree |
|---|---|---|---|
| 1 | ADD 8 | 8 | |
| 2 | ADD 2 | 8 | |
| 3 | ADD 4 | 8 | |
| 4 | ADD 10 | 8 | |
| 5 | DEL 2 | 8 | |
| 6 | 8 | 8(4,10) | |
| 7 | DEL 100 | 8 | |
| 8 | 8 | 8(4,10) |
The second deletion has no effect because the key is absent. The trace confirms that invalid operations leave the tree unchanged.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(qh + P) | Each update follows one root to leaf path. Printing costs P, the number of nodes written across all print commands. |
| Space | O(n) | Each existing key occupies one node. |
The solution fits the constraints because it never rebuilds the tree. The only full traversals happen when the output explicitly requires the entire structure.
Test Cases
import sys
import io
def run(inp: str) -> str:
return solve(inp).strip()
assert run("""ADD 5
ADD 3
ADD 7
PRINT
""") == "5(3,7)", "basic insert"
assert run("""ADD 5
ADD 3
ADD 7
DEL 5
PRINT
""") == "3(,7)", "delete root with two children"
assert run("""PRINT
DEL 1
PRINT
""") == "", "empty tree"
assert run("""ADD 1
ADD 2
ADD 3
DEL 2
PRINT
""") == "1(,3)", "delete node with one child"
assert run("""ADD 10
ADD 5
ADD 15
ADD 12
DEL 100
PRINT
""") == "10(5,15(12,))", "delete missing key"
| Test input | Expected output | What it validates |
|---|---|---|
| Three node insertion | 5(3,7) |
Basic BST construction |
| Removing the root | 3(,7) |
Two child deletion |
| Printing empty tree | empty line | Empty representation |
| Removing a chain node | 1(,3) |
One child replacement |
| Deleting absent value | 10(5,15(12,)) |
Ignored invalid operations |
Edge Cases
For the empty tree case, the input
DEL 10
PRINT
starts with no root. The deletion search immediately fails, so the root remains empty. Printing calls the recursive function on a missing node and returns an empty string.
For two child deletion, consider:
ADD 6
ADD 2
ADD 9
ADD 4
DEL 6
PRINT
The node 6 has children on both sides. The algorithm finds 4 as the rightmost node of the left subtree, copies its key into the root, and removes the old 4 position. The output becomes:
4(2,9)
A solution that simply removes 6 would lose both subtrees.
For duplicate insertion, consider:
ADD 5
ADD 5
PRINT
The second insertion reaches the existing node with key 5 and stops. The tree remains:
5
This matters because inserting duplicates would break the assumption that every key appears once, which the deletion logic relies on.