Minimize Malware Spread II
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, completely removing it and any connections from this node to any other node.
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.
Example 1:
Input: graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]
Output: 0Example 2:
Input: graph = [[1,1,0],[1,1,1],[0,1,1]], initial = [0,1]
Output: 1Example 3:
Input: graph = [[1,1,0,0],[1,1,1,0],[0,1,1,1],[0,0,1,1]], initial = [0,1]
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 simulates the malware spread for each possible scenario. We iterate through each initially infected node, hypothetically remove it, and then run a full simulation of the malware spread from the remaining infected nodes. The node whose removal results in the fewest total infected nodes is our answer.
Algorithm
- Sort the
initialarray in ascending order. This helps in tie-breaking, as we will find the smallest index first. - Initialize
minInfectedCountto a value larger than any possible outcome (e.g.,n + 1) andresultNodeto the first element of the sortedinitialarray. - Iterate through each
nodeToRemovein the sortedinitialarray. - For each
nodeToRemove, simulate the spread:- Create a set
infectedand a queue for BFS. - Add all nodes from
initial(exceptnodeToRemove) to both theinfectedset and the queue. - Perform a BFS. In each step, for a node
u, explore its neighborsv. Ifvis connected, not thenodeToRemove, and not already infected, add it to theinfectedset and the queue.
- Create a set
- After the BFS completes, the size of the
infectedset is the total number of infected nodes for this scenario. - If this count is less than
minInfectedCount, updateminInfectedCountwith the new count andresultNodetonodeToRemove. - After checking all nodes in
initial, returnresultNode.
Walkthrough
The core idea is to try removing each node from the initial set one by one. For each node u in initial that we consider removing, we define a new set of infected sources, which is initial excluding u. We also treat u as a removed node from the graph, meaning it cannot be visited or spread malware. We then perform a graph traversal (like Breadth-First Search or Depth-First Search) starting from all the new sources. We count the total number of nodes visited during this traversal. This gives us the final number of infected nodes, M, for the scenario where u is removed. We keep track of the node u that leads to the minimum M found so far. To handle ties (multiple nodes giving the same minimum M), we should return the one with the smallest index. A simple way to do this is to sort the initial array first and then iterate. The first node that yields the minimum M will be the answer.
import java.util.*; class Solution { public int minMalwareSpread(int[][] graph, int[] initial) { Arrays.sort(initial); int n = graph.length; int minInfected = n + 1; int resultNode = -1; for (int nodeToRemove : initial) { Set<Integer> infected = new HashSet<>(); Queue<Integer> queue = new LinkedList<>(); for (int startNode : initial) { if (startNode != nodeToRemove) { infected.add(startNode); queue.add(startNode); } } // Using a temporary set to avoid ConcurrentModificationException Set<Integer> currentInfected = new HashSet<>(infected); while (!queue.isEmpty()) { int u = queue.poll(); for (int v = 0; v < n; v++) { if (graph[u][v] == 1 && v != nodeToRemove && !currentInfected.contains(v)) { currentInfected.add(v); queue.add(v); } } } if (currentInfected.size() < minInfected) { minInfected = currentInfected.size(); resultNode = nodeToRemove; } } return resultNode; }}Complexity
Time
O(k * n^2), where `k` is the number of initially infected nodes and `n` is the total number of nodes. For each of the `k` nodes to remove, we perform a BFS on the graph, which takes `O(n^2)` time with an adjacency matrix representation.
Space
O(n) to store the `infected` set and the queue for BFS.
Trade-offs
Pros
Simple to understand and implement.
Directly simulates the process described in the problem.
Cons
Inefficient due to repeated computations. The malware spread simulation is run from scratch for each potential node removal.
This optimized approach avoids re-computing the spread for each removal. Instead, it first analyzes the graph structure by temporarily removing all initially infected nodes. This leaves a "healthy" graph, which is a collection of connected components. We then determine which initial nodes can infect which components. A component is saved from infection only if its single initial infector is removed. By calculating the total size of components saved by removing each initial node, we can find the best node to remove.
Algorithm
- Put all nodes from
initialinto aHashSetfor efficient lookups. - Find connected components of the graph formed by non-initial nodes. Use DFS/BFS.
- Maintain a
componentIdarray to map each node to its component ID. - Maintain a
componentSizearray to store the size of each component.
- Maintain a
- Create a map
componentToInfectorsto store the set of initial nodes that are adjacent to each healthy component. - Iterate through each initial node
u. For each neighborvofuthat is not an initial node, find its component ID and adduto the set of infectors for that component. - Initialize a
savedCountarray of sizento zeros. - Iterate through the
componentToInfectorsmap. If a component is infected by exactly one initial nodeu, add the size of this component tosavedCount[u]. - Sort the
initialarray to handle tie-breaking. - Find the node
uininitialthat has the maximum value insavedCount[u]. This is the node to remove. Return this node.
Walkthrough
The key insight is that the final number of infected nodes depends on which "healthy" components get infected. A healthy component is a connected group of nodes that are not in the initial set. The strategy is as follows:
- Isolate Healthy Components: First, we identify the connected components in the graph consisting only of non-initial nodes. We can use DFS or BFS to do this. For each such component, we calculate its size and assign a unique ID.
- Map Infectors to Components: Next, for each healthy component, we identify which nodes from the
initialset are connected to it. We can build a map where keys are component IDs and values are sets of initial nodes that can infect them. - Calculate Saved Nodes: A component will be saved from infection if and only if we remove its only source of infection. So, we look for components that are connected to exactly one initial node. For each such component, if we remove its single infector, we "save" all nodes in that component. We can maintain an array, say
savedCount, wheresavedCount[u]stores the total number of nodes that would be saved by removing initial nodeu. - Find the Best Node: Finally, we iterate through the
initialnodes. The nodeuthat has the maximumsavedCount[u]is the one that minimizes the final infection count. If there's a tie, we choose the one with the smallest index. This approach is more efficient because it processes the graph structure once to find components and then uses this information to quickly evaluate the outcome of removing each initial node.
import java.util.*; class Solution { public int minMalwareSpread(int[][] graph, int[] initial) { int n = graph.length; Set<Integer> initialSet = new HashSet<>(); for (int node : initial) { initialSet.add(node); } // 1. Find components of the graph without initial nodes int[] componentId = new int[n]; Arrays.fill(componentId, -1); int[] componentSize = new int[n]; int cId = 0; for (int i = 0; i < n; i++) { if (!initialSet.contains(i) && componentId[i] == -1) { int size = 0; Queue<Integer> q = new LinkedList<>(); q.add(i); componentId[i] = cId; size++; while (!q.isEmpty()) { int u = q.poll(); for (int v = 0; v < n; v++) { if (graph[u][v] == 1 && !initialSet.contains(v) && componentId[v] == -1) { componentId[v] = cId; size++; q.add(v); } } } componentSize[cId] = size; cId++; } } // 2. Map components to their initial node infectors Map<Integer, Set<Integer>> componentToInfectors = new HashMap<>(); for (int u : initial) { Set<Integer> infectedComponents = new HashSet<>(); for (int v = 0; v < n; v++) { if (graph[u][v] == 1 && !initialSet.contains(v)) { infectedComponents.add(componentId[v]); } } for (int infectedCId : infectedComponents) { componentToInfectors.computeIfAbsent(infectedCId, k -> new HashSet<>()).add(u); } } // 3. Calculate how many nodes are saved by removing each initial node int[] savedCount = new int[n]; for (Map.Entry<Integer, Set<Integer>> entry : componentToInfectors.entrySet()) { if (entry.getValue().size() == 1) { int infector = entry.getValue().iterator().next(); int cSize = componentSize[entry.getKey()]; savedCount[infector] += cSize; } } // 4. Find the best node to remove Arrays.sort(initial); int maxSaved = -1; int resultNode = initial[0]; for (int u : initial) { if (savedCount[u] > maxSaved) { maxSaved = savedCount[u]; resultNode = u; } } return resultNode; }}Complexity
Time
O(n^2). Finding components takes `O(n^2)`. Mapping infectors to components takes `O(k * n)`. Calculating saved nodes and finding the best node takes `O(n + k)`. The dominant part is `O(n^2)`.
Space
O(n*k) in the worst case for the `componentToInfectors` map. Other data structures like `componentId`, `componentSize`, and `savedCount` take `O(n)` space.
Trade-offs
Pros
Much more efficient than the brute-force approach for larger graphs.
Avoids redundant computations by analyzing the graph structure once.
Cons
More complex to implement, involving multiple steps and data structures.
Higher space complexity compared to the brute-force approach.
Solutions
Solution
class Solution {private int[] p;private int[] size;public int minMalwareSpread(int[][] graph, int[] initial) { int n = graph.length; p = new int[n]; size = new int[n]; for (int i = 0; i < n; ++i) { p[i] = i; size[i] = 1; } boolean[] clean = new boolean[n]; Arrays.fill(clean, true); for (int i : initial) { clean[i] = false; } for (int i = 0; i < n; ++i) { if (!clean[i]) { continue; } for (int j = i + 1; j < n; ++j) { if (clean[j] && graph[i][j] == 1) { union(i, j); } } } int[] cnt = new int[n]; Map<Integer, Set<Integer>> mp = new HashMap<>(); for (int i : initial) { Set<Integer> s = new HashSet<>(); for (int j = 0; j < n; ++j) { if (clean[j] && graph[i][j] == 1) { s.add(find(j)); } } for (int root : s) { cnt[root] += 1; } mp.put(i, s); } int mx = -1; int ans = 0; for (Map.Entry<Integer, Set<Integer>> entry : mp.entrySet()) { int i = entry.getKey(); int t = 0; for (int root : entry.getValue()) { if (cnt[root] == 1) { t += size[root]; } } if (mx < t || (mx == t && i < ans)) { mx = t; ans = i; } } return ans; }private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; }private void union(int a, int b) { int pa = find(a); int pb = find(b); if (pa != pb) { size[pb] += size[pa]; p[pa] = pb; } }}class Solution {public: vector<int> p; vector<int> size; int minMalwareSpread(vector<vector<int>> &graph, vector<int> &initial) { int n = graph.size(); p.resize(n); size.resize(n); for (int i = 0; i < n; ++i) p[i] = i; fill(size.begin(), size.end(), 1); vector<bool> clean(n, true); for (int i : initial) clean[i] = false; for (int i = 0; i < n; ++i) { if (!clean[i]) continue; for (int j = i + 1; j < n; ++j) if (clean[j] && graph[i][j] == 1) merge(i, j); } vector<int> cnt(n, 0); unordered_map<int, unordered_set<int>> mp; for (int i : initial) { unordered_set<int> s; for (int j = 0; j < n; ++j) if (clean[j] && graph[i][j] == 1) s.insert(find(j)); for (int e : s) ++cnt[e]; mp[i] = s; } int mx = -1, ans = 0; for (auto &[i, s] : mp) { int t = 0; for (int root : s) if (cnt[root] == 1) t += size[root]; if (mx < t || (mx == t && i < ans)) { mx = t; ans = i; } } return ans; } int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } void merge(int a, int b) { int pa = find(a), pb = find(b); if (pa != pb) { size[pb] += size[pa]; p[pa] = pb; } }};class Solution: def minMalwareSpread(self, graph: List[List[int]], initial: List[int]) -> int: def find(x): if p[x] != x: p[x] = find(p[x]) return p[x] def union(a, b): pa, pb = find(a), find(b) if pa != pb: size[pb] += size[pa] p[pa] = pb n = len(graph) p = list(range(n)) size = [1] * n clean = [True] * n for i in initial: clean[i] = False for i in range(n): if not clean[i]: continue for j in range(i + 1, n): if clean[j] and graph[i][j] == 1: union(i, j) cnt = Counter() mp = {} for i in initial: s = {find(j) for j in range(n) if clean[j] and graph[i][j] == 1} for root in s: cnt[root] += 1 mp[i] = s mx, ans = - 1, 0 for i, s in mp . items(): t = sum(size[root] for root in s if cnt[root] == 1) if mx < t or mx == t and i < ans: mx, ans = t, i return ansVideo 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.