减而治之 - 二分查找 - 分组

问题描述

有n个正整数排成一排,你要将这些数分成m份(同一份中的数字都是连续的,不能隔开),同时数字之和最大的那一份的数字之和尽量小。

输入

输入的第一行包含两个正整数n,m。

接下来一行包含n个正整数。

输出

输出一个数,表示最优方案中,数字之和最大的那一份的数字之和。

样例输入

5 2
2 1 2 2 3

样例输出

5

样例解释

若分成2和1、2、2、3,则最大的那一份是1+2+2+3=8;

若分成2、1和2、2、3,则最大的那一份是2+2+3=7;

若分成2、1、2和2、3,则最大的那一份是2+1+2或者是2+3,都是5;

若分成2、1、2、2和3,则最大的那一份是2+1+2+2=7。

所以最优方案是第三种,答案为5。

限制

对于50%的数据,n ≤ 100,给出的n个正整数不超过10;

对于100%的数据,m ≤ n ≤ 300000,给出的n个正整数不超过1000000。

扫描二维码关注公众号,回复: 5585331 查看本文章

时间:4 sec

空间:512 MB

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

思路

       问题的数学描述:设 n 个非负整数组成的序列为 a1a2, ..., an,将它们分成连续的 m (0 < mn) 组,对于第 i 个分组 (1 ≤  i ≤ m),组内元素为 ai1, ai2, ... aiki,它们的和为 si。设 s = max(s1, s2, ..., sm),求所有可能的 s 的最小值。

      这道题有两个难点。第一是想到用二分查找来解题;第二是如何快速判断出某个数 s 是否是一种可能的分组部分和上限(即将这 n 个数分成 m 个连续的分组,每个分组内数字之和都不大于 s),比如线性时间内完成判断。

        首先,注意到,对于两个数 s1 < s2,如果 s1 是一种可能的分组部分和上限,那么 s2 一定也是一种可能的分组部分和上限。显然的,部分和上限的最小可能取值是 0,最大可能取值是序列所有元素之和。这从小到大的若干数中:

① 如果某个数 s 是一种可能的分组部分和上限,那么比 s 大的那些数一定不是我们想要求的分组部分和上限的最小值。因为若分成 m 组,每组的数字和都不大于 是可能的,而我们要求的是所有可能 s 的最小值,自然比 s 大的那些备选都可以不再考虑了。

② 相反的,如果某个数 s 不是一种可能的分组部分和上限,那么比 s 小的那些数也一定不是我们要求的结果。因为若分成若分成 m 组,已知不可能每组的数字和都不大于 s,那么对于比 s 小的那些数,就更不可能存在一种分组方式使得每组的数字和都小于它们了。

这就提示我们可以用二分查找来找到结果。具体过程的要旨如下图所示。

      想到可以用二分在所有可能的数字中来查找结果之后,剩下的难点就是:对于一个数 s,如何快速判断出对于序列 a,是否能将它分成连续的 m 组,每组数字和都不大于 s。其实就可以顺序的来做,即从第一个数开始,尽可能的多包含新数进来,直至若再加入下一个数部分和超过 s,则新开始一个分组。如果这样分最后序列能分成不超过 m 组,则是可能的;如果分成超过 m 组,则一定是不可能的。该算法的正确性个人觉得看起来并不直观,其需要证明。

首先,如果我们这样做,最后分成了不大于 m 组,那么“将序列 a 分成 m 份,每份和不大于 s 是可能的”,这个结论是显然的。因为若最后我们这样从第一个数开始,使分组尽可能多的包含数字,最后分得的组数刚好为 m,那我们已经给出了一种分组方案;若这样分成的组数小于 m,那么只要把那些包含多于一个数的分组拆开,直至拆出 m 组即可。因为原来每个分组数字和都不大于 s,拆分后分组和显然更加不大于 s 了,且 m 不大于序列元素个数,那么一定可以在原分组基础上拆分出 m 组。 

那么,重点就是证明:如果按我们的分法最后得到的分组数大于 m,那么一定也不存在其他分组方案,使得分出的 m 组的每部分和都不大于 s。可以这样考虑,设按照我们的分法,最后得到的分组是这样的:a[1], ..., a[p1] } , { a[p1+1], ..., a[p2] }, ..., { a[pm-1+1], ..., a[pm] }, {a[n] } (为了方便,这里下标写在了中括号内)。为了简单,这里假设按我们的分法分出 m 组之后序列只剩最后一个元素,它构成第 m+1 组。若剩更多的元素、分成更多的组也无所谓,证明思路没差别,这里就不赘述了。

考虑反证,如果存在一种分组方案,可以使得序列 a 被分成 m 组,且每组的数字和都不大于 s。那么,最后一个分组一定包含 a[n] 及它前面的若干元素。那么可以最多包含哪些元素呢(为什么考虑最多呢。显然的,若序列  能分成 m 组且每组数字和不大于 s,那么只取它前面不少于 m 的若干项,显然一定可以分出 m 组,每组不大于和 s)?由于按我们的分法,a[pm-1+1], ..., a[pm] 构成一个分组,也就是说 a[pm-1+1] + ... + a[pm]  ≤ s, 但  a[pm-1+1] + ... + a[pm]  + a[n] > s,那么,至多包含到 a[pm-1+2]。这也就是说,如果对原序列存在一种满足条件的分组方案,那么 a[1], ..., a[pm-1+1] 必须能分成 m-1 组,且每组的数字和不能超过 s。那么,类似的,最后一组必须包含 a[pm-1+1] 和它前面若干项,最多可以包括哪些呢?类似上一步可知,根据我们的分法,最多包含到 a[pm-2+2]。那么 a[1], ..., a[pm-2+1] 能分成 m-2 组吗···· 依次类推,最后得到,若对序列 a 存在一种满足条件的分组方案,那么必须 a[1], ..., a[p1], a[p1+1] 能分成一组,然而根据我们的分法可知 a[1] + ... + a[p1] + a[p1+1] > s,矛盾。故由反证可知,如果按我们的分法不能实现对序列 a 分成 m 组且每组数字和不大于 s,那么也一定不存在其他满足要求的分法。

        综合以上两点,可以写出求解该题的代码如下。时间复杂度:二分为 O( log(Σa[i]) ),二分过程中每次判断是否是可能的分组方案 O(n),总的复杂度为 O(nlog(Σa[i]))。

C++代码

#include <cstdio>
#include <cstring>
using namespace std;

const int MAXN = 300005;    // 最多可能数字个数 
int a[MAXN];    // 用于存储数字序列

/* 判断从a开始的n个非负整数分成连续的m份,且每份数字和不超过s是否可能。 */ 
bool check(int * a, int n, int m, long long s) 
{
    int p = 1;    // 记录尝试分割过程中已分的份数 
    long long part_sum = 0;    // 尝试分割过程中,当前分割的部分和 
    for ( int i = 0; i < n; ++i )
    {
        if ( (long long)a[i] > s )  // 若初始p=1,一定要判断单个元素是否大于s 
            return false;
        if ( part_sum + (long long)a[i] > s )    // 若当前组再加上一个元素就超过部分和的限制s,则新开一个组 
        {
            ++p;
            part_sum = (long long)a[i];
        }
        else                                     // 若不超过,则当前分组包入当前元素后继续 
            part_sum += (long long)a[i];
        if ( p > m )
            return false; 
    }
    return true; 
} 

/* 将从a开始的n个非负整数分成连续的m份,使各份数字和最大值最小的方案对应的最大数字和。 */
long long minMaxSum(int * a, int n, int m)
{
    long long lo = 0, hi = 0, mid = 0;
    for ( int i = 0; i < n; ++i )
        hi += a[i];
    while ( lo <= hi )
    {
        mid = (lo + hi) / 2;
        if ( check(a, n, m, mid) )    // 若mid是一种可能的分组和上限,则往小的一半中继续查找(分组和最大值上限越小,越严苛) 
            hi = mid - 1;
        else                          // 若mid不是一种可能的分组和上限,则往大的一半中继续查找 
            lo = mid + 1;
    }
    return lo; 
}

int main()
{
    int n = 0, m = 0;
    scanf("%d %d", &n, &m);
    for ( int i = 0; i < n; ++i )
        scanf("%d", a+i);
    
    long long ans = minMaxSum(a, n, m);
    printf("%lld\n", ans); 
    
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/fyqq0403/p/10557193.html