17089 最大m子段和
时间限制:1000MS 代码长度限制:10KB
提交次数:0 通过次数:0
题型: 编程题 语言: G++;GCC;VC;JAVA
Description
“最大m子段和”问题:给定由n个整数(可能为负)组成的序列a1、a2、a3、…、an,以及一个正整数m,
要求确定序列的m个不相交子段,使这m个子段的总和最大!
m是子段的个数。当m个子段的每个子段和都是负值时,定义m子段和为0。
输入格式
第一行:n和m; (n,m<10000)
第二行:n个元素序列,中间都是空格相连。
比如:
6 3
2 3 -7 6 4 -5
输出格式
输出最大m子段和。
比如:
15
这15可由这三个段之和来的:(2 3) -7 (6) (4) -5
输入样例
6 3
2 3 -7 6 4 -5
输出样例
15
解题思路
1. dp 方程定义
定义二维数组 dp, dp[ i ][ j ],表示前 j 项所构成 i 子段的最大和,且必须包含着第 j 项,即以第 j 项结尾
2. 状态转移方程
求 dp[ i ][ j ],有两种情况
- dp[ i ][ j ] = dp[ i ] [ j-1 ] + a[ j ] ,即把第 j 项融合到第 j-1 项的子段中,子段数没变(即跟前面连成一段,一般发生于前面的数是正数时);
- dp[ i ][ j ] = dp[ i-1 ] [ k ] + a[ j ],(i-1<= k < j ),把第 j 项作为单独的一个子段,然后找一下 i - 1 个子段时,最大的和,然后加上a[ j ]
对于第二点,思考方式可以是如下图:当你遍历到6这个元素时,很明显不能选择前面的元素(因为是负的),那么就选择其他(尽量正数),于是目光自然就看到了前面那一份 2 + 3 = 5。
而 k 的取值范围又如何解释呢:
- 大于等于 i - 1 ,是因为 i - 1 个数字最多只能被分为 i - 1 份,即每份一个数字;
- 而小于 j 即最多可以选择到第 j - 1 个数字,是因为当你在抉择第 j 个数字如何划分范围时,前面只有 j - 1 个数字给你划分,范围怎么可能能跑到 j 自己及后面去。
然后比较上面两种情况,取大的即可。
下面看图,红色数字为输入的序列:
如图,要求dp[ 3 ][ 6 ],只需比较 他左边的那个,和上面那一行圈起来的之中 最大的数(i-1 <= t <= j),
再加上 a[ j ] 即为 dp[ 3 ][ 6 ] 的值。
3. 算法解题思路
- 初始化 dp 数组,前 0 个数字划分为任意份肯定都为0:dp[i][0] = 0; 任意个数字划分为0份肯定也为0;n 个数字肯定无法划分为 n - 1 份,最多 n 份即每份一个数字:dp[i][i - 1] = -INF(赋值为最小数即可)。
- 外面双层循环,第一层 i 是记录划分到第几份,第二层 j 是记录到是把前 j 个数字划分为 i 份。
- 在每次划分时,都要去上一份那里去寻找最大值,看看上一份中使用哪几个数字能划分成最大值 maxNum,再跟当前份数的前一个数字来比较:dp[i][j - 1],看看第 j 个数到底是继续划分到前一个数字后面,还是说单独开一份。
- 那么 dp[i][j] 就等于第三个步骤中大的那个,再加上当前数字的大小,即为最大和。
- 最后,也就是最关键的地方,输出的找法,不是单纯的 dp[m][n],因为最大m字段和不一定包括全部数,可以舍弃掉一些数比如负数,见如下:
在划分为 m 份时进行寻找,看看使用前几个数字可以找到最大字段和,比如说 5 6 7 -1 2 -8 -9,最大字段和肯定不可能把最后两位划分进去,所以代码应为如下
// 找划分成 m 份时,究竟前几个数字才能凑成最大字段和
int res = -INF;
// 由于是划分为 m 份,所以数字至少得 m 个才能,总不可能2个数字划分成3份吧
for(i = m; i <= n; i++) {
res = max(res, dp[m][i]);
}
4. 可优化点
- 沿着第 m 行的最后一个元素,往左上方向画一条线,线右上方的元素是没必要计算的
那么 dp[ i ][ j ] ,j++ 的时候,j 的上限为 i + n - m 即可。
还有左下角那一半矩阵,也是不用计算的,因为1个数字不可能分为2个子段 - 每确定一个 dp[ i ][ j ],只需用到 本行和上一行,所以不用开维数组也可以,省内存。
开两个一维数组,pre 和 dp,pre 记录上一行,dp 记录当前行 - 再对上一行红圈中的数字找最大值时,若用一个循环来找,容易超时。所以优化方法是:在每次计算 dp 之前,同时记录下j前面的最大元素。
时间复杂度大致为O(m*(n-m+1)),mn-m方
通过图片,分析情况1和2,就能发现,从左上角走到第 m 行的最后一个元素即可,找出第 m 行的最大值即为答案。
更多注释可查看下方的完整代码中,有助于理解
代码如下
#include <iostream>
#include <cmath>
#include <string.h>
#include <algorithm>
/*
6 3
2 3 -7 6 4 -5
7 7
-2 11 -4 13 -5 6 -2
*/
using namespace std;
const int INF=0x3f3f3f3f;
int a[10001];
int dp[10001][10001]; // dp[i][j],表示前 j 项所构成 i 子段的最大和,且必须包含着第j项,即以第j项结尾
int main()
{
// 此题为求最大上升子序列的变种,是求多个
int i, j, k, n, m;
cin >> n >> m;
for(i = 1; i <= n; i++) {
cin >> a[i];
}
for(i = 1; i <= n; i++) {
dp[i][0] = 0;
dp[0][i] = 0;
dp[i][i - 1] = -INF; // 每一行开头,即 n 个数字想分成 n - 1 份是不可能,赋值为最小数即可
}
// 分成 i 段时
for(i = 1; i <= m; i++) {
// 前 j 个数
for(j = i; j <= n; j++) {
int maxNum = -INF;
// 去上一行找最大值,索引结束于 j - 1 之前
for(k = i - 1; k < j; k++) {
if(dp[i - 1][k] > maxNum) {
maxNum = dp[i - 1][k];
}
}
dp[i][j] = max(dp[i][j - 1], maxNum) + a[j];
}
}
// 找划分成 m 份时,究竟几个数字才是最好的
int res = -INF;
// 由于是划分为 m 份,所以数字至少得 m 个才能,总不可能2个数字划分成3份吧
for(i = m; i <= n; i++) {
res = max(res, dp[m][i]);
}
cout << res << endl;
return 0;
}
优化后代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+5;
const int INF=0x3f3f3f3f;
int n,m;
ll a[N],dp[2][N]; //只保存上一行和当前行
int main()
{
while(~scanf("%d%d",&n,&m)) //n个数字,m子段和
{
for(int i=1;i<=n;i++)
scanf("%lld",a+i);
for(int i=0;i<=n;i++)
dp[0][i]=0,dp[1][i]=0; //关键!此题答案只允许正值
for(int i=1,k=1;i<=m;i++,k^=1) //分为i段,k为两行之间的切换
{
dp[k][i-1]=-INF; //i==j时,杜绝与前一元素共伍
ll maxpre=-INF; //maxpre记录上一行的最大值
for(int j=i;j<=n-m+i;j++)
{
maxpre=max(maxpre,dp[k^1][j-1]); //随时更新上一行最大值
dp[k][j]=max(dp[k][j-1],maxpre)+a[j]; //*对情况1、2的选择
}
}
ll ans=-INF;
for(int i=m;i<=n;i++) //找到第m行的最大值,即为答案
ans=max(ans,dp[m&1][i]);
printf("%lld\n",ans);
}
}
最后
对我感兴趣的小伙伴可查看以下链接
- 我的掘金主页:https://juejin.cn/user/1302297507801358
- 博客主页:http://blog.zhangjiancong.top/
- 公众号:Smooth前端成长记录