Minimize Malware Spread
HardPrompt
You are given a network of n nodes represented as an n x n adjacency matrix graph, where the ith node is directly connected to the jth node if graph[i][j] == 1.
Some nodes initial are initially infected by malware. Whenever two nodes are directly connected, and at least one of those two nodes is infected by malware, both nodes will be infected by malware. This spread of malware will continue until no more nodes can be infected in this manner.
Suppose M(initial) is the final number of nodes infected with malware in the entire network after the spread of malware stops. We will remove exactly one node from initial.
Return the node that, if removed, would minimize M(initial). If multiple nodes could be removed to minimize M(initial), return such a node with the smallest index.
Note that if a node was removed from the initial list of infected nodes, it might still be infected later due to the malware spread.
Example 1:
Input: graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]
Output: 0Example 2:
Input: graph = [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2]
Output: 0Example 3:
Input: graph = [[1,1,1],[1,1,1],[1,1,1]], initial = [1,2]
Output: 1
Constraints:
n == graph.lengthn == graph[i].length2 <= n <= 300graph[i][j]is0or1.graph[i][j] == graph[j][i]graph[i][i] == 11 <= initial.length <= n0 <= initial[i] <= n - 1- All the integers in
initialare unique.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the outcome for each possible removal. We iterate through every node in the initial list, temporarily remove it, and then run a full simulation of the malware spread to see how many nodes get infected. The node whose removal results in the fewest final infections is our answer.
Algorithm
- Initialize
min_infected_countto a value larger thannandresult_nodeto -1. - Sort the
initialarray in ascending order. This ensures that if we find multiple nodes that result in the same minimum number of infections, the one with the smallest index is chosen first and retained. - Iterate through each node
uin the sortedinitiallist. Thisuis the candidate node to be removed. - For each
u, create a new temporary initial setinitial'which isinitialwithoutu. - Simulate the malware spread starting from the nodes in
initial'. A Breadth-First Search (BFS) is a good choice for this simulation.- Initialize a queue for the BFS and a
Setor boolean arrayinfectedto keep track of all infected nodes. - Add all nodes from
initial'to both the queue and theinfectedset. - While the queue is not empty, dequeue a node
curr. For each of its neighborsv(wheregraph[curr][v] == 1), ifvis not yet infected, mark it as infected and add it to the queue.
- Initialize a queue for the BFS and a
- After the simulation (BFS) for
uis complete, the size of theinfectedset is the total number of infected nodes. - If this count is less than
min_infected_count, updatemin_infected_countwith the new count and setresult_node = u. - After iterating through all nodes in
initial,result_nodewill hold the answer.
Walkthrough
The core idea is to test every possibility. For each node u in the initial set, we consider a scenario where u is not initially infected.
We create a new set of initial infections, initial' = initial - {u}.
Then, we simulate the spread. A graph traversal algorithm like Breadth-First Search (BFS) is suitable for this. We start a BFS from all nodes in initial'.
A visited array or set keeps track of all nodes that have become infected. We initialize a queue with the nodes from initial' and mark them as visited.
The BFS proceeds as usual: dequeue a node, and for all its unvisited neighbors, mark them as visited and enqueue them.
After the BFS completes, the total number of infected nodes gives us the final count for that scenario.
We repeat this for every node in initial, keeping track of the removal that leads to the minimum infection count. If there's a tie, the problem specifies we should choose the node with the smallest index. Sorting the initial list beforehand simplifies handling this tie-breaking rule.
import java.util.*; class Solution { public int minMalwareSpread(int[][] graph, int[] initial) { int n = graph.length; Arrays.sort(initial); // Sort for tie-breaking int minInfectedCount = n + 1; int resultNode = -1; for (int nodeToRemove : initial) { Set<Integer> infected = new HashSet<>(); Queue<Integer> queue = new LinkedList<>(); // Create the new initial set for simulation for (int startNode : initial) { if (startNode != nodeToRemove) { if (infected.add(startNode)) { queue.add(startNode); } } } // Simulate spread with BFS while (!queue.isEmpty()) { int u = queue.poll(); for (int v = 0; v < n; v++) { if (graph[u][v] == 1 && !infected.contains(v)) { infected.add(v); queue.add(v); } } } if (infected.size() < minInfectedCount) { minInfectedCount = infected.size(); resultNode = nodeToRemove; } } return resultNode; }}Complexity
Time
O(k * n^2), where `k` is the length of `initial` and `n` is the number of nodes. The outer loop runs `k` times. Inside, the BFS on an adjacency matrix takes O(n^2) time as for each node, we may have to scan its entire row in the matrix.
Space
O(n), where `n` is the number of nodes. This space is used for the queue and the `infected` set during the BFS traversal.
Trade-offs
Pros
Simple to understand and implement.
Directly follows the problem statement's logic.
Cons
Highly inefficient due to redundant computations. The entire graph traversal and spread simulation is repeated for each of the
knodes in theinitiallist.
A more efficient approach involves pre-processing the graph to understand its structure. The key insight is that malware infects entire connected components. By identifying these components first, we can quickly determine the impact of removing an initially infected node without running a full simulation each time. We can use a Union-Find (Disjoint Set Union) data structure to efficiently group nodes into components.
Algorithm
- Step 1: Build Components with Union-Find
- Create a Union-Find (DSU) data structure for
nnodes. Initialize each node to be its own parent with a size of 1. - Iterate through the
graphmatrix. For every pair of connected nodes(i, j)wheregraph[i][j] == 1, perform aunion(i, j)operation. This merges the sets containingiandjand updates the component sizes.
- Create a Union-Find (DSU) data structure for
- Step 2: Count Infections per Component
- Create an array
infectedCountInComponentof sizen, initialized to zeros. - Iterate through each
nodein theinitiallist. For eachnode, find its component's root usingdsu.find(node)and increment the count for that root ininfectedCountInComponent.
- Create an array
- Step 3: Find the Best Node to Remove
- Sort the
initialarray to handle the tie-breaking rule (smallest index first). - Initialize
maxSaved = -1to track the maximum number of nodes we can save, andresultNode = initial[0]as a default answer. - Iterate through each
nodeToRemovein the sortedinitiallist.- Find its component's root:
root = dsu.find(nodeToRemove). - If
infectedCountInComponent[root] == 1, it means this node is the only source of infection for its entire component. Removing it will save all nodes in this component.- The number of saved nodes is
dsu.size(root). - If this
savedcount is greater thanmaxSaved, updatemaxSaved = savedandresultNode = nodeToRemove.
- The number of saved nodes is
- Find its component's root:
- Sort the
- Step 4: Return Result
- After checking all nodes in
initial,resultNodewill hold the node that, when removed, saves the most other nodes. ReturnresultNode.
- After checking all nodes in
Walkthrough
First, we model the graph's connected components using a Union-Find data structure. We iterate through the adjacency matrix and for every connection graph[i][j] == 1, we union nodes i and j. The Union-Find structure will also maintain the size of each component (the number of nodes in each disjoint set).
After building the components, we analyze the initial infections. We count how many initially infected nodes fall into each component. A map or an array can be used to store this, mapping a component's root to its count of initial infections.
Now, we can evaluate the benefit of removing each node u from the initial list. The benefit is measured by how many nodes are "saved" from infection.
- If a component contains only one initially infected node
u, removingufrom theinitiallist saves that entire component from infection. The number of saved nodes is equal to the size of that component. - If a component contains more than one initially infected node, removing just one of them (
u) won't save the component. The other infected nodes in the same component will still cause the entire component to become infected. In this case, the number of saved nodes is 0.
We iterate through each candidate node u in initial, calculate its potential "saved" score, and find the node that maximizes this score. If multiple nodes yield the same maximum score, we choose the one with the smallest index, which is handled by sorting initial first. If no removal saves any nodes (i.e., all scores are 0), we return the smallest-indexed node from initial.
import java.util.*; class Solution { public int minMalwareSpread(int[][] graph, int[] initial) { int n = graph.length; DSU dsu = new DSU(n); for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (graph[i][j] == 1) { dsu.union(i, j); } } } int[] infectedCountInComponent = new int[n]; for (int node : initial) { infectedCountInComponent[dsu.find(node)]++; } Arrays.sort(initial); // Sort for tie-breaking int resultNode = initial[0]; int maxSaved = -1; for (int nodeToRemove : initial) { int root = dsu.find(nodeToRemove); // If this component has only one initial infection, removing it saves the component. if (infectedCountInComponent[root] == 1) { int componentSize = dsu.size(root); if (componentSize > maxSaved) { maxSaved = componentSize; resultNode = nodeToRemove; } } } return resultNode; } class DSU { private int[] parent; private int[] sz; // size of component public DSU(int n) { parent = new int[n]; sz = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; sz[i] = 1; } } public int find(int i) { if (parent[i] == i) { return i; } return parent[i] = find(parent[i]); // Path compression } public void union(int i, int j) { int rootI = find(i); int rootJ = find(j); if (rootI != rootJ) { // Union by size if (sz[rootI] < sz[rootJ]) { int temp = rootI; rootI = rootJ; rootJ = temp; } parent[rootJ] = rootI; sz[rootI] += sz[rootJ]; } } public int size(int i) { return sz[find(i)]; } }}Complexity
Time
O(n^2). Building the DSU structure by iterating through the n x n matrix is the dominant step, taking O(n^2 * α(n)), where α(n) is the nearly constant inverse Ackermann function. The subsequent loops over `initial` are much faster. Thus, the total complexity is effectively O(n^2).
Space
O(n). The DSU structure requires O(n) space for the `parent` and `size` arrays. The `infectedCountInComponent` array also takes O(n) space.
Trade-offs
Pros
Highly efficient. It processes the graph once to find components and then makes decisions quickly.
Avoids redundant simulations, leading to a significantly better time complexity.
Cons
Requires knowledge of the Union-Find (Disjoint Set Union) data structure.
The implementation is more complex than the brute-force approach.
Solutions
Solution
class Solution {private int[] p;public int minMalwareSpread(int[][] graph, int[] initial) { int n = graph.length; p = new int[n]; for (int i = 0; i < n; ++i) { p[i] = i; } int[] size = new int[n]; Arrays.fill(size, 1); for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { if (graph[i][j] == 1) { int pa = find(i), pb = find(j); if (pa == pb) { continue; } p[pa] = pb; size[pb] += size[pa]; } } } int mi = Integer.MAX_VALUE; int res = initial[0]; Arrays.sort(initial); for (int i = 0; i < initial.length; ++i) { int t = 0; Set<Integer> s = new HashSet<>(); for (int j = 0; j < initial.length; ++j) { if (i == j) { continue; } if (s.contains(find(initial[j]))) { continue; } s.add(find(initial[j])); t += size[find(initial[j])]; } if (mi > t) { mi = t; res = initial[i]; } } return res; }private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; }}class Solution {public: vector<int> p; int minMalwareSpread(vector<vector<int>> &graph, vector<int> &initial) { int n = graph.size(); p.resize(n); for (int i = 0; i < n; ++i) p[i] = i; vector<int> size(n, 1); for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { if (graph[i][j]) { int pa = find(i), pb = find(j); if (pa == pb) continue; p[pa] = pb; size[pb] += size[pa]; } } } int mi = 400; int res = initial[0]; sort(initial.begin(), initial.end()); for (int i = 0; i < initial.size(); ++i) { int t = 0; unordered_set<int> s; for (int j = 0; j < initial.size(); ++j) { if (i == j) continue; if (s.count(find(initial[j]))) continue; s.insert(find(initial[j])); t += size[find(initial[j])]; } if (mi > t) { mi = t; res = initial[i]; } } return res; } int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; }};class Solution: def minMalwareSpread(self, graph: List[List[int]], initial: List[int]) -> int: n = len(graph) p = list(range(n)) size = [1] * n def find(x): if p[x] != x: p[x] = find(p[x]) return p[x] for i in range(n): for j in range(i + 1, n): if graph[i][j] == 1: pa, pb = find(i), find(j) if pa == pb: continue p[pa] = pb size[pb] += size[pa] mi = inf res = initial[0] initial . sort() for i in range(len(initial)): t = 0 s = set() for j in range(len(initial)): if i == j: continue if find(initial[j]) in s: continue s . add(find(initial[j])) t += size[find(initial[j])] if mi > t: mi = t res = initial[i] return resVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.