Facebook Pixel

928. Minimize Malware Spread II

Problem Description

You have a network of n nodes represented as an n x n adjacency matrix called [graph](/problems/graph_intro). In this matrix, graph[i][j] == 1 means node i is directly connected to node j.

Initially, some nodes specified in the array initial are infected by malware. The malware spreads according to these rules:

  • When two nodes are directly connected and at least one is infected, both become infected
  • The spread continues until no more nodes can be infected

Let M(initial) denote the total number of nodes infected after the malware stops spreading.

Your task is to remove exactly one node from the initial array (removing the node and all its connections from the network) to minimize the final infection count M(initial).

Return the node that should be removed to minimize the infection. If multiple nodes would result in the same minimum infection count, return the node with the smallest index.

Example:

If you have a network where nodes are connected and initial = [0, 1] contains the initially infected nodes, you need to determine whether removing node 0 or node 1 would result in fewer total infections. The malware will spread through all connected components that contain infected nodes.

Key Points:

  • You must remove exactly one initially infected node
  • The removed node and all its connections are completely eliminated from the network
  • After removal, malware spreads from the remaining initially infected nodes
  • Choose the node whose removal minimizes total infection
  • Break ties by choosing the smaller node index

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 explicitly states we have a network of n nodes represented as an n x n adjacency matrix graph, where graph[i][j] == 1 indicates a connection between nodes i and j.

Is it a tree?

  • No: The graph is not necessarily a tree. It can have cycles and multiple connected components. The adjacency matrix represents a general graph structure.

Is the problem related to directed acyclic graphs?

  • No: The adjacency matrix represents an undirected graph (malware spreads bidirectionally between connected nodes), and it may contain cycles.

Is the problem related to shortest paths?

  • No: We're not finding shortest paths. Instead, we're analyzing how malware spreads through connected components and determining which node's removal minimizes infection spread.

Does the problem involve connectivity?

  • Yes: The core of the problem is about connectivity. We need to understand which nodes are connected to determine how malware spreads through connected components. We also need to identify which connected components would be affected by removing each initially infected node.

Disjoint Set Union

  • Yes: The flowchart suggests DSU (Disjoint Set Union) for connectivity problems, which aligns perfectly with the solution approach.

Conclusion: The flowchart leads us to use Disjoint Set Union (DSU/Union-Find) to handle the connectivity aspect of the problem. While DFS could also explore connected components, DSU is more efficient for:

  1. Merging non-infected nodes into connected components
  2. Quickly determining component sizes
  3. Identifying which components are connected to infected nodes

The solution uses DSU to group non-infected nodes into connected components, then analyzes which initially infected node, when removed, would prevent the most infections by disconnecting these components from the malware spread.

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

Intuition

The key insight is to think about what happens when we remove an initially infected node from the network. We want to find which removal saves the most nodes from infection.

Let's break down what happens during malware spread:

  1. Initially infected nodes will spread malware to all nodes in their connected components
  2. If multiple infected nodes are in the same component, that component gets infected regardless of which one we remove
  3. The only nodes we can "save" are those in components connected to exactly one initially infected node

This leads us to a crucial observation: we can only prevent infection in a connected component if it's threatened by exactly one initially infected node. If a component is connected to multiple infected nodes, removing any single infected node won't help - the other infected nodes will still infect that component.

So our strategy becomes:

  1. First, identify all connected components that contain only non-infected nodes
  2. For each initially infected node, find which non-infected components it connects to
  3. Count how many initially infected nodes threaten each component
  4. The benefit of removing an infected node is the total size of components that are only threatened by that node

Why use Union-Find? We need to:

  • Group all non-infected nodes into their connected components efficiently
  • Track the size of each component
  • Quickly identify which component a non-infected node belongs to

The algorithm naturally follows:

  1. Use Union-Find to merge all non-infected nodes that are connected to each other
  2. For each initially infected node i, find all non-infected components it can reach
  3. Count how many different infected nodes can reach each component
  4. Calculate the benefit of removing each infected node (sum of sizes of components only it can reach)
  5. Choose the infected node with maximum benefit (or smallest index if tied)

This approach elegantly handles the complex interaction between multiple infection sources and overlapping spread patterns.

Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.

Solution Approach

The implementation follows our intuition by using Union-Find to manage connected components and carefully tracking which infected nodes threaten each component.

Step 1: Build Union-Find for Non-Infected Nodes

First, we create a Union-Find data structure and merge all non-infected nodes that are connected:

s = set(initial)  # Set of initially infected nodes
uf = UnionFind(n)
for i in range(n):
    if i not in s:  # Only process non-infected nodes
        for j in range(i + 1, n):
            if [graph](/problems/graph_intro)[i][j] and j not in s:  # Both nodes are non-infected and connected
                uf.union(i, j)

After this step, all non-infected nodes are grouped into their respective connected components.

Step 2: Map Infected Nodes to Threatened Components

For each initially infected node, we identify which non-infected components it can reach:

g = defaultdict(set)  # g[i] = set of component roots that infected node i can reach
cnt = Counter()       # cnt[root] = number of infected nodes that can reach this component

for i in initial:
    for j in range(n):
        if j not in s and [graph](/problems/graph_intro)[i][j]:  # j is non-infected and connected to i
            g[i].add(uf.find(j))  # Add j's component root to i's reachable set
  
    for root in g[i]:
        cnt[root] += 1  # Count how many infected nodes threaten this component

The g dictionary tells us which components each infected node threatens, while cnt tells us how many different infected nodes threaten each component.

Step 3: Calculate Benefit of Removing Each Infected Node

We evaluate each infected node to see how many nodes we can save by removing it:

ans, mx = 0, -1
for i in initial:
    t = sum(uf.get_size(root) for root in g[i] if cnt[root] == 1)
    if t > mx or (t == mx and i < ans):
        ans, mx = i, t

For each infected node i:

  • We sum the sizes of all components that only i threatens (cnt[root] == 1)
  • This gives us the total number of nodes we can save by removing i
  • We keep track of the best choice, breaking ties by choosing the smaller index

Union-Find Implementation Details

The Union-Find class uses two optimizations:

  1. Path Compression: In find(), we update self.p[x] to point directly to the root
  2. Union by Size: When merging, we attach the smaller tree to the larger one

These optimizations ensure nearly constant time operations for union and find.

Time Complexity: O(nΒ²) where n is the number of nodes, dominated by checking all pairs of nodes in the adjacency matrix.

Space Complexity: O(n) for the Union-Find structure and auxiliary data structures.

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 illustrate the solution approach.

Consider a network with 5 nodes and the following connections:

graph = [[1,1,0,0,0],
         [1,1,1,0,0],
         [0,1,1,1,0],
         [0,0,1,1,1],
         [0,0,0,1,1]]
       
initial = [0, 2]  // Nodes 0 and 2 are initially infected

This forms a chain: 0 -- 1 -- 2 -- 3 -- 4

Step 1: Build Union-Find for Non-Infected Nodes

First, we identify non-infected nodes: {1, 3, 4}

Now we union non-infected nodes that are connected:

  • Node 1 connects to node 2 (infected), so skip
  • Node 3 connects to node 2 (infected) and node 4 (non-infected)
  • Node 4 connects to node 3 (non-infected)
  • Union(3, 4) creates component {3, 4} with size 2

After this step:

  • Component containing node 1: {1} with size 1
  • Component containing nodes 3,4: {3, 4} with size 2

Step 2: Map Infected Nodes to Threatened Components

For infected node 0:

  • Node 0 connects to node 1 (non-infected)
  • Component root of node 1 is 1
  • g[0] = {1} (node 0 threatens component with root 1)

For infected node 2:

  • Node 2 connects to node 1 (non-infected) and node 3 (non-infected)
  • Component root of node 1 is 1
  • Component root of node 3 is 3 (or 4, depending on union order)
  • g[2] = {1, 3} (node 2 threatens components with roots 1 and 3)

Count how many infected nodes threaten each component:

  • cnt[1] = 2 (threatened by both nodes 0 and 2)
  • cnt[3] = 1 (threatened only by node 2)

Step 3: Calculate Benefit of Removing Each Infected Node

For removing node 0:

  • Node 0 threatens component {1}
  • But cnt[1] = 2, meaning component {1} is also threatened by node 2
  • Even if we remove node 0, node 2 will still infect component {1}
  • Benefit = 0 nodes saved

For removing node 2:

  • Node 2 threatens components {1} and {3, 4}
  • Component {1}: cnt[1] = 2, so it would still be infected by node 0
  • Component {3, 4}: cnt[3] = 1, so only node 2 threatens it
  • Benefit = size of component {3, 4} = 2 nodes saved

Result: Remove node 2 to save 2 nodes from infection.

Final infection outcomes:

  • If we remove node 0: Nodes 1, 2, 3, 4 get infected (4 total)
  • If we remove node 2: Only nodes 0, 1 get infected (2 total)

Therefore, the algorithm correctly returns node 2 as the optimal choice.

Solution Implementation

1from typing import List
2from collections import defaultdict, Counter
3
4
5class UnionFind:
6    """Union-Find (Disjoint Set Union) data structure with union by size optimization."""
7    __slots__ = "parent", "size"
8
9    def __init__(self, n: int):
10        """Initialize n disjoint sets, each containing one element."""
11        self.parent = list(range(n))  # Each node is initially its own parent
12        self.size = [1] * n  # Each set initially has size 1
13
14    def find(self, x: int) -> int:
15        """Find the root of the set containing x with path compression."""
16        if self.parent[x] != x:
17            # Path compression: make x point directly to the root
18            self.parent[x] = self.find(self.parent[x])
19        return self.parent[x]
20
21    def union(self, a: int, b: int) -> bool:
22        """
23        Union the sets containing a and b using union by size.
24        Returns True if sets were merged, False if already in same set.
25        """
26        root_a, root_b = self.find(a), self.find(b)
27      
28        if root_a == root_b:
29            return False  # Already in the same set
30      
31        # Union by size: attach smaller tree under larger tree
32        if self.size[root_a] > self.size[root_b]:
33            self.parent[root_b] = root_a
34            self.size[root_a] += self.size[root_b]
35        else:
36            self.parent[root_a] = root_b
37            self.size[root_b] += self.size[root_a]
38      
39        return True
40
41    def get_size(self, root: int) -> int:
42        """Get the size of the set with given root."""
43        return self.size[root]
44
45
46class Solution:
47    def minMalwareSpread(self, graph: List[List[int]], initial: List[int]) -> int:
48        """
49        Find which infected node to remove to minimize final infection spread.
50      
51        Args:
52            graph: Adjacency matrix where graph[i][j] = 1 if nodes i and j are connected
53            initial: List of initially infected nodes
54          
55        Returns:
56            The node from initial list to remove that minimizes infection spread
57        """
58        n = len(graph)
59        infected_set = set(initial)
60        union_find = UnionFind(n)
61      
62        # Step 1: Build connected components of non-infected nodes
63        # Connect all non-infected nodes that are adjacent
64        for i in range(n):
65            if i not in infected_set:
66                for j in range(i + 1, n):
67                    if graph[i][j] and j not in infected_set:
68                        union_find.union(i, j)
69      
70        # Step 2: For each infected node, find which non-infected components it connects to
71        infected_to_components = defaultdict(set)  # Maps infected node -> set of component roots
72        component_infection_count = Counter()  # Counts how many infected nodes connect to each component
73      
74        for infected_node in initial:
75            # Find all non-infected nodes connected to this infected node
76            for neighbor in range(n):
77                if neighbor not in infected_set and graph[infected_node][neighbor]:
78                    component_root = union_find.find(neighbor)
79                    infected_to_components[infected_node].add(component_root)
80          
81            # Count how many infected nodes connect to each component
82            for component_root in infected_to_components[infected_node]:
83                component_infection_count[component_root] += 1
84      
85        # Step 3: Find the infected node whose removal saves the most nodes
86        best_node_to_remove = 0
87        max_nodes_saved = -1
88      
89        for infected_node in initial:
90            # Calculate how many nodes would be saved by removing this infected node
91            # Only count components that are connected to exactly one infected node
92            nodes_saved = sum(
93                union_find.get_size(component_root) 
94                for component_root in infected_to_components[infected_node] 
95                if component_infection_count[component_root] == 1
96            )
97          
98            # Update best choice (prefer smaller node index in case of tie)
99            if nodes_saved > max_nodes_saved or (nodes_saved == max_nodes_saved and infected_node < best_node_to_remove):
100                best_node_to_remove = infected_node
101                max_nodes_saved = nodes_saved
102      
103        return best_node_to_remove
104
1class UnionFind {
2    private final int[] parent;
3    private final int[] componentSize;
4
5    /**
6     * Initialize Union-Find data structure with n nodes
7     * @param n number of nodes
8     */
9    public UnionFind(int n) {
10        parent = new int[n];
11        componentSize = new int[n];
12        for (int i = 0; i < n; i++) {
13            parent[i] = i;  // Each node is initially its own parent
14            componentSize[i] = 1;  // Each component initially has size 1
15        }
16    }
17
18    /**
19     * Find the root of the component containing node x with path compression
20     * @param x the node to find root for
21     * @return root of the component
22     */
23    public int find(int x) {
24        if (parent[x] != x) {
25            parent[x] = find(parent[x]);  // Path compression
26        }
27        return parent[x];
28    }
29
30    /**
31     * Union two components containing nodes a and b using union by size
32     * @param a first node
33     * @param b second node
34     * @return true if union was performed, false if already in same component
35     */
36    public boolean union(int a, int b) {
37        int rootA = find(a);
38        int rootB = find(b);
39      
40        if (rootA == rootB) {
41            return false;  // Already in same component
42        }
43      
44        // Union by size: attach smaller tree to larger tree
45        if (componentSize[rootA] > componentSize[rootB]) {
46            parent[rootB] = rootA;
47            componentSize[rootA] += componentSize[rootB];
48        } else {
49            parent[rootA] = rootB;
50            componentSize[rootB] += componentSize[rootA];
51        }
52        return true;
53    }
54
55    /**
56     * Get the size of the component with given root
57     * @param root root of the component
58     * @return size of the component
59     */
60    public int size(int root) {
61        return componentSize[root];
62    }
63}
64
65class Solution {
66    /**
67     * Find which initially infected node to remove to minimize the final spread of malware
68     * @param graph adjacency matrix representation of the network
69     * @param initial array of initially infected nodes
70     * @return the node to remove that minimizes spread (smallest index if tie)
71     */
72    public int minMalwareSpread(int[][] graph, int[] initial) {
73        int n = graph.length;
74      
75        // Mark initially infected nodes
76        boolean[] isInfected = new boolean[n];
77        for (int node : initial) {
78            isInfected[node] = true;
79        }
80      
81        // Build Union-Find for non-infected nodes
82        UnionFind uf = new UnionFind(n);
83        for (int i = 0; i < n; i++) {
84            if (!isInfected[i]) {
85                for (int j = i + 1; j < n; j++) {
86                    // Connect non-infected nodes that have an edge
87                    if (graph[i][j] == 1 && !isInfected[j]) {
88                        uf.union(i, j);
89                    }
90                }
91            }
92        }
93      
94        // For each infected node, find which clean components it connects to
95        Set<Integer>[] infectedToComponents = new Set[n];
96        Arrays.setAll(infectedToComponents, k -> new HashSet<>());
97      
98        // Count how many infected nodes connect to each clean component
99        int[] componentInfectedCount = new int[n];
100      
101        for (int infectedNode : initial) {
102            // Find all clean components this infected node connects to
103            for (int j = 0; j < n; j++) {
104                if (!isInfected[j] && graph[infectedNode][j] == 1) {
105                    infectedToComponents[infectedNode].add(uf.find(j));
106                }
107            }
108            // Increment count for each connected component
109            for (int componentRoot : infectedToComponents[infectedNode]) {
110                componentInfectedCount[componentRoot]++;
111            }
112        }
113      
114        // Find the best node to remove
115        int nodeToRemove = 0;
116        int maxSaved = -1;
117      
118        for (int infectedNode : initial) {
119            int savedNodes = 0;
120          
121            // Count nodes that would be saved by removing this infected node
122            for (int componentRoot : infectedToComponents[infectedNode]) {
123                // Only count components that are connected to exactly one infected node
124                if (componentInfectedCount[componentRoot] == 1) {
125                    savedNodes += uf.size(componentRoot);
126                }
127            }
128          
129            // Update answer if this saves more nodes, or same but smaller index
130            if (savedNodes > maxSaved || (savedNodes == maxSaved && infectedNode < nodeToRemove)) {
131                nodeToRemove = infectedNode;
132                maxSaved = savedNodes;
133            }
134        }
135      
136        return nodeToRemove;
137    }
138}
139
1class UnionFind {
2public:
3    UnionFind(int n) {
4        parent = vector<int>(n);
5        componentSize = vector<int>(n, 1);
6        // Initialize each node as its own parent
7        iota(parent.begin(), parent.end(), 0);
8    }
9
10    // Unite two components, return true if they were in different components
11    bool unite(int nodeA, int nodeB) {
12        int rootA = find(nodeA);
13        int rootB = find(nodeB);
14      
15        // Already in the same component
16        if (rootA == rootB) {
17            return false;
18        }
19      
20        // Union by size: attach smaller tree under root of larger tree
21        if (componentSize[rootA] > componentSize[rootB]) {
22            parent[rootB] = rootA;
23            componentSize[rootA] += componentSize[rootB];
24        } else {
25            parent[rootA] = rootB;
26            componentSize[rootB] += componentSize[rootA];
27        }
28        return true;
29    }
30
31    // Find root of component with path compression
32    int find(int node) {
33        if (parent[node] != node) {
34            parent[node] = find(parent[node]);  // Path compression
35        }
36        return parent[node];
37    }
38
39    // Get size of the component containing the given root
40    int getSize(int root) {
41        return componentSize[root];
42    }
43
44private:
45    vector<int> parent;        // Parent array for union-find
46    vector<int> componentSize; // Size of each component
47};
48
49class Solution {
50public:
51    int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
52        int n = graph.size();
53      
54        // Mark initially infected nodes
55        bool isInfected[n];
56        memset(isInfected, false, sizeof(isInfected));
57        for (int node : initial) {
58            isInfected[node] = true;
59        }
60      
61        // Build union-find for non-infected nodes
62        UnionFind uf(n);
63        for (int i = 0; i < n; ++i) {
64            if (!isInfected[i]) {
65                for (int j = i + 1; j < n; ++j) {
66                    // Connect non-infected nodes that have an edge
67                    if (graph[i][j] && !isInfected[j]) {
68                        uf.unite(i, j);
69                    }
70                }
71            }
72        }
73      
74        // For each infected node, find which non-infected components it connects to
75        unordered_set<int> connectedComponents[n];
76        // Count how many infected nodes connect to each component
77        int componentInfectionCount[n];
78        memset(componentInfectionCount, 0, sizeof(componentInfectionCount));
79      
80        for (int infectedNode : initial) {
81            // Find all non-infected components this infected node connects to
82            for (int j = 0; j < n; ++j) {
83                if (!isInfected[j] && graph[infectedNode][j]) {
84                    connectedComponents[infectedNode].insert(uf.find(j));
85                }
86            }
87            // Increment count for each connected component
88            for (int componentRoot : connectedComponents[infectedNode]) {
89                ++componentInfectionCount[componentRoot];
90            }
91        }
92      
93        // Find the best node to remove (minimize final infection spread)
94        int bestNodeToRemove = 0;
95        int maxNodesSaved = -1;
96      
97        for (int infectedNode : initial) {
98            int nodesSaved = 0;
99          
100            // Count nodes that would be saved by removing this infected node
101            for (int componentRoot : connectedComponents[infectedNode]) {
102                // Only count components that are connected to exactly one infected node
103                if (componentInfectionCount[componentRoot] == 1) {
104                    nodesSaved += uf.getSize(componentRoot);
105                }
106            }
107          
108            // Update best choice (maximize saved nodes, minimize node index on tie)
109            if (nodesSaved > maxNodesSaved || 
110                (nodesSaved == maxNodesSaved && infectedNode < bestNodeToRemove)) {
111                bestNodeToRemove = infectedNode;
112                maxNodesSaved = nodesSaved;
113            }
114        }
115      
116        return bestNodeToRemove;
117    }
118};
119
1// Parent array for Union-Find data structure
2let parent: number[];
3// Size array to track component sizes for union by rank
4let componentSize: number[];
5
6/**
7 * Initializes the Union-Find data structure
8 * @param n - Number of nodes in the graph
9 */
10function initializeUnionFind(n: number): void {
11    parent = Array(n).fill(0).map((_, index) => index);
12    componentSize = Array(n).fill(1);
13}
14
15/**
16 * Finds the root parent of a node with path compression
17 * @param x - Node to find the root parent for
18 * @returns Root parent of the node
19 */
20function find(x: number): number {
21    if (parent[x] !== x) {
22        parent[x] = find(parent[x]); // Path compression
23    }
24    return parent[x];
25}
26
27/**
28 * Unions two nodes into the same component using union by rank
29 * @param a - First node
30 * @param b - Second node
31 * @returns true if union was performed, false if already in same component
32 */
33function union(a: number, b: number): boolean {
34    const rootA = find(a);
35    const rootB = find(b);
36  
37    if (rootA === rootB) {
38        return false;
39    }
40  
41    // Union by rank: attach smaller tree under larger tree
42    if (componentSize[rootA] > componentSize[rootB]) {
43        parent[rootB] = rootA;
44        componentSize[rootA] += componentSize[rootB];
45    } else {
46        parent[rootA] = rootB;
47        componentSize[rootB] += componentSize[rootA];
48    }
49  
50    return true;
51}
52
53/**
54 * Gets the size of the component containing the given root
55 * @param root - Root node of the component
56 * @returns Size of the component
57 */
58function getSize(root: number): number {
59    return componentSize[root];
60}
61
62/**
63 * Finds which initially infected node to remove to minimize malware spread
64 * @param graph - Adjacency matrix representing the network
65 * @param initial - Array of initially infected nodes
66 * @returns Node to remove that minimizes final infection count
67 */
68function minMalwareSpread(graph: number[][], initial: number[]): number {
69    const numberOfNodes = graph.length;
70    const infectedSet = new Set(initial);
71  
72    // Initialize Union-Find structure
73    initializeUnionFind(numberOfNodes);
74  
75    // Union all non-infected nodes that are connected
76    for (let i = 0; i < numberOfNodes; i++) {
77        if (!infectedSet.has(i)) {
78            for (let j = i + 1; j < numberOfNodes; j++) {
79                if (graph[i][j] === 1 && !infectedSet.has(j)) {
80                    union(i, j);
81                }
82            }
83        }
84    }
85  
86    // For each infected node, track which clean components it can reach
87    const reachableComponents: Set<number>[] = Array.from(
88        { length: numberOfNodes }, 
89        () => new Set()
90    );
91  
92    // Count how many infected nodes can reach each clean component
93    const infectedCountPerComponent: number[] = Array(numberOfNodes).fill(0);
94  
95    for (const infectedNode of initial) {
96        // Find all clean components this infected node can reach
97        for (let j = 0; j < numberOfNodes; j++) {
98            if (graph[infectedNode][j] === 1 && !infectedSet.has(j)) {
99                reachableComponents[infectedNode].add(find(j));
100            }
101        }
102      
103        // Increment count for each reachable component
104        for (const componentRoot of reachableComponents[infectedNode]) {
105            infectedCountPerComponent[componentRoot]++;
106        }
107    }
108  
109    // Find the best node to remove
110    let nodeToRemove = 0;
111    let maxNodesSaved = -1;
112  
113    for (const infectedNode of initial) {
114        let nodesSaved = 0;
115      
116        // Count nodes that would be saved by removing this infected node
117        // Only count components that are reached by exactly one infected node
118        for (const componentRoot of reachableComponents[infectedNode]) {
119            if (infectedCountPerComponent[componentRoot] === 1) {
120                nodesSaved += getSize(componentRoot);
121            }
122        }
123      
124        // Update best choice (maximize saved nodes, minimize node index for ties)
125        if (nodesSaved > maxNodesSaved || 
126            (nodesSaved === maxNodesSaved && infectedNode < nodeToRemove)) {
127            nodeToRemove = infectedNode;
128            maxNodesSaved = nodesSaved;
129        }
130    }
131  
132    return nodeToRemove;
133}
134

Time and Space Complexity

Time Complexity: O(nΒ² Γ— Ξ±(n))

The analysis breaks down as follows:

  • Building the UnionFind structure for non-infected nodes: The nested loops iterate through all pairs (i, j) where i < j, performing union operations when both nodes are not in the initial infected set and are connected. This requires O(nΒ²) iterations, with each union/find operation taking O(Ξ±(n)) amortized time, resulting in O(nΒ² Γ— Ξ±(n)).
  • Processing initial infected nodes: For each node in initial (at most n nodes), we iterate through all n nodes to find connected components, performing find operations. This contributes O(n Γ— |initial| Γ— Ξ±(n)) which is bounded by O(nΒ² Γ— Ξ±(n)).
  • Computing the answer: For each initial node, we sum up component sizes which takes O(|initial|) operations per node, bounded by O(nΒ²).

The dominant term is O(nΒ² Γ— Ξ±(n)) where Ξ±(n) is the inverse Ackermann function, which grows extremely slowly and is effectively constant for all practical values of n.

Space Complexity: O(n)

The space usage includes:

  • UnionFind data structure: O(n) for parent array p and size array size
  • Set s for initial infected nodes: O(|initial|) which is bounded by O(n)
  • Dictionary g storing connected components for each infected node: O(|initial| Γ— n) in worst case, bounded by O(nΒ²), but since we only store root representatives, it's effectively O(n) in practice
  • Counter cnt: O(n) for counting occurrences

While the reference answer states O(nΒ²) space complexity (likely accounting for the input graph), the additional space used by the algorithm itself is O(n).

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

Common Pitfalls

Pitfall: Incorrectly Counting Components Threatened by Multiple Infected Nodes

The Problem: A common mistake is to assume that removing an infected node will always save all the non-infected nodes it connects to. This is incorrect when a non-infected component is threatened by multiple infected nodes.

Example Scenario: Consider a graph where:

  • Component A (5 nodes) is connected to infected nodes 0 and 1
  • Component B (3 nodes) is connected only to infected node 0

If we remove node 0:

  • Component B is saved (3 nodes saved)
  • Component A still gets infected from node 1 (0 nodes saved)
  • Total saved: 3 nodes

The Mistake:

# WRONG: Counting all connected components without checking exclusivity
nodes_saved = sum(
    union_find.get_size(component_root) 
    for component_root in infected_to_components[infected_node]
)

This would incorrectly count Component A's 5 nodes as "saved" when removing node 0, even though node 1 would still infect it.

The Solution: Only count components that are threatened by exactly one infected node (the one we're considering removing):

# CORRECT: Only count components with exactly one threat
nodes_saved = sum(
    union_find.get_size(component_root) 
    for component_root in infected_to_components[infected_node] 
    if component_infection_count[component_root] == 1  # Key condition!
)

Pitfall: Building Union-Find with Infected Nodes Included

The Problem: Including infected nodes when building the Union-Find structure leads to incorrect component identification.

The Mistake:

# WRONG: Including infected nodes in Union-Find
for i in range(n):
    for j in range(i + 1, n):
        if graph[i][j]:  # Connects ALL nodes, including infected ones
            union_find.union(i, j)

This creates components that span across infected nodes, making it impossible to determine which non-infected components would be saved.

The Solution: Only union non-infected nodes to properly identify separate non-infected components:

# CORRECT: Only union non-infected nodes
for i in range(n):
    if i not in infected_set:  # Check i is not infected
        for j in range(i + 1, n):
            if graph[i][j] and j not in infected_set:  # Check j is not infected
                union_find.union(i, j)

Pitfall: Not Handling Edge Cases Properly

The Problem: Failing to handle cases where:

  1. All initially infected nodes are isolated (not connected to any non-infected nodes)
  2. The initial array contains only one node

The Mistake: Not initializing the answer properly or assuming there will always be nodes to save:

# WRONG: Uninitialized or incorrect default
ans = -1  # or ans = None
mx = 0

The Solution: Always initialize with a valid node from the initial array and handle the case where no nodes can be saved:

# CORRECT: Initialize with first node, handle -1 for "no savings"
best_node_to_remove = 0
max_nodes_saved = -1  # -1 ensures any non-negative savings will be better

# Or explicitly handle the edge case:
if not initial:
    return -1
best_node_to_remove = min(initial)  # Default to smallest index
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

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

Load More