题目解析
1. 题目抽象与理解
首先,我们把问题抽象化:
- 有一个长度为
的数组 a,其中a[i]表示第天的支出。 - 需要将这个数组划分成 恰好
个连续的段(每个段是一个「fajo 月」)。 - 设第
个段的元素和为 S_k,我们要最小化max(S_1, S_2, ..., S_M)。
这就是经典的 「划分型二分」,也叫 「最小化最大子数组和」 问题。它类似于 LeetCode 的「410. Split Array Largest Sum」。
2. 核心思路:二分答案
观察
答案 [max_day, total_sum] 内:
- 下界
max_day = max(a[i]):如果小于单日最大开销,那么包含这一天的月份肯定装不下。 - 上界
total_sum = sum(a[i]):如果只用一个月(),总开销就是全部加起来。
单调性
当
- 当
时,可以划分成 个月份。 - 当
时,需要 个月份。
这个临界值
算法
二分枚举 check(X) 判断是否能将数组划分成 不超过
- 如果
check(X)为真,说明可能可以更小,令 right = X。 - 如果
check(X)为假,说明太小,令 left = X + 1。
贪心策略
从左到右扫描,在不超过上限 mid 的前提下,尽可能多地把天数塞进当前月份。这是一种「能装就装」的贪心,其正确性在于:
- 每个段都是连续且不可重排:我们必须按顺序装填。
- 越晚分段越好:如果把一些天提前分到下一月,只会让当前月变小、下一月变大,可能导致后面的分段更难满足
mid的限制。所以尽可能装满是最优策略。
时间复杂度:
为什么这种贪心是正确的?
反证法思路:
假设存在某种最优划分方式,在某个位置我们本可以继续将 a[i] 放入当前段(当前段和 + a[i] ≤ mid),但我们却选择新开一段。那么:
- 新开段的第一天是
a[i],当前段的和不变。 - 这会导致后面的天数更难分配到有限的段中(因为我们浪费了一次分段的机会)。
因此,尽可能装满当前段 的贪心策略不会比任何最优解差。
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github : https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* date: 2026-01-15 22:09:50
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 2e6+5;
int n,m;
int a[maxn];
int max_spend;
//oisnip_beginbinary_search.cpp
//这是最快的写法
ll mid(ll l,ll r) { return (l+r) >> 1; }
//检查pos位置的值是否符合要求
bool check(ll money){
int cnt = 0;
ll sum = 0;
for(int i =1;i<=n;i++) {
if( sum + a[i] <= money) {
sum += a[i];
}
else {
cnt++;
sum = a[i];
}
}
cnt++;
return cnt <= m;
}
//bs_find = binary search find
// 返回第一个满足条件的位置
// 如果所有的值都不满足条件,返回r,
// !!! r位置对应的是Guard,一个虚拟的位置,保证一定各满足check
// !!! 写题目的时候一定要注意r的值,通常是n+1
// 保障初始区间[l,r],r一定是满足的,通常r = n+1
ll bs_find(ll l,ll r) {
while( l < r) {
int m = mid(l,r);
if( check(m)) //成立
r = m;
else //不成立,抛弃左半边
l = m+1;
}
return l ;
}
//oisnip_end
void init(){
std::cin >> n >> m;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
std::cin >> a[i];
if( max_spend < a[i])
max_spend = a[i];
}
}
signed main () {
ios::sync_with_stdio(false); cin.tie(0);
init();
ll pos = bs_find(max_spend, 1e9);
std::cout << pos << "\n";
return 0;
}