dp问题——最大子段和及其扩展

1. 基础

(1)最大子段和

题目连接:最大子段和

解法

//状态表示及转移
用f[i] 表示 以a[i]结尾的连续子序列的集合。
根据最后一个元素的不同,可将集合如此划分:
1. 子序列长度为1   ————> f[i] = a[i].
2. 子序列长度大于1 ————> f[i] = f[i - 1] + a[i].
(因为f[i] 表示以a[i]结尾的连续子序列,当长度大于1时
 a[i - 1]必定存在。)
根据题目要求,求的是集合最大值
所以状态转移方程:
f[i] = max(f[i - 1] + a[i],a[i]) 
     = max(f[i - 1],0) + a[i];  
//边界处理
f[1] = ?
根据f[i]的定义,f[1]表示以a[1]结尾的连续子序列的集合,
此时子序列长度为1。所以f[1] = a[1]。
所以在状态转移时要确保f[1] = a[1]。
1.可以设定f[1] = a[1],然后i从2开始循环。
2.若i从1开始循环,根据状态转移方程
  f[1] = max(f[0],0) + a[i].
  要确保f[1] = a[1].
  所以f[0] <= 0,一般设f[0] = 0.
// 输出的答案
根据f[i] 的定义。
由于不确定最后答案(也就是最大值)所在的子序列是不是以
a[n]结尾,所以f[n]并不是最终答案。
最终答案需要遍历f[i],找到所有f[i]中的最大值。
// 优化空间
根据状态转移方程:
f[i] = max(f[i - 1],0) + a[i].
发现对于每一个f[i]只会用到它前一个状态f[i - 1],
所以只需要一个变量f保存前一个状态的值。
即f = max(f,0) + a[i];
执行max时,f值未被更新,所以此时f保存的是上一个循环
i - 1时的值,也就是f[i - 1].
// 代码
// 注释为优化
#include<iostream>

using namespace std;
typedef long long ll;
const int N = 50010;
ll f[N],a[N];
//ll a[N];

int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    
    ll ans = -0x3f3f3f3f;
    f[0] = 0;
    //ll f = 0;
    for(int i = 1; i <= n; i ++)
    {
        f[i] = max(f[i - 1],0ll) + a[i];
        ans = max(f[i],ans);
        //f = max(f,0ll) + a[i];
        //ans = max(f,ans);
    }
    cout << ans << endl;
}

2. 扩展

(1)循环数组最大子段和

题目链接:循环数组最大子段和

解法

// 区别
本题不同点以及难处理的点在于
a[i] + a[i + 1] + ... + a[n] + 
a[1] + a[2] + ... + a[j](#)
 这种情况.(j < i)

// 如何求这种类型的值呢
1.  sum = # + a[j] + a[j + 1] + ... + a[i](*)
	sum代表a[1] + a[2] + ... + a[n].
2. 求#式的最大值,这意味着只要求*式的最小值,然后用sum
	减去该值即可。
3. *式是什么?是j ~ i这一段区间的和,要求是值最小。
	就是求最小子段和

// 最小子段和的求法
	和最大子段和思想相同。
// 代码
#include<iostream>

using namespace std;

typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 500010;
ll a[N];

int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    
    ll maxm = -INF,minm = INF;
    ll fmax = 0,fmin = 0;// 最大子段和,最小子段和
    ll sum = 0; // 数组总和

    for(int i = 1; i <= n; i ++)
    {
        fmax = max(fmax,0ll) + a[i];
        fmin = min(fmin,0ll) + a[i];
        maxm = max(fmax,maxm);
        minm = min(fmin,minm);
        sum += a[i];
    }
    
    cout << max(maxm,sum - minm) << endl;
}

(2)二维最大子段和

题目链接:二维最大子段和

解法

// 二维变一维
如何将二维转换为一维呢?
1.一维数组是高度为1的矩阵。
2.二维数组是高度>1的矩阵。
只需在不改变答案的前提下压缩高度即可。
就是将每一列的值累加。(换而言之就是在列方向上
                      快速求出一段区间和的值)
如:
1 2 3
1 2 3   ————————> 3 6 9
1 2 3

3.前缀和可以在O(1)的时间内求出任意一段区间和。
  只需要对于每一列预处理出前缀和即可。
4.在对转化后的"一维数组"求最大子段和即可。
5.如何枚举任意子矩阵?
  只需确定子矩阵的高度和长度即可。
  高度如何确定?高度是行数的差值。(只需确定矩阵所在的
  第一行和最后一行即可,)
  如3 * 3 的矩阵。
  高度为2的子矩阵有:(1,2),(2,3)。
  [(x,y),x为矩阵所在的第一行,y为矩阵所在的最后一行]。
#include<iostream>

using namespace std;
typedef long long ll;
const int N = 510;
const int INF = 0x3f3f3f3f;
ll s[N][N];

int main()
{
    int n,m;
    cin >> m >> n;
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
        {
            int x;
            cin >> x;
            s[i][j] = s[i - 1][j] + x; 
            // 预处理每一列的前缀和
        }
    
    ll ans = -INF;
    for(int i = 1; i <= n; i ++)// 枚举矩阵所在的第一行
        for(int j = i; j <= n; j ++)// 枚举矩阵所在的最后一行
        {
            ll f = 0;
            for(int k = 1; k <= m; k ++)
            {
                f = max(f,0ll) + s[j][k] - s[i - 1][k];
                // s[j][k] - s[i - 1][k] 表示将第k列i ~ j这段压缩
                ans = max(f,ans);
            }
        }
    cout << max(ans,0ll) << endl; // 全为负数要输出0
}

(3)最大M子段和

题目链接:最大m子段和

解法

// 状态表示及转移
f[i][j] 表示 分成i段且最后一段必须包含a[j]的连续子序列
的集合。
根据a[j],可以如此划分集合:
1.a[j]不是单独成段。又因为最后一段必须包含a[j]且序列
连续,所以最后一段必包含a[j - 1]。
表示为f[i][j - 1] + a[j].
2.a[j]单独成段。所以剔除a[j]后,还剩i-1段,此时最后一段
也就是第i- 1段以谁结尾?是不确定的。
假设为a[k](i - 1<= k <= j - 1)。
表示为f[i - 1][k] + a[j].
3.所以f[i][j] = max(f[i][j - 1],f[i - 1][k]) + a[j].
// 边界处理
因为分成i段至少要有i个数,所以f[i][j]当i大于j时为不合法
所以f[i][i] 只能由第二种情况转移。
f[i][i] = f[i - 1][k] + a[i].k>= i - 1&&k <= i - 1
所以f[i][i] = f[i - 1][i - 1] + a[i]
//优化时间
发现转移时需要枚举k,大大增加了时间复杂度,对此优化。
因为不确定a[j - 1]是否被包含在第i-1段,所以需要枚举k
那能不能根据最后一个数是a[j - 1]来完成优化呢?
// 引入辅助数组 p[i][j].
p[i][j] 表示分为i段的连续子序列且最后一个数为a[j]的集合
注意:这里最后一段不一定需要包含a[j].
// p[i][j]的转移
1.最后一段必须包含a[j].此时p[i][j]的定义等同于f[i][j]
表示为f[i][j].
2.最后一段不包含a[j].表示为p[i][j - 1]
3. p[i][j] = max(p[i][j - 1],f[i][j]).
// 有了p[i][j]的定义后,发现f[i][k] == p[i][j - 1].
故f[i][j] = max(f[i][j - 1],p[i - 1][j - 1]) 
				+ a[j].
转移的时间复杂度由O(n)变为O(1)

优化时间空间:参考

//代码
#include<iostream>

using namespace std;

typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 5010;
ll f[N],p[N],a[N];

int main()
{
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    
    ll cur;
    for(int i = 1; i <= m; i ++)
    {
        f[i] = f[i - 1] + a[i];
        cur = f[i];
        for(int j = i + 1; j <= n - m + i; j ++)
        {
            f[j] = max(f[j - 1],p[j - 1]) + a[j];
            p[j - 1] = cur;
            if(f[j] > cur) cur = f[j];
        }
        p[n - m + i] = cur;
    }
    for(int i = 1; i <= m; i ++) cur = max(cur,f[i]);
    cout << cur << endl;
}

(4)最大子段和v2

题目链接:最大子段和v2

(5)最大m子段和v2

题目链接:最大子段和v2

(6)最大m子段和v3

题目链接:最大子段和v3

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您好!针对您的问题,我可以为您提供一个使用动态规划和二分法解决最大段和问题的算法。 首先,让我们定义一个函数`dp`来表示以`mid`为中点的最大段和。我们可以使用动态规划来计算`dp`的值。 1. 首先,我们定义一个数组`sum`,其中`sum[i]`表示以索引`i`结尾的段的和。 2. 然后,我们初始化`dp`数组,将所有元素初始化为负无穷,即`dp[i] = -∞`。 3. 接下来,我们从左到右遍历数组,计算每个`sum[i]`的值,并更新`dp[i]`的值: - 如果`i`等于0,则`dp[i]`等于`sum[i]`。 - 否则,`dp[i]`等于`max(dp[i-1] + sum[i], sum[i])`,即选择继续扩展前一个段或者从当前位置开始一个新的段。 4. 在计算`dp`数组的过程中,我们记录最大的`dp`值,并更新最大段和。 接下来,我们可以使用二分法来寻找最大段和的起始和结束位置。 1. 我们定义一个函数`binary_search`,它接受一个目标值`target`和一个排序好的数组`arr`。 2. 在`binary_search`函数中,我们使用二分法来找到第一个大于等于`target`的元素的索引。 - 初始化左指针`left`为0,右指针`right`为数组长度减一。 - 当`left`小于等于`right`时,执行以下步骤: - 计算中间指针`mid`,即`(left + right) // 2`。 - 如果`arr[mid]`小于`target`,则更新`left`为`mid + 1`。 - 否则,更新`right`为`mid - 1`。 - 返回`left`作为结果。 3. 使用`binary_search`函数找到最大段和的起始位置和结束位置。 这就是使用动态规划和二分法解决最大段和问题的算法。希望对您有帮助!如果还有其他问题,请随时提问。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值