Count and Print Changed Nodes in DoorDash Menu Tree
At DoorDash, menus are updated daily and even hourly to keep them up-to-date. Each menu can be regarded as a tree. When the merchant sends the latest menu, calculate how many nodes have changed, been added, or been deleted.
Each node in a tree is represented using the following string format:
"key,value,children" Here:
key is the node key.
value is the node value.
children is the list of direct child keys separated by |.
- Child keys within the same
children list are distinct.
- For a leaf node,
children is empty.
- Tree will always be connected. i.e. every node is always reachable from root
Example:
"a,1,b|c" means node a has value 1 and direct children b and c.
"f,66," means node f has value 66 and no children.
Assume:
- There are no duplicate node keys within the same tree.
- All
key and value strings use only lowercase letters and digits.
- Parent-child relationships are derived from the
children lists.
- The order of node strings inside
existingTree and newTree does not matter.
Methods
Count Changed Nodes
int countChangedNodes(List<String> existingTree, List<String> newTree)
existingTree and newTree contain node strings in the format "key,value,children".
- Return the total number of change records.
- A node is counted as changed if it is deleted, added, or its value changes.
- If a node appears in both trees with the same
key but under a different immediate parent, treat it as one deleted node and one added node.
- If a node is deleted or added, its descendants are also compared independently, and each deleted or added descendant is counted separately.
- Children are ordered in ascending order of their keys lexicographically.
Print Changes
List<String> printChanges(List<String> existingTree, List<String> newTree)
- Return all changes using the formats below:
"DELETED:key,value,parentKey"
"ADDED:key,value,parentKey"
"UPDATED_VALUE:key,oldValue,newValue,parentKey"
- Use
ROOT as the parent key for the root node.
- Return deleted entries first, then added entries, then updated-value entries.
- Within each group, sort lexicographically by
key.
Change Rules
- If a node exists in
existingTree but not in newTree, it is deleted.
- If a node exists in
newTree but not in existingTree, it is added.
- If a node exists in both trees with the same
key and same immediate parent, but different value, it is a changed node.
- If a node exists in both trees with the same
key and same value, but its immediate parent changes, treat the old node as deleted and the new node as added.
- If a node's immediate parent changes, treat it only as one deleted node and one added node, even if its
value also changes. Do not also emit UPDATED_VALUE for that node.
- A node is not considered changed merely because its child list differs. Child-list differences matter only through node additions, deletions, or re-parenting.
- Parent comparison is based only on the immediate parent key, not on higher ancestors.
- The count returned by
countChangedNodes is the total number of change records, not the number of distinct keys.
Constraints
0 ≤ existingTree.size(), newTree.size() ≤ 100000
1 ≤ key.length(), value.length() ≤ 100
key and value contain only characters in [a-z0-9].
- Each node string is always in the format
"key,value,children".
- Each tree has unique keys.
- Each non-root node appears as a child of exactly one parent.
- If a tree is non-empty, it has exactly one root.
- Every child key listed in
children must also appear as a node key in the same tree.
Examples
Example 1
Existing tree:
a(1) / \ b(2) c(3) / \ \ d(4) e(5) f(6) New tree:
a(1) | c(3) | f(66) countChangedNodes( existingTree = ["a,1,b|c", "b,2,d|e", "c,3,f", "d,4,", "e,5,", "f,6,"], newTree = ["a,1,c", "c,3,f", "f,66,"] ) = 4 printChanges( existingTree = ["a,1,b|c", "b,2,d|e", "c,3,f", "d,4,", "e,5,", "f,6,"], newTree = ["a,1,c", "c,3,f", "f,66,"] ) = ["DELETED:b,2,a", "DELETED:d,4,b", "DELETED:e,5,b", "UPDATED_VALUE:f,6,66,c"] Explanation:
- Node
b, d, and e are deleted.
- When
b disappears, its descendants d and e are also compared independently and counted separately as deleted nodes.
- The value of node
f changes from 6 to 66.
- Total change records =
4.
Example 2
Existing tree:
a(1) / \ b(2) c(3) / \ \ d(4) e(5) g(7) New tree:
a(1) / \ b(2) h(8) / | \ \ d(4) e(5) f(6) g(7) countChangedNodes( existingTree = ["a,1,b|c", "b,2,d|e", "c,3,g", "d,4,", "e,5,", "g,7,"], newTree = ["a,1,b|h", "b,2,d|e|f", "h,8,g", "d,4,", "e,5,", "f,6,", "g,7,"] ) = 5 printChanges( existingTree = ["a,1,b|c", "b,2,d|e", "c,3,g", "d,4,", "e,5,", "g,7,"], newTree = ["a,1,b|h", "b,2,d|e|f", "h,8,g", "d,4,", "e,5,", "f,6,", "g,7,"] ) = ["DELETED:c,3,a", "DELETED:g,7,c", "ADDED:f,6,b", "ADDED:g,7,h", "ADDED:h,8,a"] Explanation:
- Node
f is newly added under b.
- Node
c is deleted.
- Node
g changes immediate parent from c to h, so it is treated as one deleted node and one added node.
- The children list
d|e|f is written in ascending lexicographic order.
- Total change records =
5.
Example 3
Existing tree:
a(1) New tree:
a(1) / \ b(2) d(4) | c(3) countChangedNodes( existingTree = ["a,1,"], newTree = ["a,1,b|d", "b,2,c", "c,3,", "d,4,"] ) = 3 printChanges( existingTree = ["a,1,"], newTree = ["a,1,b|d", "b,2,c", "c,3,", "d,4,"] ) = ["ADDED:b,2,a", "ADDED:c,3,b", "ADDED:d,4,a"] Explanation:
- Node
b is added under a.
- Node
c is also counted separately because descendants of an added node are compared independently.
- Node
d is another added node under a.
- The root children list is
b|d, which is in ascending lexicographic order.
- Total change records =
3.
Example 4
Existing tree:
m(1) / | \ b(2) d(4) f(6) New tree:
m(1) / | \ a(1) d(44) e(5) countChangedNodes( existingTree = ["m,1,b|d|f", "b,2,", "d,4,", "f,6,"], newTree = ["m,1,a|d|e", "a,1,", "d,44,", "e,5,"] ) = 5 printChanges( existingTree = ["m,1,b|d|f", "b,2,", "d,4,", "f,6,"], newTree = ["m,1,a|d|e", "a,1,", "d,44,", "e,5,"] ) = ["DELETED:b,2,m", "DELETED:f,6,m", "ADDED:a,1,m", "ADDED:e,5,m", "UPDATED_VALUE:d,4,44,m"] Explanation:
- Deleted entries come first and are sorted by key, so
b appears before f.
- Added entries come next and are sorted by key, so
a appears before e.
- The updated-value entry for
d comes after all deleted and added entries.
- The new root children list is
a|d|e, which is in ascending lexicographic order.
- Total change records =
5.