這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos
輸入:nums = [4,6,15,35]
輸出:4
輸入:nums = [20,50,9,63]
輸出:2
輸入:nums = [2,3,6,7,4,12,21,39]
輸出:8
// 並查集的陣列, fathers[3]=1的意思是:數位3的父節點是1
int[] fathers = new int[100001];
// 並查集中,每個數位與其子節點的元素數量總和,rootSetSize[5]=10的意思是:數位5與其所有子節點加在一起,一共有10個元素
int[] rootSetSize = new int[100001];
// map的key是質因數,value是以此key作為質因數的數位
// 例如題目的陣列是[4,6,15,35],對應的map就有四個key:2,3,5,7
// key等於2時,value是[4,6],因為4和6的質因數都有2
// key等於3時,value是[6,15],因為6和16的質因數都有3
// key等於5時,value是[15,35],因為15和35的質因數都有5
// key等於7時,value是[35],因為35的質因數有7
Map<Integer, List<Integer>> map = new HashMap<>();
// 用來儲存並查集中,最大樹的元素數量
int maxRootSetSize = 1;
/**
* 帶壓縮的並查集查詢(即尋找指定數位的根節點)
* @param i
*/
private int find(int i) {
// 如果執向的是自己,那就是根節點了
if(fathers[i]==i) {
return i;
}
// 用遞迴的方式尋找,並且將整個路徑上所有長輩節點的父節點都改成根節點,
// 例如1的父節點是2,2的父節點是3,3的父節點是4,4就是根節點,在這次查詢後,1的父節點變成了4,2的父節點也變成了4,3的父節點還是4
fathers[i] = find(fathers[i]);
return fathers[i];
}
/**
* 並查集合並,合併後,child會成為parent的子節點
* @param parent
* @param child
*/
private void union(int parent, int child) {
int parentRoot = find(parent);
int childRoot = find(child);
// 如果有共同根節點,就提前返回
if (parentRoot==childRoot) {
return;
}
// child元素根節點是childRoot,現在將childRoot的父節點從它自己改成了parentRoot,
// 這就相當於child所在的整棵樹都拿給parent的根節點做子樹了
fathers[childRoot] = fathers[parentRoot];
// 合併後,這個樹變大了,新增元素的數量等於被合併的字數元素數量
rootSetSize[parentRoot] += rootSetSize[childRoot];
// 更像最大數量
maxRootSetSize = Math.max(maxRootSetSize, rootSetSize[parentRoot]);
}
// 對陣列中的每個數,算出所有質因數,構建map
for (int i=0;i<nums.length;i++) {
int cur = nums[i];
for (int j=2;j*j<=cur;j++) {
// 從2開始逐個增加,能整除的一定是質數
if(cur%j==0) {
map.computeIfAbsent(j, key -> new ArrayList<>()).add(nums[i]);
}
// 從cur中將j的因數全部去掉
while (cur%j==0) {
cur /= j;
}
}
// 能走到這裡,cur一定是個質數,
// 因為nums[i]被除過多次後結果是cur,所以nums[i]能被cur整除,所以cur是nums[i]的質因數,應該放入map中
if (cur!=1) {
map.computeIfAbsent(cur, key -> new ArrayList<>()).add(nums[i]);
}
}
class Solution {
// 並查集的陣列, fathers[3]=1的意思是:數位3的父節點是1
int[] fathers = new int[100001];
// 並查集中,每個數位與其子節點的元素數量總和,rootSetSize[5]=10的意思是:數位5與其所有子節點加在一起,一共有10個元素
int[] rootSetSize = new int[100001];
// map的key是質因數,value是以此key作為質因數的數位
// 例如題目的陣列是[4,6,15,35],對應的map就有四個key:2,3,5,7
// key等於2時,value是[4,6],因為4和6的質因數都有2
// key等於3時,value是[6,15],因為6和16的質因數都有3
// key等於5時,value是[15,35],因為15和35的質因數都有5
// key等於7時,value是[35],因為35的質因數有7
Map<Integer, List<Integer>> map = new HashMap<>();
// 用來儲存並查集中,最大樹的元素數量
int maxRootSetSize = 1;
/**
* 帶壓縮的並查集查詢(即尋找指定數位的根節點)
* @param i
*/
private int find(int i) {
// 如果執向的是自己,那就是根節點了
if(fathers[i]==i) {
return i;
}
// 用遞迴的方式尋找,並且將整個路徑上所有長輩節點的父節點都改成根節點,
// 例如1的父節點是2,2的父節點是3,3的父節點是4,4就是根節點,在這次查詢後,1的父節點變成了4,2的父節點也變成了4,3的父節點還是4
fathers[i] = find(fathers[i]);
return fathers[i];
}
/**
* 並查集合並,合併後,child會成為parent的子節點
* @param parent
* @param child
*/
private void union(int parent, int child) {
int parentRoot = find(parent);
int childRoot = find(child);
// 如果有共同根節點,就提前返回
if (parentRoot==childRoot) {
return;
}
// child元素根節點是childRoot,現在將childRoot的父節點從它自己改成了parentRoot,
// 這就相當於child所在的整棵樹都拿給parent的根節點做子樹了
fathers[childRoot] = fathers[parentRoot];
// 合併後,這個樹變大了,新增元素的數量等於被合併的字數元素數量
rootSetSize[parentRoot] += rootSetSize[childRoot];
// 更像最大數量
maxRootSetSize = Math.max(maxRootSetSize, rootSetSize[parentRoot]);
}
public int largestComponentSize(int[] nums) {
// 對陣列中的每個數,算出所有質因數,構建map
for (int i=0;i<nums.length;i++) {
int cur = nums[i];
for (int j=2;j*j<=cur;j++) {
// 從2開始逐個增加,能整除的一定是質數
if(cur%j==0) {
map.computeIfAbsent(j, key -> new ArrayList<>()).add(nums[i]);
}
// 從cur中將j的因數全部去掉
while (cur%j==0) {
cur /= j;
}
}
// 能走到這裡,cur一定是個質數,
// 因為nums[i]被除過多次後結果是cur,所以nums[i]能被cur整除,所以cur是nums[i]的質因數,應該放入map中
if (cur!=1) {
map.computeIfAbsent(cur, key -> new ArrayList<>()).add(nums[i]);
}
}
// 至此,map已經準備好了,接下來是並查集的事情,先要初始化陣列
for(int i=0;i< fathers.length;i++) {
// 這就表示:數位i的父節點是自己
fathers[i] = i;
// 這就表示:數位i加上其下所有子節點的數量等於1(因為每個節點父節點都是自己,所以每個節點都沒有子節點)
rootSetSize[i] = 1;
}
// 遍歷map
for (int key : map.keySet()) {
// 每個key都是一個質因數
// 每個value都是這個質因數對應的數位
List<Integer> list = map.get(key);
// 超過1個元素才有必要合併
if (null!=list && list.size()>1) {
// 取第0個元素作為父節點
int parent = list.get(0);
// 將其他節點全部作為地0個元素的子節點
for(int i=1;i<list.size();i++) {
union(parent, list.get(i));
}
}
}
return maxRootSetSize;
}
}
個人在做這道題的時候,最大的誤解就是對並查集合並的理解錯誤,導致做錯,這裡列出來,以避免您犯相同錯誤
以4,6,15,35這四個數位為例,以2為質因數的有4和6,以3為質因數的有6和15,以5為質因數的有15和35,以7為質因數的有35,邏輯關係如下圖
所以,我們在說並查集合並操作,到底在合併什麼?(這是核心,理解正確,這道題就解開了)
之前的誤解如下圖,以為是將紅色箭頭指向的四個集合合併,這樣就達到了連通效果,實際上這樣的理解是大錯特錯
接下來是自我救贖的糾正之路
首先,圖就是錯誤的,既然是並查集,就應該按照並查集的資料結構來畫圖:一個int陣列,陣列下標就代表具體數位,值代表該數位的父節點是誰,例如 a[2]=5,其含義就是數位2的父節點是5,這是基本定義
並查集初始化的時候,每個元素的父節點都是它自己,如下圖,注意,這個陣列的長度其實是36(既從0到35),但是其他元素都用不上,所以我們無需關注它們,也就沒有畫進圖中
接下來就是本題最核心的操作:合併,究竟該怎麼合併呢?
答案是:相同質因數的數位合併,也就是說:以2為質因數的是4和6,所以4和6合併,以3為質因數的是6和15,所以6和15合併,以5為質因數的是15和35,所以15和35合併,7的質因數只有35,那就沒法合併了
以上就是合併的操作,沒錯,就是這麼簡單:在並查集中對擁有相同質因數的數位進行合併
看到這裡,您應該會疑惑:這樣的合併,和連通有什麼關係?和解題又有什麼關係呢?
不急,咱能就用上面的陣列,合併一下試試,稍後就會見證奇蹟,也許能幫您找到豁然開朗的感覺
為了形象的理解,接下來我給陣列再配上圖,用來更形象的表達元素之間的父子關係,合併前的陣列和關係圖如下圖,每個圓圈都有個箭頭指向自己,表示每個元素的父節點是自己
接下來,合併4和6,這裡的做法是把4作為6的父節點,所以,如下圖,陣列下標為4的元素值等於6,用邏輯圖來表示,就是6的箭頭指向4
接下來該合併6和15了,它們都有質因數3,這一步非常關鍵,因為我就是在這一步恍然大悟的,如下圖,將6的父節點設定為4,再看邏輯關係圖,明明只是在合併6和15,然而,4、6、15已經連通了!
恍然大悟:我們無需對各個質因數之間做什麼,只要將每個質因數對應的數位合併即可,有的數位本來就屬於多個質因數,所有跨質因數的連線都是因為這個特點而存在!
接下來是連線15和35,相信聰明的您也已經徹底領悟了,此時4個元素已經連通了
最後質因數7對應的數位只有35,一個數位就不需要合併操作了