写在前面
这次周赛的第四题还是比较有意思的,尤其是时间复杂度方面,给的数据范围在10^5,需要O(NlogN)的算法,就很容易将思想局限在二分、排序、堆、并查集,这些方法之中,而这道题复杂度的计算用到级数的概念,表面接近O(N ^ 2)的算法,实际复杂度为O(NlogN),还是十分值得思考的一道题目。
题目
核心思路
这道题目使用暴力的解法显然是不行的,单单是枚举所有子序列都需要指数级别的时间复杂度,而题目需要的算法为O(NlogN),显然会超时。不过注意到给定数组中元素的大小范围限定在[1, 200000],给了这一限制,显然是一个突破口。
所以不如直接枚举 元素的值i ∈ [1, 200000]
,为了得到结果,就需要判断当前元素i
是否能作为某一个子序列的最大公约数。如果可以的话,这个子序列中的元素必然都能被i
整除,例如:i, 2i, 4i, 5i, 9i
,那么就可以枚举i, 2i, 3i, 4i, ...
并判断其是否在数组中,然后将其加入到一个可能的子序列里,并求得这个子序列的最大公约数curGcd
,如果curGcd == i
的话,就代表枚举到的i
是满足的,将答案加一即可。
这里需要承认的一个事实是:如果存在一个子序列a, b, c
的最大公约数为i
,那么新加入一个元素x
,整个序列a, b, c, x
的最大公约数应该为gcd(i, x)
有了这个事实,在求子序列的最大公约数时就可以每添加一个元素求解一次curGcd
,然后判断是否为目标值i
即可,不需要维护这个枚举的子序列了。
完整代码
class Solution {
public int gcd(int a, int b){
return a % b == 0 ? b : gcd(b, a % b);
}
public int countDifferentSubsequenceGCDs(int[] nums) {
boolean[] mark = new boolean[200001];
int maxNum = 0;
for(int num : nums) mark[num] = true;
int res = 0;
for(int i = 1; i < 200001; i++){
int curGcd = 0;
for(int j = i; j < 200001; j += i){
if(mark[j]){
if(curGcd == 0) curGcd = j;
else curGcd = gcd(curGcd, j);
if(curGcd == i){
res++;
break;
}
}
}
}
return res;
}
}
代码也不难,就是根据上边的思路维护一个结果res
和当前子序列的最大公约数curGcd
即可。比较难理解的就是算法的时间复杂度的计算了,看似时间为O(N ^ 2),不过实际的复杂度应该为O(NlogN),这里的N为数组元素的最大值,代码为了简洁取了数据范围峰值,具体计算方法如下:
通过上述公式计算,总的时间复杂度应该为O(m * logm),m代表数组元素的最大值,是满足题目要求的。