You are given a tree with n nodes numbered from 0 to n - 1. You are given a 0-indexed integer array parent of length n, where parent[i] is the parent of node i. The tree's root is the node with parent[i] == -1.
You are also given an integer array degree of length n, where degree[i] is the degree of the ith node.
You will mark the nodes in the tree in multiple rounds. In each round, you mark all unmarked nodes that satisfy the following conditions:
degree.Return an array containing all the marked nodes in the last round.
You can return the answer in any order.
Example 1:
Input: parent = [-1,0,0,1,1,2,2,3,3,4,4,5,5], degree = [0,1,2] Output: [6,7,8,9,10,11,12] Explanation: The graph is shown above. In the first round, nodes [6,7,8,9,10,11,12] are marked because they are leaf nodes and their degrees are in the array degree. So, the answer is [6,7,8,9,10,11,12].
Example 2:
Input: parent = [-1,0,0,1,1,2,2,3,3,4,4,5,5], degree = [2] Output: [] Explanation: The graph is shown above. There is no leaf node with degree 2. So, the answer is an empty array.
Constraints:
2 <= n <= 105parent.length == n-1 <= parent[i] < ndegree.length == n0 <= degree[i] <= nparent represents a valid tree.When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
Imagine you're exploring a maze and want to find the last rooms you visit before escaping. The brute force approach means we try marking rooms based on every single possible order until we've checked all the nodes.
Here's how the algorithm would work step-by-step:
import itertools
def find_last_marked_nodes_brute_force(tree):
nodes = list(tree.keys())
all_permutations = list(itertools.permutations(nodes))
last_marked_nodes_for_all_orders = []
for marking_order in all_permutations:
last_marked_nodes_for_current_order = simulate_marking(tree, marking_order)
last_marked_nodes_for_all_orders.append(last_marked_nodes_for_current_order)
final_result = determine_final_last_marked_nodes(last_marked_nodes_for_all_orders)
return final_result
def simulate_marking(tree, marking_order):
marked_nodes = set()
last_marked_nodes = []
for node_to_mark in marking_order:
marked_nodes.add(node_to_mark)
is_removable = False
for neighbor in tree[node_to_mark]:
if neighbor in marked_nodes:
is_removable = True
break
if not is_removable:
last_marked_nodes.append(node_to_mark)
return last_marked_nodes
def determine_final_last_marked_nodes(all_last_marked_nodes):
# Find the intersection of last marked nodes across all permutations
if not all_last_marked_nodes:
return []
# Use the first list as the basis for intersection.
intersection_of_last_marked_nodes = set(all_last_marked_nodes[0])
# Iteratively find the intersection with other permutations' results.
for last_marked_nodes in all_last_marked_nodes[1:]:
intersection_of_last_marked_nodes.intersection_update(last_marked_nodes)
return list(intersection_of_last_marked_nodes)
def example_usage():
# Example tree structure represented as an adjacency list.
example_tree = {
'A': ['B', 'C'],
'B': ['A', 'D'],
'C': ['A', 'E'],
'D': ['B'],
'E': ['C']
}
result = find_last_marked_nodes_brute_force(example_tree)
print(f"Last marked nodes: {result}")
if __name__ == "__main__":
example_usage()This problem asks us to figure out which nodes in a tree are the last to be marked given a specific marking process. The key insight is to simulate the marking process in reverse, starting from the end to efficiently determine the last marked nodes.
Here's how the algorithm would work step-by-step:
def find_last_marked_nodes(tree, marking_threshold): last_marked_nodes = []
# Helper function to determine if a node is a potential last marked node.
def is_potential_last_marked(node): if not node:
return False
if not node.children:
return True
# Check if the node satisfies criteria for being last marked.
unmarked_children_count = 0
for child in node.children:
if not child.marked:
unmarked_children_count += 1
if unmarked_children_count <= (len(node.children) - marking_threshold):
return True
return False
# Need to explore all nodes.
def explore_tree(node):
if not node:
return
for child in node.children:
explore_tree(child)
# Add potential last marked nodes.
if is_potential_last_marked(node):
last_marked_nodes.append(node)
explore_tree(tree)
# Filter out nodes that depend on descendants' marking.
final_last_marked_nodes = []
for node in last_marked_nodes:
is_truly_last = True
for other_node in last_marked_nodes:
if node != other_node and is_ancestor(node, other_node):
is_truly_last = False
break
if is_truly_last:
final_last_marked_nodes.append(node)
return final_last_marked_nodes
def is_ancestor(node, potential_descendant):
if not node or not potential_descendant:
return False
current = potential_descendant
while current:
if current == node:
return True
current = current.parent
return False| Case | How to Handle |
|---|---|
| Null or empty edges array | Return an empty list as there are no edges and thus no tree. |
| n is 0 or negative | Return an empty list as the number of nodes must be positive. |
| n is 1, and edges is empty | Return a list containing only node 0, as it's the only node and therefore a leaf in the initial state. |
| Fully connected graph (complete graph) | The algorithm should correctly identify leaves and remove them iteratively, eventually resulting in the last marked nodes. |
| Star graph (one central node connected to all others) | The algorithm should remove all leaf nodes (outer nodes) in the first round, leaving only the central node. |
| Linear chain of nodes (a single path) | The algorithm should remove the ends of the chain in each round, converging towards the center. |
| Large n with edges representing a tree, but high degree nodes exist | The solution should scale efficiently, likely requiring adjacency list representation rather than adjacency matrix to avoid memory issues and speed up neighbor lookups. |
| Duplicate edges in the edges array | The algorithm should treat duplicate edges as a single edge, which is inherently handled by degree tracking. |