面试算法:在整形数组中构建元素之和能整除数组长度的子集

28 篇文章 2 订阅

更详细的讲解和代码调试演示过程,请参看视频
如何进入google,算法面试技能全面提升指南

假设A是一个整数数组,长度为n,数组中的元素可能是重复的。设计一个算法,找到一系列下标的集合I = {i(0), i(1), i(2)….i(n)}. 使得(A[i(0)] + A[i(1)] + … A[i(n)] ) mod n = 0.例如假定A = {711, 704, 427, 995, 334, 62, 763, 98, 733, 721}, 于是I = {0,1,3}, 因为(A[0] + A[1] + A[3] ) mod 10 = 0.
请给出一个有效的算法找到满足条件的集合I, 无论A的元素如何取值,这样的集合总是存在的。

这是一道较为复杂的算法题,能在一个小时内完成绝非易事。我们需要解决两个问题,第一需要证明,为何这样的集合肯定存在,第二,集合存在,如何把它给挖掘出来。我们先证明第一个问题。

1:如果数组A中含有元素它的值是0,那么满足条件的集合存在,这个集合就只包含元素0.

2:如果数组A只有一个元素,也就是A的长度只有1,那么A本身就是满足条件的集合。

3:如果A包含不只一个元素,我们用归纳法来证明。假设n=k时,我们能从A中找到给定集合,使得(A[i(0)] + A[i(1)] + … A[i(n)] ) mod n = 0,那么当n=k+1时,我们从数组A中任意取出一个元素,如果被拿走的这个元素值是1,那么根据归纳法,我们可以从剩下的k个元素中,找到一个子集,子集中的元素之和能够整除k, 把子集中的元素加上拿走的那个值为1的元素形成新的集合,这个集合之和能够整除k+1.

如果拿走的元素值为t > 1, 剩下的元素个数为k, 那么肯定有 k > k+ 1 - t。我们从剩下的k 个元素中,随便选出 k+1-t 个元素,在这些元素中,根据归纳法,肯定存在一个子集,使得子集元素的和能整除 k + 1 - t. 把这些子集的元素加上前面拿走的元素,那么所形成的新集合,一定能整除 k + 1

综上,我们就证明了,这样的集合是一定存在的,接下来我们需要考虑的是,如何找到这个集合。

我们看看,把元素逐个加总后对n求余会有什么性质:
sum = (A[0] + A[1] + …. + A[j]) mod n
j 由0一直增加到n-1.

在这个过程中,一定会出现下面这种情况:
存在一个 i, 0 <= i < j, 使得 (A[0]+A[1]+…+A[i]) mod n = (A[0]+A[1] + …A[j]) mod n. (*)

当上面情况出现时,我们有(A[i+1] + A[i+2] +… + A[j]) mod n = 0;

于是(A[i+1], … ,A[j]) 就是满足条件的子集。为何(*)所描述的情况一定会发生呢?我们知道,对某个数求余,所得结果必然小于该数,因此对n求余,那么结果一定落入到1和n-1之间。

sum = (A[0] + A[1] + …. + A[j]) mod n

j 由0一直增加到n-1, 也就是上面的计算最多会进行n 次,得到n个求余的结果,然而求余的结果最多只可能有n-1中不同情况,那么n个结果中,肯定得有出现重复的时候,一旦出现重复的时候,(*)所描述的情况就出现了。

根据上面算法原理,我们可以实现如下代码:

public ArrayList<Integer> moduleSubSet(int[] A) {
        int[] B = new int[A.length];
        for (int i = 0; i < B.length; i++) {
            B[i] = 0;
        }

        int sum = 0;
        ArrayList<Integer> subSet = new ArrayList<Integer>();
        for (int i = 0; i < A.length; i++) {
            sum += A[i];
            subSet.add(i);
            int t = sum % A.length;
            if (t == 0) {
                return subSet;
            }

            int pre_sum = 0;
            if (t != 0) {
                if (B[t] != 0) {
                    for (int j = 0; j < i; j++) {
                        pre_sum += A[j];
                        if (pre_sum % A.length != t) {
                            subSet.remove((Integer)j);
                        } else {
                            subSet.remove((Integer)j);
                            return subSet;
                        }
                    }
                }

               B[t] = 1;
            }
        }

        return null;
    }

在for循环中,我们把数组A的元素逐个相加,然后对长度n求余,所得结果记为t,然后拿这个结果到另一个校验数组B中检验一下,如果B[t] 的值是1,这表明(*)所描述的情况出现了,此时我们再遍历以便(A[0]….A[j]) 把i找出来,找到i的办法就是再次将元素逐个相加,当所得结果对n求余后,与前面算得的t相等,那么此时的元素下标i满足条件,找到i之后,再把下标0到i从集合中去除,那么集合中剩下的下标就是对应自己中的元素在原数组中的下标。

我们再看看主入口函数的实现:

 public static void main(String[] args) {
        ArrayAndString  as = new ArrayAndString();
        int[] A = new int[]{1,1,1,3};
        //int[] A = new int[]{711, 704, 427, 995, 334, 62, 763, 98, 733, 721};
        ArrayList<Integer> subSet = as.moduleSubSet(A);
        System.out.println("sub set is: ");
        for (int i = 0; i < subSet.size(); i++) {
            System.out.print(subSet.get(i) + " ");
        }
    }

上面代码运行后结果为:
sub set is:
1 2 3 4
也就是说(A[1] + A[2] + A[3] + A[4]) mod 10 = 0, 大家可以自己检测一下,所得结果确实是正确的。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值