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 ann x n
adjacency matrixgraph
, wheregraph[i][j] == 1
indicates a connection between nodesi
andj
.
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:
- Merging non-infected nodes into connected components
- Quickly determining component sizes
- 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.
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:
- Initially infected nodes will spread malware to all nodes in their connected components
- If multiple infected nodes are in the same component, that component gets infected regardless of which one we remove
- 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:
- First, identify all connected components that contain only non-infected nodes
- For each initially infected node, find which non-infected components it connects to
- Count how many initially infected nodes threaten each component
- 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:
- Use Union-Find to merge all non-infected nodes that are connected to each other
- For each initially infected node
i
, find all non-infected components it can reach - Count how many different infected nodes can reach each component
- Calculate the benefit of removing each infected node (sum of sizes of components only it can reach)
- 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:
- Path Compression: In
find()
, we updateself.p[x]
to point directly to the root - 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 EvaluatorExample 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)
wherei < j
, performing union operations when both nodes are not in the initial infected set and are connected. This requiresO(nΒ²)
iterations, with each union/find operation takingO(Ξ±(n))
amortized time, resulting inO(nΒ² Γ Ξ±(n))
. - Processing initial infected nodes: For each node in
initial
(at mostn
nodes), we iterate through alln
nodes to find connected components, performing find operations. This contributesO(n Γ |initial| Γ Ξ±(n))
which is bounded byO(nΒ² Γ Ξ±(n))
. - Computing the answer: For each initial node, we sum up component sizes which takes
O(|initial|)
operations per node, bounded byO(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 arrayp
and size arraysize
- Set
s
for initial infected nodes:O(|initial|)
which is bounded byO(n)
- Dictionary
g
storing connected components for each infected node:O(|initial| Γ n)
in worst case, bounded byO(nΒ²)
, but since we only store root representatives, it's effectivelyO(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:
- All initially infected nodes are isolated (not connected to any non-infected nodes)
- 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
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
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!