# 题目描述
给定一组不同的正整数nums
,返回最大的子集答案,使得该子集中的每对(answer[i], answer[j])
元素满足:
answer[i] % answer[j] == 0
,或者answer[j] % answer[i] == 0
如果有多个答案,则返回其中任意一个。
例1:
输入: nums = [1,2,3]
输出: [1,2]
// 这里输出[1,3]亦可
2
3
例2:
输入: nums = [1,2,4,8]
输出: [1,2,4,8]
2
约束条件:
- 1 <= nums.length <= 1000
- 1 <= nums[i] <= 2 * 109
- nums 里的所有整数都是唯一的。
# 暴力解
暴力解的思路是,遍历出所有的可以互相整除的数组,然后取长度最大的一组返回。
首先为了减少计算量,需要把nums
由小到大排序,记做questionArray
。
这样顺次从questionArray
取出数字比较时,每次取出的数字都比已排序的数字大。
知道数字大小后,判断是否整除就只需要做一次取余运算,可以节约一次取余运算。
然后创建一个二维数组answerArrayList
,放入数组内的每个元素都是一个可以互相整除的数组。
以questionArray
内的第一个元素(也就是最小的元素)创建一个数组作为第一个解放入answerArrayList
。
这时这个数组相当于哨兵节点,后续代码就不需要考虑answerArrayList
为空的情况了。
初始条件设计完了,以下就是暴力计算所有可以互相整除的数组的算法:
- 从
1
开始遍历,每次从questionArray
取出一个新的数字questionArray[i]
,进行以下判断: - 记录当前
answerArrayList
的长度canPassAnswerNum
; - 遍历
answerArrayList
内的元素,也就是目前可以互相整除的所有数字组合answerArray
,针对answerArray
做以下操作: - 遍历
answerArray
的每个元素answerArray[k]
,当questionArray[i]
不能整除answerArray[k]
时,跳出循环; - 判断循环次数
k
的大小: - 如果
k === answerArray.length
,证明questionArray[i]
可以被当前数组所有元素整除,将当前元素添加到answerArray
; - 而如果
k > 0
,证明questionArray[i]
可以被当前数组的一部分整除,以可以整除的部分加questionArray[i]
创建一个新数组放入newAnswerArrayList
,注意这里需要对newAnswerArrayList
去重; - 如果
k === 0
,将canPassAnswerNum
减1
; - 对
answerArray
的遍历结束; - 将
newAnswerArrayList
拼接到answerArrayList
内; - 如果
canPassAnswerNum === 0
,证明questionArray[i]
和所有answerArray
互质,需要单独用questionArray[i]
创建一个数组放入answerArrayList
; - 此时对
questionArray[i]
的遍历完毕。
按以上步骤遍历questionArray
,最后可以得出所有可以互相整除的数组answerArrayList
。
遍历answerArrayList
,取出最长的数组返回即可。
代码如下:
var largestDivisibleSubset = function(nums) {
const numsLength = nums.length;
const questionArray = nums.sort((a, b) => a - b);
let answerArrayList = [];
answerArrayList.push([questionArray[0]]);
for (let i = 1; i < numsLength; i++) {
let newAnswerArrayList = [];
let j = 0;
let canPassAnswerNum = answerArrayList.length;
for (; j < answerArrayList.length; j++) {
const answerArray = answerArrayList[j];
let k = 0;
for (; k < answerArray.length; k++) {
if (questionArray[i] % answerArray[k] !== 0) {
break;
}
}
if (k === answerArray.length) {
answerArray.push(questionArray[i]);
} else if (k > 0) {
const newAnsArray = answerArray.slice(0, k);
newAnsArray.push(questionArray[i]);
let l = 0;
for (;l < newAnswerArrayList.length; l++) {
if (newAnswerArrayList[l].toString() === newAnsArray.toString()) {
break;
}
}
if (l === newAnswerArrayList.length) {
newAnswerArrayList.push(newAnsArray);
}
} else {
canPassAnswerNum--
}
}
answerArrayList = answerArrayList.concat(newAnswerArrayList);
newAnswerArrayList = [];
if (canPassAnswerNum === 0) {
answerArrayList.push([questionArray[i]]);
}
}
const answer = {
index: 0,
length: 0
}
answerArrayList.forEach((item, index) => {
if (item.length > answer.length) {
answer.index = index;
answer.length = item.length;
}
});
return answerArrayList[answer.index];
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
不难看出,这个算法无论时间复杂度还是空间复杂度都极高。 在 leetcode 上提交时也卡在了一个长度为635的数组上。 所以势必需要寻找更优秀的算法。
# 动态规划
利用动态规划解决这个问题的详细思路可以参看 leetcode 的解答 (opens new window),这里只简要说明这道题的思路: 寻找最大整除子集,可以化简为寻找最大整除子集中的最大整数,再由最大整数可以反推出最大子集内的所有整数。 算法如下:
- 首先还是将输入数组排序;
- 声明一个等于当前数组长度的空数组 dp ;
- 然后依次计算数组内的数字是可整除子集内的第几个数字,每次循环做的运算如下:
- 只需要以当前数依次从大往小除以已遍历过的数字,第一个能整除的就是当前数字的所在的子集,记录
dp[i]=dp[j] + 1
; - 同时在循环中记录当前最大的
dp[i]
与i
,为后续遍历求解做准备; - 遍历排序后的输入数组,取出每个可以整除最大值的元素,拼接出结果。
代码如下:
var largestDivisibleSubset = function(nums) {
const numsLength = nums.length;
const dp = new Array(numsLength).fill(1);
const ansArr = [];
const maxInfo = {
dp: 1,
index: 0
}
nums.sort((a, b) => a - b);
for (let i = 1; i < numsLength; i++) {
for(let j = i - 1; j >= 0; j--) {
if (nums[i] % nums[j] === 0 && dp[j] > dp[i] - 1) {
dp[i] = dp[j] + 1;
}
}
if (dp[i] > maxInfo.dp) {
maxInfo.dp = dp[i];
maxInfo.index = i;
}
}
for (let k = maxInfo.index; k >= 0; k--) {
if (nums[maxInfo.index] % nums[k] === 0 && maxInfo.dp === dp[k]) {
ansArr.unshift(nums[k]);
maxInfo.dp--;
maxInfo.index = k;
}
}
return ansArr;
};
// Runtime: 148 ms, faster than 14.67% of JavaScript online submissions for Largest Divisible Subset.
// Memory Usage: 40.3 MB, less than 99.02% of JavaScript online submissions for Largest Divisible Subset.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
上述答案消耗的时间还是与平均水平有差距,对比了一下优秀答案,有以下优化点:
- maxInfo 对象储存了 dp 属性,导致每次遍历时都需要多保存一个数值,其实只保存 index 就可以找到 dp 值了;
- 虽然不知道有没有性能上的优化,但优秀代码中创建数组使用的是
Int16Array
。
修改后代码如下:
var largestDivisibleSubset = function(nums) {
const numsLength = nums.length;
const dp = new Int16Array(numsLength).fill(1);
const ansArr = [];
let maxIndex = 0;
nums.sort((a, b) => a - b);
for (let i = 1; i < numsLength; i++) {
for(let j = i - 1; j >= 0; j--) {
if (nums[i] % nums[j] === 0 && dp[j] > dp[i] - 1) {
dp[i] = dp[j] + 1;
}
}
if (dp[i] > dp[maxIndex]) {
maxIndex = i;
}
}
let currentDp = dp[maxIndex];
for (let k = maxIndex; k >= 0; k--) {
if (nums[maxIndex] % nums[k] === 0 && currentDp === dp[k]) {
ansArr.unshift(nums[k]);
currentDp--;
maxIndex = k;
}
}
return ansArr;
};
// Runtime: 96 ms, faster than 85.33% of JavaScript online submissions for Largest Divisible Subset.
// Memory Usage: 40.7 MB, less than 94.62% of JavaScript online submissions for Largest Divisible Subset.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
优化有效,本题作答完成。