Skip to content

M04135: 月度开销

binary search, greedy , http://cs101.openjudge.cn/practice/04135

农夫约翰是一个精明的会计师。他意识到自己可能没有足够的钱来维持农场的运转了。他计算出并记录下了接下来 N (1 ≤ N ≤ 100,000) 天里每天需要的开销。

约翰打算为连续的M (1 ≤ MN) 个财政周期创建预算案,他把一个财政周期命名为fajo月。每个fajo月包含一天或连续的多天,每天被恰好包含在一个fajo月里。

约翰的目标是合理安排每个fajo月包含的天数,使得开销最多的fajo月的开销尽可能少。

输入

第一行包含两个整数N,M,用单个空格隔开。 接下来N行,每行包含一个1到10000之间的整数,按顺序给出接下来N天里每天的开销。

输出

一个整数,即最大月度开销的最小值。

样例输入

7 5
100
400
300
100
500
101
400

样例输出

500

提示:若约翰将前两天作为一个月,第三、四两天作为一个月,最后三天每天作为一个月,则最大月度开销为500。其他任何分配方案都会比这个值更大。

python
def minMaxMonthlyExpense(N, M, expenses):
    def can_split(max_expense):
        """ 判断是否能合并至多 M 个花费,使最大花费不超过 max_expense """
        months = 1  # 记录当前使用的月份数
        current_sum = 0 # 当前月的开销
        for cost in expenses:
            if current_sum + cost > max_expense:
                months += 1
                if months > M:
                    return False
                current_sum = cost
            else:
                current_sum += cost
        return True

    # 可能的最小开销范围。所以二分是在 [left, right) 区间内进行的
    left, right = max(expenses), sum(expenses) + 1
    ans = -1
    while left < right: # 二分查找最小的 "最大月度开销"
        mid = (left + right) // 2
        if can_split(mid):
            ans = mid   # 记录可行的 `mid`
            right = mid # 继续尝试更小的值
        else:
            left = mid + 1
    return ans

# 读取输入
N, M = map(int, input().split())
expenses = [int(input()) for _ in range(N)]

# 计算并输出答案
print(minMaxMonthlyExpense(N, M, expenses))

在所给的N天开销中寻找连续M天的最小和,即为最大月度开销的最小值。

OJ08210:河中跳房子 一样都是二分+贪心判断,但注意这道题目是最大值求最小。

参考 bisect 源码的二分查找写法,https://github.com/python/cpython/blob/main/Lib/bisect.py ,两个题目的代码均进行了规整。 因为其中涉及到 num==m 的情况,有点复杂。二者思路一样,细节有点不一样。

python
n,m = map(int, input().split())
expenditure = []
for _ in range(n):
    expenditure.append(int(input()))

def check(x):
    num, s = 1, 0
    for i in range(n):
        if s + expenditure[i] > x:
            s = expenditure[i]
            num += 1
        else:
            s += expenditure[i]
    
    return [False, True][num > m]

# https://github.com/python/cpython/blob/main/Lib/bisect.py
lo = max(expenditure)
# hi = sum(expenditure)
hi = sum(expenditure) + 1
ans = 1
while lo < hi:
    mid = (lo + hi) // 2
    if check(mid):      # 返回True,是因为num>m,是确定不合适
        lo = mid + 1    # 所以lo可以置为 mid + 1。
    else:
        ans = mid    # 记录可行的 `mid`
        hi = mid		 # 继续尝试更小的值
        
#print(lo)
print(ans)

为了练习递归,写出了下面代码

python
n, m = map(int, input().split())
expenditure = [int(input()) for _ in range(n)]

left,right = max(expenditure), sum(expenditure)

def check(x):
    num, s = 1, 0
    for i in range(n):
        if s + expenditure[i] > x:
            s = expenditure[i]
            num += 1
        else:
            s += expenditure[i]
    
    return [False, True][num > m]

res = 0

def binary_search(lo, hi):
    if lo >= hi:
        global res
        res = lo
        return
    
    mid = (lo + hi) // 2
    #print(mid)
    if check(mid):
        lo = mid + 1
        binary_search(lo, hi)
    else:
        hi = mid
        binary_search(lo, hi)
        
binary_search(left, right)
print(res)

2021fall-cs101,郑天宇。

一开始难以想到用二分法来解决此题,主要是因为长时间被从正面直接解决问题的思维所禁锢,忘记了对于有限的问题,其实可以采用尝试的方法来解决。这可能就是“计算思维”的生动体现吧,也可以说是计算概论课教会我们的一个全新的思考问题的方式。

2021fall-cs101,韩萱。居然还能这么做...自己真的想不出来,还是“先完成,再完美”,直接看题解比较好,不然自己想是真的做不完的。

2021fall-cs101,欧阳韵妍。

解题思路:这道题前前后后花了大概3h+(如果考试碰到这种题希望我能及时止损马上放弃),看到老师分享的叶晨熙同学的作业中提到“两球之间的最小磁力”问题的题解有助于理解二分搜索,去找了这道题的题解,看完之后果然有了一点思路,体会到了二分搜索其实就相当于一个往空隙里“插板”的问题,只不过可以运用折半的方法代替一步步挪动每个板子,从而降低时间复杂度。不过虽然有了大致思路但是还是不知道怎么具体实现,于是去仔仔细细地啃了几遍题解。def 的check 函数就是得出在确定了两板之间最多能放多少开销后的一种插板方法;两板之间能放的开销的最大值的最大值(maxmax)一开始为开销总和,两板之间能放的开销的最大值的最小值minmax)一开始为开销中的最大值,我们的目标就是尽可能缩小这个maxmax。如果通过每次减去1 来缩小maxmax 就会超时,那么这时候就使用二分方法,看看 (maxmax+minmax)//2 能不能行,如果可以,大于 (maxmax+minmax)//2的步骤就能全部省略了,maxmax 直接变为 (maxmax+minmax)//2;如果不可以,那么让minmax 变成 (maxmax+minmax)//2+1,同样可以砍掉一半【为什么可以砍掉一半可以这样想:按照check()的定义,如果输出了False 代表板子太多了,那么“两板之间能放的开销的最大值”(这里即middle)太小了,所以最后不可能采用小于middle 的开销,即maxmax不可能为小于middle 的值,那么这时候就可以把小于middle 的值都砍掉】

感觉二分法是用于在一个大范围里面通过范围的缩小来定位的一种缩短搜素次数的方法。

2021fall-cs101,王紫琪。【月度开销】强烈建议把 欧阳韵妍 同学的思路放进题解!对于看懂代码有很大帮助(拯救了我的头发)

python
n, m = map(int, input().split())
L = list(int(input()) for x in range(n))

def check(x):
    num, cut = 1, 0
    for i in range(n):
        if cut + L[i] > x:
            num += 1
            cut = L[i]  #在L[i]左边插一个板,L[i]属于新的fajo月
        else:
            cut += L[i]
    
    if num > m:
        return False
    else:
        return True

maxmax = sum(L)
minmax = max(L)
while minmax < maxmax:
    middle = (maxmax + minmax) // 2
    if check(middle):   #表明这种插法可行,那么看看更小的插法可不可以
        maxmax = middle
    else:
        minmax = middle + 1#这种插法不可行,改变minmax看看下一种插法可不可以

print(maxmax)