信息学奥赛一本通 1243:月度开销 | OpenJudge NOI 1.11 06:月度开销

【题目链接】

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个数字一定是第1个子段的元素
  2. 证明:前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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值