【题目链接】
ybt 1243:月度开销
OpenJudge NOI 1.11 06:月度开销
【题目考点】
1. 二分答案
【解题思路】
该题可以抽象为:给定数字序列,将该数字序列分为m个子段,每个子段的数字加和为子段和,一种划分方案有一个最大子段和。比较各方案的最大子段和,存在一个方案的最大子段和最小。求最大子段和最小的方案中,最大的子段和。
可以反向思考,把枚举方案的问题变为判断一个方案是否可行。
假定给定了最大子段和k,那么每个子段的和都必须小于等于k,在这一要求下看分成是子段最少数量是否小于等于m。
自然最大子段和k越小,分成的子段数量越大。
如果最少子段数量小于等于m,还可以继续随意做分割,使子段数量等于m,这样的子段划分一定满足每个子段和小于等于k。接下来可以看看k能否更小。
如果最少子段数量大于m,那么在最大子段和为k的情况下,无法分成m段。k应该取更大的值。
给定最大子段和k,求最少子段数量的方法:顺序遍历数组,对于每个数字,只要能加入前一个子段后,子段和不超过k,那么就加入前一个子段。否则自己作为下一个子段的第一个元素。
这是一种贪心选择,可以证明其贪心选择性质
用数学归纳法证明:
- 证明:第1个数字一定是第1个子段的元素
- 证明:前k次都做了贪心选择,存在最优解包含第k+1次的贪心选择
如果第k+1个数加入到前一个子段中,前一个子段的加和超过k,那么第k+1个数只能自己新开一个子段,作为子段的第1个元素。
如果第k+1个数可以加入前一个子段。用反证法,假设对所有最优解,第k+1个数没有加入前一个子段,而是自己作为新子段的第1个元素。
在这个最优解中,把第k+1个数从其当前所属的子段中删去,将其加入前一个子段中。总子段数不变(或减少),仍然是最优解,这与假设相悖,原命题得证。
按照二分的答案的思路来描述:
判断条件:最大子段和为k时,最少子段数量小于等于m
- 如果满足条件,k取更小的值,取左半边区间。
- 如果不满足条件,k取更大的值,取右半边区间。
【题解代码】
解法1:二分答案
#include <bits/stdc++.h>
using namespace std;
#define N 100005
int n, m, a[N];
bool check(int k) //在每个子段和小于等于k的情况下,最少的子段数量是否小于等于m
{//注意:a中的单独一个元素也可能大于k
int sum = 0, ct = 1;//sum:当前子段加和 ct:现在在看第几个子段
for(int i = 1; i <= n; ++i)
{
if(a[i] > k)//存在元素大于k,
return false;
if(sum + a[i] <= k)
sum += a[i];
else
{
ct++;//看下一子段
sum = a[i];//i作为下一子段的第一个元素
}
}
return ct <= m;
}
int main()
{
int tot = 0;//加和
cin >> n >> m;
for(int i = 1; i <= n; ++i)
{
cin >> a[i];
tot += a[i];
}
int l = 0, r = tot, mid;//子段和最大值不会大过所有数的加和
while(l < r)//二分答案求满足某一条件的最小值
{
mid = (l + r) / 2;
if(check(mid))
r = mid;
else
l = mid + 1;
}
cout << l;
return 0;
}