可被 k 整除的子数组数

Posted

技术标签:

【中文标题】可被 k 整除的子数组数【英文标题】:Number of subarrays divisible by k 【发布时间】:2013-05-12 10:36:02 【问题描述】:

我在一次采访中遇到了以下问题,尽管我给出了一个可行的实现,但它的效率还不够高。

数组 A 的切片是任意一对整数 (P, Q),满足 0 ≤ P ≤ Q

要求我编写的函数必须返回可被 K 整除的切片数。预期时间复杂度为 O(max(N, K)),空间复杂度为 O(K)。

我的解决方案是最简单的,一个循环在另一个循环中检查每个切片:O(n^2)

我一直在想,但我真的不知道如何在 O(max(N, K)) 中做到这一点。

它可能是subset sum problem 的变体,但我不知道如何计算每个子数组。

编辑:数组中的元素可能是负数。这是一个例子:

A = 4, 5, 0, -2, -3, 1, K = 5

Function must return 7, because there are 7 subarrays which sums are divisible by 5
4, 5, 0, -2, -3, 1
5
5, 0
5, 0, -2, -3
0
0, -2, -3
-2, -3

【问题讨论】:

【参考方案1】:

由于您只对可被 K 整除的数字感兴趣,因此您可以以 K 为模进行所有计算。 考虑累积和数组 S 使得S[i] = S[0] + S[1] + ... + S[i]。那么 (P, Q) 是一个可以被 K 整除的切片,当且仅当 S[P] = S[Q](请记住,我们进行所有模 K 的计算)。因此,您只需计算 [0 ,..., K-1] 的每个可能值它在 S 中出现的次数。

这是一些伪代码:

B = new array( K )
B[0]++
s = 0
for i = 0 to N - 1
  s = ( s + A[i] ) % K
  B[s]++
ans = 0
for i = 0 to K - 1
  ans = ans + B[i] * ( B[i] - 1 ) / 2

一旦您知道它们是 S 中具有值 i 的 x 个单元格,您想要计算从具有值 i 的单元格开始到具有值 i 的单元格结束的切片数,这个数字是 x ( x - 1 ) / 2。为了解决边缘问题,我们添加一个值为 0 的单元格。

x ( x - 1 ) / 2 代表什么:假设我们的数组是 [4, 5, 0] 并且 4 作为前缀和的频率是 x,在这种情况下是 3。现在我们可以从 x 的值得出结论,至少有 x-1 个数字可以被 k 整除或 mod k 等于 0。现在这些 x-1 数字中可能的子数组总数为 1 + 2 + 3 ... + ( x - 1 ) 即 ( ( x - 1 ) * ( ( x - 1 ) + 1 ) / 2 。 (从 1 到 N 求和的标准公式,其中 N 代表 ( x - 1 )。

【讨论】:

它没有改变任何东西,我的解决方案仍然有效。 B[0]++和B[S]++是什么意思? @Thomash 从理论上讲,模数仅用于正数。对于负数,模没有定义的值。如果您声称您的解决方案适用于负数,则应定义模数如何适用于负数。 B[0]++ 是干什么用的? ans = ans + B[i] * ( B[i] - 1 ) / 2 我想为其他不理解的人澄清这一点:B 包含每个索引 mod 的总和 mod K 的数量。由于我们试图找到 sum mod K = 0 中的差异数,因此实现这一目标的唯一方法是减去数组中具有相同 mod 的两个和。这一行通过在 B 的每个 mod 中找到 2 组合的数量来实现这一点。 n 选择 2 = n!/(2!(n-2)!) = n(n-1)/2【参考方案2】:

这是@Thomash 提出的解决方案的Java 实现。

第二个循环不是必须的,因为我们可以直接将答案增加当前值,然后增加它。

为了避免负数组索引,我们还必须调整模块计算。

public static int countSubarrays(int[] nums, int k) 
    int[] cache = new int[k];
    cache[0]++;
    int s = 0, counter = 0;
    for (int i = 0; i < nums.length; i++) 
        s = ((s + nums[i]) % k + k) % k;
        counter += cache[s];
        cache[s]++;
    

    return counter;

【讨论】:

【参考方案3】:

示例:-

输入数组

int [] nums = 4,3,1,2,1,5,2;

K 是 3

连续求和

4,7,8,10,11,16,18

将上面的连续和数组除以3

1,1,2,1,2,1,0

所以我们有四个 1,两个 2,一个 0

所以总数将是 (4*3)/2 + (2*1)/2 + (2*1)/2 = 8

(4*3)/2 来自从四个中选择任意两个 1,即 nC2 = n(n-1)/2

这是程序

public static long countSubArrayDivByK(int k, int[] nums)

    Map<Integer, Integer> modulusCountMap = new HashMap<Integer, Integer>();
    int [] consecSum = new int[nums.length];
    consecSum[0]=nums[0];

    for(int i=1;i<nums.length;i++)
        consecSum[i]= consecSum[i-1] +nums[i];
    

    for(int i=0;i<nums.length;i++)
        consecSum[i]= consecSum[i]%k;

            if(consecSum[i]==0 && modulusCountMap.get(consecSum[i])==null)
                modulusCountMap.put(consecSum[i], 2);
            else
                modulusCountMap.put(consecSum[i], modulusCountMap.get(consecSum[i])==null ? 1 : modulusCountMap.get(consecSum[i])+1);
            

    

    int count = 0;

    for (Integer val : modulusCountMap.values()) 
        count = count +  (val*(val-1))/2;
    

    return count;

上述优化版

static long customOptimizedCountSubArrayDivByK(int k, int[] nums) 

        Map<Integer, Integer> modulusCountMap = new HashMap<Integer, Integer>();
        int [] quotient = new int[nums.length];
        quotient[0]=nums[0]%3;



        if(quotient[0]==0)
            modulusCountMap.put(quotient[0], 2);
        else
            modulusCountMap.put(quotient[0], 1);
        


        for(int i=1;i<nums.length;i++)
            quotient[i]= (quotient[i-1] + nums[i])%3;


                if(quotient[i]==0 && modulusCountMap.get(quotient[i])==null)
                    modulusCountMap.put(quotient[i], 2);
                else
                    modulusCountMap.put(quotient[i], modulusCountMap.get(quotient[i])==null ? 1 : modulusCountMap.get(quotient[i])+1);
                

        

        int count = 0;

        for (Integer val : modulusCountMap.values()) 
            count = count +  (val*(val-1))/2;
        

        return count;
    

【讨论】:

【参考方案4】:
    private int GetSubArraysCount(int[] A, int K)
    
        int N = A.Length;
        int[] B = new int[K];
        for (int i = 0; i < B.Length; i++)
        
            B[i] = 0;
        
        B[0]++;
        int s = 0;
        for (int i = 0; i < N; i++)
        
            s = (s + A[i]) % K;
            while (s < 0)
            
                s += K;
            
            B[s]++;
        
        int ans = 0;
        for (int i = 0; i <= K - 1; i++)
        
            ans += B[i] * (B[i] - 1) / 2;
        
        return ans;
    

【讨论】:

【参考方案5】:

感谢your solution、@damluar,不过它非常简洁!我只想添加一些cmets。

    输出应该是 7,而不是 6 作为您的输出。因为我们有 7 个可以被 k 整除的子数组,如下所示,添加 res += storedArray[0]; 来修复它。

4, 5, 0, -2, -3, 1; 5; 5, 0; 5, 0, -2, -3; 0; 0, -2, -3; -2, -3

Ref link

    初始化 cache[0]++; 取决于语言,如果使用 C++,则需要,但对于 java [link] 则不需要。

代码:

public class HelloWorld

public static void main(String []args)
    int [] A = new int[] 4,5,0,-2,-3,1;
    int k = 5;
    int ans=0;
    System.out.println(countSubArray(A, k)); // output = 7



public static int countSubArray(int [] nums, int k)
    int [] storedArray = new int[k];
    int sum=0, res=0;
    for(int i=0; i<nums.length; i++)
        sum = (((sum + nums[i]) % k) + k) % k;
        res += storedArray[sum];
        storedArray[sum]++;

    
    res += storedArray[0];
    return res; 


【讨论】:

【参考方案6】:
static void Main(string[] args)
    
        int[] A = new int[]  4, 5, 0, -2, -3, 1 ;
        int sum = 0;
        int i, j;
        int count = 0;
        for (i = 0; i < A.Length; i++)
        
            for (j = 0; j < A.Length; j++)
            
                if (j + i < 6)
                    sum += A[j + i];
                if ((sum % 5) == 0)
                    count++;

            
            sum = 0;
        
        Console.WriteLine(count);
        Console.ReadLine();


    

【讨论】:

建议:您在内部循环中遍历整个数组,而实际上您只需要遍历其中的一部分——因为j + i 将在中途达到 6。此外,即使在该迭代中没有设置它,您也正在使用 sum 进行比较。这个解决方案还需要一些工作。 OP 要求一种有效的方法,这是一个蛮力解决方案【参考方案7】:
public class SubArrayDivisible 


    public static void main(String[] args) 
    
        int[] A = 4, 5, 0, -2, -3, 1;
        SubArrayDivisible obj = new SubArrayDivisible();
        obj.getSubArrays(A,5);
    

    private void getSubArrays(int[] A,int K)
    
        int count = 0,s=0;
        for(int i=0;i<A.length;i++)
        
            s = 0;
            for(int j = i;j<A.length;j++)
            
                s = s+A[j];
                if((s%K) == 0)
                
                    System.out.println("Value of S "+s);
                    count++;
                

            
        
        System.out.println("Num of Sub-Array "+count);
    

【讨论】:

这是我在采访中给出的解决方案,但它的复杂度是 O(n!),他们要求 O(max(k,n))。托马斯的解决方案是正确的

以上是关于可被 k 整除的子数组数的主要内容,如果未能解决你的问题,请参考以下文章

可被 k 整除的子数组数

974. 和可被 K 整除的子数组

[LeetCode] 974. 和可被 K 整除的子数组 !!!

[LeetCode] 974. 和可被 K 整除的子数组 !!!

计算总和可被 k 整除的子序列总数

⭐算法入门⭐《前缀和》中等02 —— LeetCode 974. 和可被 K 整除的子数组