2447. 最大公因数等于 K 的子数组数目

难度中等

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 nums 的子数组中元素的最大公因数等于 k 的子数组数目。

子数组 是数组中一个连续的非空序列。

数组的最大公因数 是能整除数组中所有元素的最大整数。

示例 1:

输入:nums = [9,3,1,2,6,3], k = 3

输出:4

解释:nums 的子数组中,以 3 作为最大公因数的子数组如下:

  • [9,3,1,2,6,3]
  • [9,3,1,2,6,3]
  • [9,3,1,2,6,3]
  • [9,3,1,2,6,3]

示例 2:

输入:nums = [4], k = 7

输出:0

解释:不存在以 7 作为最大公因数的子数组。

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i], k <= 109

# 暴力枚举

class Solution {
    public int subarrayGCD(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int g = 0;
            // [i ~ j]
            for (int j = i; j < n; j++) {
                // 区间数组的公共最大公约数
                g = gcd(g, nums[j]);
                
                // 说明不是 k 的倍数,就是 nums [j] 整除不了 k 
                // 要满足每个数字都是 k 的倍数
                if (g % k != 0) {
                    break;
                }
                //g 必然是 k 的倍数
                //  - 若 g == k, 后面满足条件的话,g 依然等于 k
                if (g == k) {
                    ans++;
                }
            }
        }
        return ans;
    }
    public int gcd(int a, int b) {
        while (b > 0) {
            int t = a % b;
            a = b;
            b = t;
        }
        return a;
    }
}

一开始写的代码

class Solution {
    public int subarrayGCD(int[] nums, int k) {
        int n = nums.length;
        //  9 3 1 2 6 3 12
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (nums[i] % k != 0) {
                continue;
            }
            boolean f = false;
            if (nums[i] == k) {
                f = true;
                ans++;
            }
            // [i ~ j]
            for (int j = i + 1; j < n; j++) {
                // 当前 j 不行,后面肯定不行
                if (nums[j] % k != 0) {
                    break;
                }
                // 有希望
                // 看前面 f 是不是 t : 至少有一个最大公约数是 k
                //	若是就不用在遍历前面的元素,直接下一个
                if (f) {
                    ans++;
                } else {
                    for (int l = i; l < j; l++) {
                        if (gcd(nums[j], nums[l]) == k) {
                            ans++;
                            f = true;
                            break;
                        }
                    }
                }
            }
        }
        return ans;
    }
    public int gcd(int a, int b) {
        while (b > 0) {
            int t = a % b;
            a = b;
            b = t;
        }
        return a;
    }
}

# 利用 gcd 的性质

枚举子数组的右端点,去看有多少个不同的 GCD

暴力枚举就是计算了重复的 GCD
由于有大量的重复的 GCD , 需要去重

  • 每个元素不同的 GCDlog(nums[i])log(nums[i])
    • 一个元素的因子要么是自身
    • 要么小于等于该元素的一半
  • 所有子数组至多有 nlog(U)nlog(U) 个不同的 GCD
    相同的 GCD 都相邻,在一起

相同的 GCD 都相邻,在一起


由于 GCD 是递增的,所以只需看去重后第一个 GCD 是不是 k
k = 3 ,前面 2 的倍数不能算,需要保存上一次失败元素的下标 fi

  • 保存 fi 为了快速取 k = 3 的个数

每一次 GCD 都用上一次的 GCD 求出当前 GCD

  • 因为若 GCD 相同,说明是重复的,只需要求解一次即可,没有必要全部求解

img

import java.util.ArrayList;
import java.util.List;
class Solution {
    public int subarrayGCD(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        //[GCD, 相同 GCD 区间的右端点]
        // 以 i 结尾的 gcd 数组 gs
        List<int[]> gs = new ArrayList<>();
        int fi = -1;
        for (int i = 0; i < n; i++) {
            if (nums[i] % k != 0) {
                fi = i;
                gs.clear();
                continue;
            }
            gs.add(new int[]{nums[i], i});
            int j = 0;
            // 原地去重
            for (int[] g : gs) {
                int gcd = gcd(nums[i], g[0]);
                g[0] = gcd;                
                if (gs.get(j)[0] != g[0]) {
                    gs.set(++j, g);
                } else {
                    // 更新相同 GCD 区间的右端点
                    gs.get(j)[1] = g[1];
                }
            }
            // 删除 j 后面的元素
            gs = gs.subList(0, j + 1);
            int[] g = gs.get(0);
            if (g[0] == k) {
                ans += g[1] - fi;
            }
        }
        return ans;
    }
    public int gcd(int a, int b) {
        while (b > 0) {
            int t = a % b;
            a = b;
            b = t;
        }
        return a;
    }
}

# 复杂度分析

  • 时间复杂度O(nlogU)O(nlogU)gcdgcd 减半的次数为 logUlogU,每个数只会被加入数组 aa 一次,然后随着新元素加入进来后不断 gcdgcd 变得越来越小,最多减小 O(logU)O(logU) 次。

  • 空间复杂度O(logU)O(logU)