最大M子段和

题目模型

  • N个整数组成的序列 \(a_1,a_2,a_3,…,a_n\) ,将这N个数划分为互不相交的M个子段,并且这M个子段的和是最大的。

问题分析

  • 方法一:

    • 看到序列,我们首先要尝试用线性dp去处理,线性dp经典状态定义:f[i][j],i一般表示序列的前i个元素,j表示限制,这里表示划分了j个不相交的子段,我们还需要对i进行进一步的定义,即是否包含第i项,因为对当前元素a[i]来说,要么单独成一个子段,要么和最后一个子段合并,所以必须包含第i个元素。

    • 动态转移方程:dp[i][j]=max(dp[i-1][j],dp[k][j-1])+a[i] (j-1<=k<i)。

    • Code

      #include <bits/stdc++.h>
      const int maxn = 1e3+3,Inf=0x3f3f3f3f;
      typedef long long LL;
      int a[maxn],dp[maxn][maxn];
      void Solve(){
      int n,m;scanf("%d%d",&n,&m);
      for(int i=1;i<=n;++i)
      scanf("%d",&a[i]);
      for(int i=1;i<=n;++i){//前i个元素
      for(int j=1;j<=std::min(i,m);++j){//划分出j个子段
      if(i==j)dp[i][j]=dp[i-1][j-1]+a[i];//显然
      else{
      int temp=dp[i-1][j];//把a[i]直接并到最后一子段
      for(int k=j-1;k<i;++k)//枚举上一个状态的最后一个子段的右端点,a[i]单独作为一个子段
      temp=std::max(temp,dp[k][j-1]);
      dp[i][j]=temp+a[i];
      }
      }
      }
      int ans=-Inf;
      for(int i=m;i<=n;++i)
      ans=std::max(ans,dp[i][m]);
      printf("%d\n",ans);
      }
      int main(){
      Solve();
      return 0;
      }
    • 时间效率为:\(O(n^3)\) ,空间效率为:\(O(m*n)\)。

  • 方法二:

    • 我们尝试对方法一的dp阶段和状态进行修改, 即把子段限制数M作为阶段,即状态dp[i][j]表示把序列前j分成i个子段且包含a[j]的最大子段和。

    • 动态转移方程有:dp[i][j]=max(dp[i][j-1],dp[i-1][k])+a[j] (i-1<=k<j)。

      • dp[i][j-1]+a[i]:表示合并到最后一个子段里

      • dp[i-1][k]+a[i]:表示前k元素挑出k个子段,所以k>=j-1,然后a[i]单独的子段。

      • 此动态转移方程同样满足无后效性和最优子结构。

      • 我们把问题的所有状态记录下来形成一个二维矩阵,显然当前状态只跟它上一行和左边的状态有关,我们可以把空间效率压掉以为变成 \(O(n)\) 。

      • 同时上一行的状态只有在当前状态前面的最大值对转移有用,我们可以在遍历当前行时维护一下上一行的最大值,这样时间效率就压掉了一个n,变成\(O(n*m)\)。

      • Code

        #include <bits/stdc++.h>
        typedef long long LL;
        const int maxn = 1e4+5;
        const LL Inf=0x3f3f3f3f3f3f3f3f;
        LL a[maxn],dp[2][maxn];
        void Solve(){
        int n,m;scanf("%d%d",&n,&m);
        for(int i=1;i<=n;++i)
        scanf("%lld",&a[i]);
        int k=1;//滚动数组指针,k表示当前行,!k表示上一行
        for(int i=1;i<=m;++i,k=!k){//枚举区间个数
        LL Max=-Inf;
        for(int j=i;j<=n;j++){
        Max=std::max(Max,dp[!k][j-1]);//记录前j-1,分成i-1个区间时最大值
        if(i==j)
        dp[k][j]=dp[!k][j-1]+a[j];
        else//要么是a[j]单独成一个区间,此时为Max+a[j],或者直接合并为dp[k][j-1]+a[j]
        dp[k][j]=std::max(Max,dp[k][j-1])+a[j];
        }
        } LL ans=-Inf;
        for(int i=m;i<=n;++i)//!k行才记录的是第m行的状态
        ans=std::max(ans,dp[!k][i]);
        printf("%lld\n",ans);
        }
        int main(){
        Solve();
        return 0;
        }
  • 方法三

    • 方法二把空间优化到线性,时间优化到\(O(n^2)\) ,但如果 \(n\) 和 \(m\) 高达 \(10^5\)显然dp是无法解决了。比如:51nod 1115 最大M子段和 V3

    • 对这个问题我们先对原数组进行处理,然后利用可撤销贪心解决。

      • 首先把原数组连续的正数加起来变成一个数,连续的负数加起来变成一个数,0加到哪里都一样,这样我们就得到了一个正负交替的环形序列。

      • 如果新的环形序列里正数的个数为cnt,所有正数之和为ans则存在两种情况:

        1. \(cnt<=M\),因为可以选空,所以答案就是所有正数之和ans。
        2. \(cnt>M\) ,此时ans包含了cnt个正数区间,所以我们需要通过操作减少cnt-M个区间。
      • 对于情况2,我们可以通过两种操作减少区间:

        1. 删除一个正数(ans包含所有正数之和,删除一个正数相当于减少了一个区间).
        2. 将一个负数与它两边的正数合并,相当于把两个正数区间合并成了一个区间,也减少了一个区间。
      • 很容易能想到一个贪心思想,即每次选择最小的正数删除一个区间,或选择最大一个负数与它两边的正数合并减少一个区间。

      • 但这个贪心是不正确的,因为删除一个较小的正数是在减少一个区间的最优,但有可能不删除这个正数,而是通过若干次合并在减少多个区间。

        • 例如:序列10 -4 3 -4 8 -100其中M=1,根据刚才的贪心策略,我们先删除3,减少了一个区间,然后最大的负数为-4,此时因为3已经被删除-4就无法合并了。为了解决类似的问题,我们要用到可撤销的贪心思想。
      • 实现步骤:

        1. 把序列中正数均变成负数,并把数都压入大根堆。
        2. 套用可撤销贪心思想,做cnt-M次即可,具体见代码。
      • Code

        #include <bits/stdc++.h>
        typedef long long LL;
        const int maxn=2e5+5;
        struct Node{
        int id;
        LL w;
        Node(){};
        Node(int x,LL y){id=x;w=y;}
        bool operator <(const Node &a)const{
        return w<a.w;
        }
        };
        int n,m,N=0,L[maxn],R[maxn];
        LL ans=0,a[maxn],b[maxn];
        std::priority_queue<Node> q;
        bool flag[maxn];
        void Init(){
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;++i){
        scanf("%lld",&a[i]);//原始数组
        if(!N || (a[i]>=0)!=(b[N]>=0))
        b[++N]=a[i];//新数组,上个数和当前数符号不一样节点++
        else
        b[N]+=a[i];//符号一样,累加
        }
        if((b[1]>=0)==(b[N]>=0))//因为是环,b[1]和b[N]同号就合并
        b[1]+=b[N--];//合并后新数组个数减一,记得N--
        }
        void Solve(){
        Init();
        int tot=0;//记录正数个数
        for(int i=1;i<=N;++i){//遍历新数组,压入大根堆
        L[i]=i-1; R[i]=i+1;//初始化i的左右邻居
        if(b[i]>0){//正数累加到答案,然后变负
        ans+=b[i]; b[i]=-b[i]; tot++;
        }
        q.push(Node(i,b[i]));
        }
        R[N]=1; L[1]=N;//注意是环形
        if(m>=tot){//正数个数小于m,则全部选
        printf("%lld\n",ans);return;
        }
        m=tot-m;//正数大于m则合并或删除tot-m个区间
        while(m--){
        Node t=q.top(); q.pop();
        int i=t.id;
        if(flag[i]){++m; continue;}
        else{//可撤销贪心
        ans+=b[i];
        flag[L[i]]=1;
        flag[R[i]]=1;
        b[i]=b[L[i]]+b[R[i]]-b[i];
        R[L[L[i]]]=i;
        L[R[R[i]]]=i;
        q.push(Node(i,b[i]));//压入新点
        L[i]=L[L[i]];
        R[i]=R[R[i]];
        }
        }
        printf("%lld\n",std::max(0LL,ans));
        }
        int main(){
        Solve();
        return 0;
        }

最大子段和之M子段和的更多相关文章

  1. (最大连续和/最大子段和) P1115 最大子段和 洛谷

    题目描述 给出一段序列,选出其中连续且非空的一段使得这段和最大. 输入输出格式 输入格式: 第一行是一个正整数NN,表示了序列的长度. 第二行包含NN个绝对值不大于1000010000的整数A_iAi ...

  2. XCOJ 1103 (LCA+树链最大子段和)

    题目链接: http://xcacm.hfut.edu.cn/problem.php?id=1103 题目大意:链更新.链查询,求树链的最大子段和.(子段可以为空) 解题思路: 将所有Query离线存 ...

  3. hdu1003 dp(最大子段和)

    题意:给出一列数,求其中的最大子段和以及该子段的开头和结尾位置. 因为刚学过DP没几天,所以还会这题,我开了一个 dp[100002][2],其中 dp[i][0] 记录以 i 为结尾的最大子段的和, ...

  4. HDOJ-1003 Max Sum(最大连续子段 动态规划)

    http://acm.hdu.edu.cn/showproblem.php?pid=1003 给出一个包含n个数字的序列{a1,a2,..,ai,..,an},-1000<=ai<=100 ...

  5. 最大子段和(c++)

    // 最大子段和.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> using namesp ...

  6. 51Node 1065----最小正子段和

    51Node  1065----最小正子段和 N个整数组成的序列a[1],a[2],a[3],…,a[n],从中选出一个子序列(a[i],a[i+1],…a[j]),使这个子序列的和>0,并且这 ...

  7. 最大M子段和 V2

    51nod1053 这题还是我们熟悉的M子段和,只不过N,M<=50000. 这题似乎是一个堆+链表的题目啊 开始考虑把所有正数负数锁在一起. 比如: 1 2 3 -1 –2 -3 666 缩成 ...

  8. 51nod 循环数组最大子段和

    http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1050 对于普通的数组,只要求一次最大子段和即可.但是这题是可以循环的,所 ...

  9. [日常训练]最大M子段和

    Description 在长度为的序列中选出段互不相交的子段,求最大字段和. Input 第一行两个整数. 第二行个整数. Output 一行一个整数表示最大值. Sample Input 5 2 1 ...

随机推荐

  1. 【盗墓笔记】图解使用fat-aar方式在AndroidStudio中打包嵌套第三方aar的aar

    将一些项目中的一些独立功能打包成aar,不仅能于项目解耦,还能够提供给其它项目使用相同的功能,可谓是为项目开发带来了很大的便利.最近第一次做sdk,碰到一些问题,花了不少时间才解决,所以这里做一下简单 ...

  2. Linux:apache安装

    1.查询是否已安装 rpm -qa httpd 如果已安装,先卸载 发现有依赖包,先把依赖卸载 或者加上--nodeps参数,不考虑依赖,直接卸载   rpm -e --nodeps httpd-2. ...

  3. Java中CAS 基本实现原理

    一.前言 了解CAS,首先要清楚JUC,那么什么是JUC呢?JUC就是java.util.concurrent包的简称.它有核心就是CAS与AQS.CAS是java.util.concurrent.a ...

  4. 最全 IT 相关网站,软件开发网站收集

    前端比较流行的 UI 框架 目前前端用的比较多的框架有如下几个 Ant Design:Ant Design - 一套企业级 UI 设计语言和 React 组件库  Mint UI:https://mi ...

  5. A Funny Game(POJ 2484)

    原题如下: A Funny Game Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 7108   Accepted: 446 ...

  6. oracle之三rman 不完全恢复

    rman 不完全恢复 9.1 rman 不完全恢复的三个标准模式:基于time.基于scn和基于sequence: 范例1:恢复过去某个时间点误操作,一般使用基于time或scn. 1)环境:有一套全 ...

  7. [计算机网络]图解HTTP阅读笔记

    总述 书的定位:一本十分浅显的HTTP书籍,主要介绍了HTTP与HTTPS.适合入门了解,很多地方都是蜻蜓点水,但稍微深入的地方能让人了解重点在哪,后面应该有针对性地阅读深入书籍. 主要内容:介绍了T ...

  8. 离线安装Superset 0.37

    上文提到了Superset 0.37的在线安装方式,只需要更新pip,然后pip install就可以了.但是在生产环境中,特别是内网环境中,很多时候是没有外网的,这时候就需要采取离线安装的方式. 本 ...

  9. PE安装window 10操作系统

    一.进入bios,通过u盘启动 1. 在bios中选择U盘启动 2. 选择第二选项,如果是老机器,就选择03或者04 二.进入PE操作系统后: 打开桌面上的Ghost手动工具,点击OK 依次点击loc ...

  10. 如何在 PyPI安装python的软件包?

    安装软件包 本节介绍如何安装Python的基本知识.包裹. 需要注意的是,这个上下文中的“包”一词被用作分布(即要安装的一组软件),而不是指包装在Python源代码中导入(即模块的容器).Python ...