总结一类编程题--数组的n项和为M的存在性问题

引言


最近春招,同学都在各种面试和各种刷题,面试完之后常常互相分享在面试过程中遇到的题目,在分享过程中,我发现有些题目之间有雷同之处,所以总结一下

先上两道题


360的2018年面试真题:

小明手中有一张游乐园的游玩券,凭该券可以在游乐园中游玩时长度为T的时间,游乐园中有一些项目,它们占用的时长分别是t1,t2,t3,t4等等(每个项目最多只能玩一次),如果T时间到了,但是有一个项目的游玩还没有结束,那么小明可以继续把这个项目玩完,请问小明在游乐园中最多可以游玩多长时间?

为了更清晰地说明题意,举个例子:
  假如T是100,游乐园中项目的时长分别是10,20,33,40,50。那么为了在游乐园中可以游玩的最长时间是20 + 33 + 40 + 50=143分钟(在100分钟到的时候,因为那个50分钟的项目还没有结束,所以可以把游玩时间一直拖延到最后的那个50分钟的项目结束)。

网易2015年笔试真题:

任意2n个数,从中选出n个数,使得它们的和与剩下的数的和的差最小。

题目分析


两道题目看起来没什么关系,但是进过分析之后可以看出他们都衍生自同一个基本问题,分析如下:

第一题

为了能够游玩的时间尽可能地长,时间最长的那个项目肯定要放在最后进行游玩,这样才能尽可能地拖延游玩时间,然后我们要从剩下的项目中选取n个项目,使得这n个项目时间的和最接近T(但是必须要小于T),然后再加上时长最长的那个项目就可以得到最长游玩时间了。
  举个例子:T为100,游乐园中项目的游玩时间分别为10,20,33,40,50,我们首先将50放在一旁,然后发现剩下的项目中(20 + 33 + 40 = 93)的和是最接近T(=100)的,然后将其加上时间最长的那个项目(93 + 50 = 143)就是最长的游玩时间了。
  现在问题就转换成如何求出最接近T的那个n项和,假如我们我们有一个方法,可以快速查询出是否存在n项的和等于T,T-1,T-2......,这样我们就可以从T开始向前(从T到0)查询,查找到为true的第一个数就是那个最接近T的n项和,所以问题可以进一步转换为数组的n项和为M的存在性问题。下面我们将会看到第二道题最后也可以转换成这个问题。

第二题

为了能够在2n个数中找出n个数,使得这n个数的和与剩下的n个数的和尽可能地接近,那么选出的这n个数的和就要尽可能地接近2n个数的和的一半,这个问题就和第一题差不多了,根据第一题的分析,这个问题进一步可以转换成数组的n项和为M的存在性问题。

基本问题:数组的n项和为M的存在性问题--动态规划


从上面分析可以看出,只要解决了这个问题就可以通杀一开始给出的两道题了。
  如果使用暴力破解的话,n项的组合有n!种,那么时间复杂度是指数级,显然难以接受。使用动态规划的方法可以在伪多项式的时间复杂度内解决该问题。
  为了使用动态规划方法,我们定义一个布尔类型的数组flag[n][m],规定如果数组中存在n项的和为m,则flag[n][m]=true,反之flag[n][m]=false,则存在如下的递推关系(设nums代表数组):

flag[n][m]=flag[n-1][m-nums[i]]

上面的式子其实并不完全正确(只是为了表述得更加简洁),完整的算法是:只要在nums数组中存在一个数字num使得flag[n-1][m-num]为true,则flag[n][m]为true,否则为false。

计算flag数组

首先初始化flag数组的第一行(下标为0),它代表0项和的存在性,显然0项的和只能是0,所以第一行中只有flag[0][0]是true,其他都是false,然后使用上面给出的递推公式一行行算出flag数组。
  为了循序渐进地介绍flag数组的计算方法,这里先假设数组中的元素可以被重复使用,举个例子:数组为{1,5,6},那么该数组存在3项的和为3(即1+1+1,1被重复使用了三次)。允许重复使用数组中数字的计算nums数组的flag的Java代码如下:(注:代码使用了Java中布尔数组初始化时默认值为false的特性)

boolean[][] flag = new boolean[nums.length][T];
flag[0][0] = true;

//允许重复使用时的计算方法,假设待计算的数组为nums
for ( int i = 1; i < nums.length; i++ ){
    for ( int j = 0; j < T; j++ ){
        for ( int h = 0; h < nums.length - 1; h++ ){
            if ( j >= nums[h] && flag[i - 1][j - nums[h]] ){
                flag[i][j] = true;
                break;
            }
        }
    }
}

稍微解释一下代码,flag[0][0]=true这一句代码初始化了flag数组的第一行,从之前给出的flag数组递推公式可以看出,每一行的flag数组的计算只依赖于上一行的结果,所以我们从第二行开始一行一行进行计算,最外层的循环变量i代表flag数组的行,第二层循环j代表flag数组的列,第三层循环则是遍历nums数组,只要在nums数组中存在一个num使得flag[i - 1][j - nums[h]]为true,则flag[i][j]为true,注意我在判断flag[i - 1][j - nums[h]]之前还加了一个j >= nums[h]的判断条件,这是为了避免后面计算flag的时候下标为负导致程序出错。
  上面的代码假设了nums中的项可以重复使用,因为它将nums数组遍历放在了循环的第三层,计算每一个flag单元时都会尝试nums中的每一个数,不管它在计算上一行的时候有没有使用过。但是很多问题都要求数组中的项不能够重复使用,比如一开始给出的那一道360面试题中每个项目就只能够玩一次。为了让数组中每个项只使用一次,就需要将代码改一改,将nums数组遍历放在最外层循环中。不允许重复使用数组中数字的计算nums数组的flag的Java代码如下:

boolean[][] flag = new boolean[nums.length][T];
flag[0][0] = true;

//不允许重复使用时的计算方法
for ( int h = 0; h < nums.length - 1; h++ ){
    for ( int i = h + 1; i > 0; i-- ){
        for ( int j = 0; j < T; j++ ){
            if ( j >= nums[h] && flag[i - 1][j - nums[h]] ){
                flag[i][j] = true;
            }
        }
    }
}

为了方便理解,这个代码的i,j,h的含义和之前的代码是一样的,分别代表flag的行号,flag的列号以及nums数组下标。i从h +1开始遍历的原因时,h=0时只有一个数字,所以只能求出1项和(i=1),当h=1时有两个数字,可以求出1项和与2项和(i=1,2),以此类推。

使用基本问题来解决开头给出的问题


问题一

思路(续之前的分析):在这个问题中,我只关心是否存在n项和为T,T-1,T-2......,而并不关心n具体等于多少,所以我们在计算flag数组时顺带再计算一个一维数组colFlag[0...T],通过colFlag数组的下标就可以快速查询出是否存在n项和为T,T-1,T-2......,Java代码如下(基本上没怎么用到Java的特性,所以不熟悉Java的童鞋也不要担心看不懂):

static private void swap(int[] array, int k, int t){
    int temp = array[k];
    array[k] = array[t];
    array[t] = temp;
}

public static void main(String[] args) {
    int T = 100;
    int[] nums = {10,20,33,40,50};
    int max = 0;
    int maxindex = 0;
    for ( int i = 0; i < nums.length; i++ ){
        if ( nums[i] > max ){
            max = nums[i];
            maxindex = i;
        }
    }
    //将时间最长的项目放到最后
    swap(nums, maxindex, nums.length - 1);

    //n个数的和为m是否存在标志数组
    boolean[][] flag = new boolean[nums.length][T];
    flag[0][0] = true;

    //计算flag数组
    boolean[] colFlag = new boolean[T];
    for ( int h = 0; h < nums.length - 1; h++ ){
        for ( int i = h + 1; i > 0; i-- ){
            for ( int j = 0; j < T; j++ ){
                if ( j >= nums[h] && flag[i - 1][j - nums[h]] ){
                    flag[i][j] = true;
                    colFlag[j] = true;
                }
             }
        }
    }

    for ( int i = T - 1; i >= 0; i-- ){
        if ( colFlag[i] ){
            System.out.println("小明的最长游玩时间是:" + (i + nums[nums.length - 1]));
            break;
        }
    }
}

问题二

思路(续之前的分析):相比基本问题唯一的区别就是它的flag数组的行数不必和nums数组规模(2n)一样大,只要一半(n)就可以了,所以在求flag数的第二层循环i的初始值是((h + 1) > n ? n : h),代码如下:

public static void main(String[] args) {
    int nums[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    //计算数组和的一半
    int sum = 0;
    for (int i = 0; i < nums.length; i++) {
        sum += nums[i];
    }
    int n = nums.length / 2;
        
    //计算flag数组
    boolean flag[][] = new boolean[n + 1][sum / 2 + 1];
    flag[0][0] = true;
    for (int h = 0; h < 2 * n; h++) {
        for (int i = (h + 1) > n ? n : h; i > 0; i--) {//只需要求到n项即可
            for (int j = 0; j <= sum / 2; j++) {
                if (j >= nums[h] && flag[i - 1][j - nums[h]]) {
                    flag[i][j] = true;
                }
            }
        }
    }
        
    for (int i = sum / 2; i > 0; i--) {
        if (flag[n][i]) {
            System.out.println("最小差值为:" + (sum - 2 * i));
            break;
        }
    }
}

END


在面试前背一背flag数组的计算代码,应该就能轻松攻破这一类问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,723评论 0 33
  • 岩羊的精灵阅读 133评论 0 0
  • 再小的努力,乘以365都很明显。 再大的困难,除以365都变得微不足道。 你的坚持终将美好[太阳] 2810一2一...
    胜者为王王臣森阅读 237评论 0 0
  • 35岁 你因为身体越来越差 加班越来越少 晋升的速度也越来越缓慢 那天下班,媳妇告诉你 孩子要上幼儿园了 双语的一...
    Nikinger阅读 342评论 0 0
  • 堵车已经是城市生活中每天都会上演剧目,早晚上班高峰时,难免会因为路上一下子纷涌而来的车流而处于崩溃的境地。有的人会...
    夏日瑞西阅读 1,211评论 0 1