Facebook Pixel

582. Kill Process 🔒

Problem Description

You have n processes that form a rooted tree structure. Each process has a unique ID and exactly one parent process (except for the root process).

You're given two arrays:

  • pid[i]: the ID of the i-th process
  • ppid[i]: the ID of the i-th process's parent

The root process has ppid[i] = 0, meaning it has no parent.

When a process is killed, all of its descendant processes (children, grandchildren, etc.) are also killed automatically.

Given an integer kill representing the ID of a process you want to kill, return a list containing all process IDs that will be killed as a result. The order of IDs in the output doesn't matter.

How the solution works:

The solution builds a graph g where each key represents a parent process ID and its value is a list of direct child process IDs. This is done by iterating through the pid and ppid arrays together - for each process i with parent p, we add i to g[p].

Then, starting from the process we want to kill, we perform a depth-first search (DFS) to traverse the entire subtree rooted at that process. During the traversal, we collect all visited process IDs in the ans list. The DFS ensures we visit the killed process and all its descendants, giving us the complete list of processes that will be terminated.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem involves processes with parent-child relationships, which forms a graph structure. Each process can be viewed as a node, and the parent-child relationships are edges.

Is it a tree?

  • Yes: The problem explicitly states we have "n processes forming a rooted tree structure." Each process has exactly one parent (except the root), and there's a single root with ppid[i] = 0. This confirms we're dealing with a tree.

DFS

  • Since we identified this as a tree problem, the flowchart leads us directly to DFS as the solution approach.

Why DFS is appropriate here:

  • We need to traverse from a starting node (the process to kill) and visit all its descendants
  • The tree structure guarantees no cycles, making DFS safe and efficient
  • DFS naturally explores the entire subtree rooted at the killed process, which is exactly what we need - to find all child processes that will also be killed

Conclusion: The flowchart correctly guides us to use DFS for this tree traversal problem. Starting from the process we want to kill, DFS will systematically visit all descendant processes in the tree, giving us the complete list of processes that will be terminated.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we think about this problem, we need to understand what happens when a process is killed - it creates a cascading effect where all its children die, then all their children die, and so on. This is like cutting a branch from a tree - when you cut it, everything attached to that branch falls off too.

The key insight is that we need to find all processes in the subtree rooted at the process we want to kill. Since each process can have multiple children but only one parent, we're dealing with a tree structure where information flows from parent to children.

The input gives us parent-child relationships in a "bottom-up" format (each child knows its parent through ppid), but to solve this problem efficiently, we need to think "top-down" (each parent should know its children). This is why we first build an adjacency list g where g[parent] contains all direct children of that parent.

Once we have this parent-to-children mapping, finding all processes that will be killed becomes a classic tree traversal problem. Starting from the process to kill, we need to visit it and all its descendants. DFS is perfect for this because:

  1. It naturally explores one branch completely before moving to another
  2. The recursive nature of DFS mirrors the recursive structure of the problem (kill a process → kill its children → kill their children, etc.)
  3. We can collect all visited nodes during the traversal, which gives us our answer

The beauty of this approach is that we're essentially asking: "What are all the nodes in the subtree rooted at the kill node?" - and DFS answers this question directly by visiting exactly those nodes.

Learn more about Tree, Depth-First Search and Breadth-First Search patterns.

Solution Approach

Following the DFS approach identified earlier, let's walk through the implementation step by step:

Step 1: Build the Parent-to-Children Graph

First, we need to transform the input data into a more useful structure. The input gives us pid (process IDs) and ppid (parent process IDs), but we need to know which children belong to each parent.

g = defaultdict(list)
for i, p in zip(pid, ppid):
    g[p].append(i)

This creates an adjacency list where:

  • g[p] contains a list of all direct children of process p
  • We use defaultdict(list) to automatically handle cases where a process has no children
  • By zipping pid and ppid, we iterate through each process i and its parent p, adding i as a child of p

Step 2: Implement DFS Traversal

The DFS function visits a process and all its descendants:

def dfs(i: int):
    ans.append(i)      # Add current process to kill list
    for j in g[i]:     # Visit all children of current process
        dfs(j)         # Recursively kill each child

The function works by:

  1. Adding the current process i to our answer list (it will be killed)
  2. Looking up all direct children of process i from our graph g
  3. Recursively calling dfs on each child, which will in turn visit their children

Step 3: Execute the Solution

ans = []
dfs(kill)
return ans

We initialize an empty list ans to collect all processes that will be killed, start the DFS from the process we want to kill, and return the collected list.

Time Complexity: O(n) where n is the number of processes, as we visit each process at most once.

Space Complexity: O(n) for storing the graph and the recursion stack in the worst case (when the tree is skewed).

The solution elegantly handles the cascading nature of process termination by leveraging the tree structure and DFS's natural ability to explore all nodes in a subtree.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the solution works.

Example Input:

  • pid = [1, 3, 10, 5]
  • ppid = [3, 0, 5, 3]
  • kill = 5

This represents the following process tree:

        3 (root)
       / \
      1   5
           \
            10

Step 1: Build the Parent-to-Children Graph

We iterate through pid and ppid together:

  • Process 1 has parent 3 → add 1 to g[3]
  • Process 3 has parent 0 → add 3 to g[0]
  • Process 10 has parent 5 → add 10 to g[5]
  • Process 5 has parent 3 → add 5 to g[3]

Resulting graph g:

g = {
    0: [3],      # root's child
    3: [1, 5],   # process 3's children
    5: [10]      # process 5's child
}

Step 2: Execute DFS from kill = 5

Starting DFS traversal:

  1. Visit process 5:

    • Add 5 to ans → ans = [5]
    • Look up children of 5 in graph → g[5] = [10]
    • Recursively visit child 10
  2. Visit process 10:

    • Add 10 to ans → ans = [5, 10]
    • Look up children of 10 in graph → g[10] = [] (no children)
    • Return to parent
  3. Complete traversal:

    • All descendants of process 5 have been visited
    • Final answer: [5, 10]

When we kill process 5, both process 5 and its child process 10 are terminated. The DFS traversal naturally captures this cascading effect by visiting the entire subtree rooted at the killed process.

Solution Implementation

1from typing import List
2from collections import defaultdict
3
4class Solution:
5    def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
6        """
7        Find all processes that need to be killed when killing a specific process.
8        When a process is killed, all its child processes are killed recursively.
9      
10        Args:
11            pid: List of process IDs
12            ppid: List of parent process IDs (ppid[i] is parent of pid[i])
13            kill: The process ID to kill
14          
15        Returns:
16            List of all process IDs that will be killed
17        """
18      
19        # Build adjacency list: parent -> list of children
20        parent_to_children = defaultdict(list)
21        for process_id, parent_id in zip(pid, ppid):
22            parent_to_children[parent_id].append(process_id)
23      
24        # Store all processes to be killed
25        killed_processes = []
26      
27        def depth_first_search(process_id: int) -> None:
28            """
29            Recursively find and mark all processes to be killed starting from given process.
30          
31            Args:
32                process_id: Current process ID to kill
33            """
34            # Add current process to killed list
35            killed_processes.append(process_id)
36          
37            # Recursively kill all child processes
38            for child_process_id in parent_to_children[process_id]:
39                depth_first_search(child_process_id)
40      
41        # Start DFS from the process to be killed
42        depth_first_search(kill)
43      
44        return killed_processes
45
1class Solution {
2    // Graph to store parent-child relationships (parent ID -> list of child IDs)
3    private Map<Integer, List<Integer>> parentToChildrenMap = new HashMap<>();
4    // Result list to store all processes that need to be killed
5    private List<Integer> killedProcesses = new ArrayList<>();
6
7    /**
8     * Kills a process and all its descendant processes.
9     * 
10     * @param pid List of process IDs
11     * @param ppid List of parent process IDs (ppid[i] is parent of pid[i])
12     * @param kill The process ID to kill
13     * @return List of all process IDs that will be killed
14     */
15    public List<Integer> killProcess(List<Integer> pid, List<Integer> ppid, int kill) {
16        int numberOfProcesses = pid.size();
17      
18        // Build the parent-child relationship graph
19        for (int i = 0; i < numberOfProcesses; i++) {
20            int parentId = ppid.get(i);
21            int childId = pid.get(i);
22          
23            // Add child to parent's children list (create list if doesn't exist)
24            parentToChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(childId);
25        }
26      
27        // Perform DFS to find all processes to kill starting from the target process
28        depthFirstSearch(kill);
29      
30        return killedProcesses;
31    }
32
33    /**
34     * Recursively kills a process and all its child processes using DFS.
35     * 
36     * @param processId The current process ID to kill
37     */
38    private void depthFirstSearch(int processId) {
39        // Add current process to the killed list
40        killedProcesses.add(processId);
41      
42        // Recursively kill all child processes
43        for (int childProcessId : parentToChildrenMap.getOrDefault(processId, Collections.emptyList())) {
44            depthFirstSearch(childProcessId);
45        }
46    }
47}
48
1class Solution {
2public:
3    vector<int> killProcess(vector<int>& pid, vector<int>& ppid, int kill) {
4        // Build adjacency list where key is parent process ID and value is list of child process IDs
5        unordered_map<int, vector<int>> parentToChildren;
6        int processCount = pid.size();
7      
8        // Iterate through all processes to build parent-child relationships
9        for (int i = 0; i < processCount; ++i) {
10            parentToChildren[ppid[i]].push_back(pid[i]);
11        }
12      
13        // Result vector to store all processes that need to be killed
14        vector<int> killedProcesses;
15      
16        // Define recursive DFS function to traverse process tree
17        function<void(int)> dfs = [&](int currentProcess) {
18            // Add current process to the kill list
19            killedProcesses.push_back(currentProcess);
20          
21            // Recursively kill all child processes
22            for (int childProcess : parentToChildren[currentProcess]) {
23                dfs(childProcess);
24            }
25        };
26      
27        // Start DFS from the process to be killed
28        dfs(kill);
29      
30        return killedProcesses;
31    }
32};
33
1/**
2 * Kills a process and all its child processes
3 * @param pid - Array of process IDs
4 * @param ppid - Array of parent process IDs (ppid[i] is the parent of pid[i])
5 * @param kill - The process ID to kill
6 * @returns Array of all killed process IDs (including the target and its descendants)
7 */
8function killProcess(pid: number[], ppid: number[], kill: number): number[] {
9    // Build adjacency list: maps parent process ID to list of child process IDs
10    const parentToChildren: Map<number, number[]> = new Map();
11  
12    // Iterate through all processes to build parent-child relationships
13    for (let i = 0; i < pid.length; i++) {
14        const parentId = ppid[i];
15        const childId = pid[i];
16      
17        // Initialize empty array for parent if not exists
18        if (!parentToChildren.has(parentId)) {
19            parentToChildren.set(parentId, []);
20        }
21      
22        // Add current process as child of its parent
23        parentToChildren.get(parentId)!.push(childId);
24    }
25  
26    // Store all processes that need to be killed
27    const killedProcesses: number[] = [];
28  
29    /**
30     * Depth-first search to find all descendant processes
31     * @param processId - Current process ID to kill
32     */
33    const killProcessAndDescendants = (processId: number): void => {
34        // Add current process to killed list
35        killedProcesses.push(processId);
36      
37        // Recursively kill all child processes
38        const children = parentToChildren.get(processId) ?? [];
39        for (const childProcessId of children) {
40            killProcessAndDescendants(childProcessId);
41        }
42    };
43  
44    // Start DFS from the target process to kill
45    killProcessAndDescendants(kill);
46  
47    return killedProcesses;
48}
49

Time and Space Complexity

Time Complexity: O(n)

The algorithm constructs a graph representation of the process tree and then performs a depth-first search (DFS) starting from the kill process.

  • Building the graph takes O(n) time, where we iterate through all n processes once to create the adjacency list.
  • The DFS traversal visits each process that needs to be killed exactly once. In the worst case, when we need to kill all processes (if kill is the root of all processes), we visit all n nodes.
  • Each node is processed once during DFS (appended to the answer list and its children are explored).

Therefore, the overall time complexity is O(n) + O(n) = O(n).

Space Complexity: O(n)

The space usage comes from:

  • The graph g (adjacency list) stores all parent-child relationships, which can have at most n entries across all lists combined.
  • The ans list stores all processes to be killed, which in the worst case contains all n processes.
  • The recursive DFS call stack can go as deep as the height of the process tree. In the worst case (a linear chain of processes), this could be O(n).

Therefore, the overall space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Handling Missing Process IDs

A critical pitfall occurs when the kill parameter refers to a process ID that doesn't exist in the pid array. The current solution will still return [kill] even if that process doesn't exist, which could be misleading.

Problem Example:

pid = [1, 3, 10, 5]
ppid = [3, 0, 5, 3]
kill = 7  # Process 7 doesn't exist!
# Current solution returns: [7]
# Expected behavior: return [] or handle appropriately

Solution: Add validation to check if the process exists before attempting to kill it:

def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
    # Create a set of valid process IDs for O(1) lookup
    valid_processes = set(pid)
  
    # Check if the process to kill actually exists
    if kill not in valid_processes:
        return []  # or raise an exception depending on requirements
  
    # Build adjacency list: parent -> list of children
    parent_to_children = defaultdict(list)
    for process_id, parent_id in zip(pid, ppid):
        parent_to_children[parent_id].append(process_id)
  
    # Rest of the solution remains the same...

2. Stack Overflow with Deep Process Trees

For extremely deep process trees (thousands of levels), the recursive DFS approach can cause a stack overflow due to Python's default recursion limit.

Problem Example:

# Creating a linear chain of 10000 processes
pid = list(range(1, 10001))
ppid = [0] + list(range(1, 10000))
kill = 1
# This could hit Python's recursion limit!

Solution: Use an iterative approach with an explicit stack instead of recursion:

def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
    # Build adjacency list
    parent_to_children = defaultdict(list)
    for process_id, parent_id in zip(pid, ppid):
        parent_to_children[parent_id].append(process_id)
  
    # Iterative DFS using explicit stack
    killed_processes = []
    stack = [kill]
  
    while stack:
        current_process = stack.pop()
        killed_processes.append(current_process)
      
        # Add all children to the stack
        for child in parent_to_children[current_process]:
            stack.append(child)
  
    return killed_processes

3. Assuming Single Root Process

The problem states that the root process has ppid[i] = 0, but there could be multiple processes with parent ID 0 (multiple roots), creating a forest rather than a single tree.

Problem Example:

pid = [1, 2, 3, 4]
ppid = [0, 0, 1, 2]  # Both process 1 and 2 have parent 0
kill = 1
# This works fine, but be aware of the forest structure

Solution: The current implementation actually handles this correctly since it only traverses from the kill node downward. However, if you need to validate that there's only one root:

def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]:
    # Count root processes (those with parent 0)
    root_count = sum(1 for parent in ppid if parent == 0)
    if root_count > 1:
        # Handle multiple roots if needed
        pass  # Current solution still works correctly
  
    # Continue with existing solution...

These pitfalls highlight the importance of input validation and considering edge cases in tree traversal problems.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More