In a network of nodes, each node `i` is directly connected to another node `j` if and only 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 one node from the initial list. 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 may still be infected later as a result of the malware spread.
Example 1:
Input: graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]
Output: 0
Example 2:
Input: graph = [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2]
Output: 0
Example 3:
Input: graph = [[1,1,1],[1,1,1],[1,1,1]], initial = [1,2]
Output: 1
Note:
1 < graph.length = graph[0].length <= 3000 <= graph[i][j] == graph[j][i] <= 1graph[i][i] = 11 <= initial.length < graph.length0 <= initial[i] < graph.length
這道題說是讓我們最大程度的減少惡意軟件的傳播,談到這個話題,博主最先想到的就是紅衣教主周鴻禕,當年的 3Q 大戰的場景還歷歷在目,將殺毒軟件完全免費確實是造福了用戶。回到本題吧,這里給了一個二維數組 graph,其中 graph[i][j] 的值表示結點i和結點j是否相連,1為相連,0為不連,這就是鄰接矩陣啊,已經幫我們建立好了,遍歷的時候就可以直接使用了。還給了一個數組 initial,里面是病毒源,所有跟病毒源相連接的結點都會被感染,現在問若從病毒源中去掉哪個結點,會使得感染結點的數量最大程度的減少,若出現平局,則返回結點序號較小的那個。那么實際上這道題的本質還是遍歷這個無向圖,遍歷的方法就有 DFS 和 BFS 兩種。這里先來看 BFS 的解法,既然要在病毒源中去掉一個結點,由於不知道該去掉哪個結點,就遍歷所有的情況,每次去掉一個不同的結點,然后剩下的病毒源結點就是起點,都排入隊列中開始遍歷,一般來說迭代的解法不需要用子函數,但這里為了使程序的結構更加清晰,還是使用了子函數,這里的 BFS 遍歷有向圖的寫法就不多解釋,差不多都是一樣的寫法,得到了所有可以被感染的結點個數 cnt 之后,跟全局最小值 mn 比較,假如 cnt 小於 mn,或者二者相等但是當前去掉的結點 num 序號小於 res 時,需要更新 mn 和 res,參見代碼如下:
解法一:
class Solution {
public:
int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
int mn = INT_MAX, res = 0;
unordered_set<int> infected(initial.begin(), initial.end());
for (int num : initial) {
infected.erase(num);
int cnt = helper(graph, infected);
if (cnt < mn || (cnt == mn && num < res)) {
mn = cnt;
res = num;
}
infected.insert(num);
}
return res;
}
int helper(vector<vector<int>>& graph, unordered_set<int> infected) {
queue<int> q;
for (int num : infected) q.push(num);
while (!q.empty()) {
auto t = q.front(); q.pop();
for (int i = 0; i < graph[t].size(); ++i) {
if (graph[t][i] != 1 || infected.count(i)) continue;
infected.insert(i);
q.push(i);
}
}
return infected.size();
}
};
當然也可以使用 DFS 的寫法,但是為了避免寫兩個子函數,需要在遞歸的子函數中帶上當前結點這個參數,那么在調用的時候就不能像 BFS 那么簡單了,而是要定義一些變量,比如 cnt,還有 HashSet,而且要對每一個剩余的病毒源結點調用遞歸函數,其他部分跟上的解法沒啥區別,參見代碼如下:
解法二:
class Solution {
public:
int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
int mn = INT_MAX, res = 0;
unordered_set<int> infected(initial.begin(), initial.end());
for (int num : initial) {
infected.erase(num);
int cnt = 0;
unordered_set<int> visited;
for (int cur : infected) {
helper(graph, cur, visited, cnt);
}
if (cnt < mn || (cnt == mn && num < res)) {
mn = cnt;
res = num;
}
infected.insert(num);
}
return res;
}
void helper(vector<vector<int>>& graph, int cur, unordered_set<int>& visited, int& cnt) {
if (visited.count(cur)) return;
visited.insert(cur);
++cnt;
for (int i = 0; i < graph[cur].size(); ++i) {
if (graph[cur][i] != 1) continue;
helper(graph, i, visited, cnt);
}
}
};
這道題也可以使用聯合查找 Union Find 來做,因為 UF 算法可以將屬於同一個群組的結點歸類,使用一個 root 數組和一個 findRoot 函數,對於同一個群組的結點,調用 findRoot 函數會得到相同的祖先結點。這里還需要使用兩個數組 area 和 malware,其中 area[i] 就表示祖先結點是i的群組中的結點個數,malware[i] 表示祖先結點是i的某個感染群組中的結點個數。首先初始化 root 數組,然后遍歷 graph,對於每個 graph[i][j] 為1的位置,建立 root 數組的映射。注意這里跟之前有些不同的是,不能直接 root[i] = j,而是要分別對i和j調用 findRoot 函數,一定要找到i和j所屬群組的祖先結點,然后對其建立映射。之后再遍歷所有結點,找到每個結點的祖先結點,並在 area 數組中進行累加,同理,遍歷所有的感染源結點,找到每個感染源結點的祖先結點,並在 malware 數組中進行累加。新建結果 res 為一個 pair 對兒,分別為去掉某個感染源結點后還會感染的結點總個數和那個去掉的結點序號,初始化為1和0,然后此時遍歷所有的感染源結點,需要找到只有一個感染源結點的感染結點群組,想想為什么,因為多個感染源結點相連的話,不管去掉哪個,最終傳染的結點個數都是相同的,所以若某個感染群組只有一個感染源的話,去掉這個感染源結點,就會拯救很多結點不被感染,而該群組的結點個數越多,表示去掉該感染源結點的效果越好,這里將結點個數取相反數,然后比較取最小值,得到的就是群結點數最多的情況,假如不存在只有一個感染源結點的群組,那么就返回序號最小的那個感染源結點即可,參見代碼如下:
解法三:
class Solution {
public:
int minMalwareSpread(vector<vector<int>>& graph, vector<int>& initial) {
int n = graph.size();
vector<int> root(n), area(n), malware(n), res{1, 0};
for (int i = 0; i < n; ++i) root[i] = i;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (graph[i][j] == 1) root[findRoot(root, i)] = findRoot(root, j);
}
}
for (int i = 0; i < n; ++i) ++area[findRoot(root, i)];
for (int i : initial) ++malware[findRoot(root, i)];
for (int i : initial) {
res = min(res, {(malware[findRoot(root, i)] == 1 ) * (-area[findRoot(root, i)]), i});
}
return res[1];
}
int findRoot(vector<int>& root, int i) {
return i == root[i] ? i : findRoot(root, root[i]);
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/924
參考資料:
https://leetcode.com/problems/minimize-malware-spread/
https://leetcode.com/problems/minimize-malware-spread/discuss/181116/Java-BFS
https://leetcode.com/problems/minimize-malware-spread/discuss/181129/C%2B%2BPython-Union-Found
[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)
