问题描述
有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。
时间:4 sec
空间:512 MB
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
思路
问题的数学描述:设 n 个非负整数组成的序列为 a1, a2, ..., an,将它们分成连续的 m (0 < m ≤ n) 组,对于第 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 不是一种可能的分组部分和上限,那么比 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] 及它前面的若干元素。那么可以最多包含哪些元素呢(为什么考虑最多呢。显然的,若序列 a 能分成 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; }