P1182 数列分段 Section II

- 答案范围:左边界 l=max(Ai)(每段至少包含一个数,最大值不可能小于数组最大元素),右边界 r=∑Ai(不分段时的总和)。
- 核心逻辑 :对于一个候选的最大值 mid,验证是否能将数组分成不超过 M 段 ,且每段和 ≤mid。
- 若能分:说明 mid 可行,尝试更小的值(r=mid)
- 若不能分:说明 mid 太小,尝试更大的值(l=mid+1)
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010;
ll a[N];
ll n, m;
bool check(ll x){
ll cnt = 1;//刚开始 1 段
ll num = 0;
for(int i =0; i<n; i++){
if(num + a[i] > x){
cnt ++;
num = a[i];
if(cnt > m) return false;
}
else {
num +=a[i];
}
}
return true;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin >> n >> m;
ll l = 0, r = 0;
for(int i = 0; i<n; i++){
cin >> a[i];
l = max(l , a[i]);
r += a[i];
}
while(l < r){
ll mid = l+r >> 1;
if(check(mid)) r = mid;//如果这个最大值能分成的段数<=mid;可以更小
else l = mid + 1;
}
cout << r;
return 0;
}
一、DP 状态定义
dp[i][j]:将前 i 个数字,分成 j 段时,每段和的最大值的最小值。
二、状态转移方程
要计算
dp[i][j],我们枚举最后一段的起点k(k < i):
- 最后一段是
[k+1, i],和为sum[i] - sum[k](sum是前缀和数组)- 前
k个数字分成j-1段的最优解是dp[k][j-1]- 那么以
k为分界点时,当前方案的最大值是max(dp[k][j-1], sum[i] - sum[k])- 我们要在所有
k中取最小值,因此转移方程:dp[i][j]=min0≤k<i{max(dp[k][j−1], sum[i]−sum[k])}
三、初始化
dp[i][1] = sum[i]:前 i 个数分成 1 段,最大值就是总和- 其余状态初始化为极大值(如
0x3f3f3f3f)
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1010; // 注意:DP版时间复杂度O(n2m),仅适合n≤1000的小规模
const ll INF = 0x3f3f3f3f3f3f3f3f;
ll a[N], sum[N];
ll dp[N][N]; // dp[i][j]:前i个数分j段的最小最大值
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
sum[i] = sum[i-1] + a[i]; // 前缀和
}
// 初始化
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i++) {
dp[i][1] = sum[i]; // 分1段,最大值就是总和
}
// 状态转移
for (int j = 2; j <= m; j++) { // 枚举段数
for (int i = j; i <= n; i++) { // 枚举前i个数(至少j个数才能分j段)
for (int k = j-1; k < i; k++) { // 枚举最后一段的起点k,前j-1段最少要j-1个数
ll last = sum[i] - sum[k]; // 最后一段的和
dp[i][j] = min(dp[i][j], max(dp[k][j-1], last));
}
}
}
cout << dp[n][m] << endl;
return 0;
}