• 算法与数据结构---4.6、最大子段和-贪心优化


    算法与数据结构---4.6、最大子段和-贪心优化

    一、总结

    一句话总结:

    1、时间方面优化:循环可以合并(循环方向一致,循环最大值也是,并且两个循环之间没有什么逻辑操作代码)
    2、空间方面优化:代码中只用到了a[i],所以a[]数组可以用一个变量来代替
    #include <iostream>
    #include <algorithm>
    using namespace std;
    int main(){
        int n;
        cin>>n;
        int sum;
        cin>>sum;
        int maxx=sum;
        for(int i=2;i<=n;i++){
            int x;
            cin>>x;
            if(sum<=0) sum=0;
            sum+=x;
            maxx=max(sum,maxx);
        }
        cout<<maxx<<endl;
        return 0;
    }

    二、最大子段和

    博客对应课程的视频位置:4.6、最大子段和-贪心优化
    https://www.fanrenyi.com/video/27/268

    1、题目描述

    最大子段和(最大连续子序列的和)

    题目描述
    给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。

    输入格式
    第一行是一个整数,表示序列的长度 n。
    第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字 ai

    输出格式
    输出一行一个整数表示答案。

    输入输出样例
    输入
    7
    2 -4 3 -1 2 -4 3
    输出
    4

    说明/提示
    样例解释
    选取 [3,5] 子段{3,−1,2}最大,其和为 4。

    数据规模与约定
    对于40%的数据,保证n<=2×10^3
    对于100%的数据,保证1<=n<=2×10^5, -10^4<=a[i]<=10^4

    题目提交位置:
    P1115 最大子段和 - 洛谷
    https://www.luogu.com.cn/problem/P1115

    2、枚举解法

     1 /*
     2 枚举法
     3 
     4 分析:
     5 我们可以直接按照题目的要求来枚举就好了
     6 
     7 题目的要求是要 求a[1]-a[n]中连续非空的一段的和最大
     8 那么我们把每个连续的一段都枚举出来,然后来算出里面的和,找出最大值即可
     9 
    10 所以在这个需求下:
    11 我们需要枚举每一段的起点、每一段的终点
    12 然后对这一段进行求和
    13 
    14 枚举变量:每一段的起点、终点
    15 枚举范围:起点:1-n,终点:起点-n
    16 枚举判断条件:
    17 求和得到每一段的和,在这些和里面选出最大的
    18 
    19 时间复杂度:
    20 O(n^3)
    21 
    22 算法思路:
    23 1、枚举每一段的起点和终点
    24 2、对每一段进行求和,在这些和里面选出最大的
    25 
    26 */
    27 #include <iostream>
    28 using namespace std;
    29 int a[200005];
    30 int main(){
    31     int n;
    32     cin>>n;
    33     int maxx=-0x7fffffff;
    34     for(int i=1;i<=n;i++){
    35         cin>>a[i];
    36     }
    37     //1、枚举每一段的起点和终点
    38     for(int i=1;i<=n;i++){
    39         for(int j=i;j<=n;j++){
    40             //2、对每一段进行求和,在这些和里面选出最大的
    41             int sum=0;
    42             for(int k=i;k<=j;k++){
    43                 sum+=a[k];
    44             }
    45             if(sum>maxx) maxx=sum;
    46         }
    47     }
    48     cout<<maxx<<endl;
    49     return 0;
    50 }

    3、枚举优化

     1 /*
     2 枚举优化
     3 
     4 可以把求和的那层循环去掉,我们可以对数据做预处理
     5 用s[i]表示第一个数到第i个数这个序列的和
     6 
     7 那么求s[i-j](第i个数到第j个数这个序列的和)的时候,
     8 可以直接用s[j]-s[i]+a[i]即可
     9 s[j]-s[i]表示的是i+1到j这个序列的和,所以需要加上a[i]
    10 
    11 现在的时间复杂度:
    12 O(n)+O(n^2)=O(n^2)
    13 
    14 优化方法:
    15 减少重复计算
    16 
    17 
    18 */
    19 #include <iostream>
    20 using namespace std;
    21 int a[200005];
    22 int s[200005]={0};
    23 int main(){
    24     int n;
    25     cin>>n;
    26     int maxx=-0x7fffffff;
    27     for(int i=1;i<=n;i++){
    28         cin>>a[i];
    29         s[i]=s[i-1]+a[i];
    30     }
    31     //1、枚举每一段的起点和终点
    32     for(int i=1;i<=n;i++){
    33         for(int j=i;j<=n;j++){
    34             //2、对每一段进行求和,在这些和里面选出最大的
    35             int sum=s[j]-s[i]+a[i];
    36             if(sum>maxx) maxx=sum;
    37         }
    38     }
    39     cout<<maxx<<endl;
    40     return 0;
    41 }

    4、分治解法

      1 /*
      2 
      3 样例
      4 7
      5 2 -4 3 -1 2 -4 3
      6 
      7 分治解法
      8 假定a[1]-a[n]的序列对应的区间[l...r],其中间位置为mid,其最大和的子序列为[i...j]。
      9 那么显然,最大连续子序列的位置只有三种可能:
     10 ①完全处于序列的左半:l<=i<=j<=mid
     11 ②跨越序列中间:i<=mid<=j<=r
     12 ③完全处于序列的右半:mid<i<=j<=r
     13 
     14 
     15 只需要分别求出三种情况下的值,取他们最大的即可。
     16 其中,很容易求出第二种情况,第二种情况也就是包含mid的子序列,
     17 也就是[i...mid...j],而求[i...mid...j]的最大值,
     18 即求出区间[i..mid]的最大值maxx1与区间[mid..j]的最大值maxx2,将其合并即可。
     19 合并之后就变成了[i...mid mid...j],mid出现了两次,要减掉一次
     20 所以[i...mid...j]的最大值就是maxx1+maxx2-mid
     21 
     22 复杂度O(n)
     23 如何处理第一种和第三种情况呢?
     24 也不难发现,
     25 第一种情况,其实就是求区间[l..mid]中的最大值,
     26 第三种情况就是求区间[mid+1..r]中的最大值。那么,只需递归求出即可。
     27 显然,该解法的复杂度为O(nlogn)通过此题是没问题的。
     28 
     29 
     30 算法时间复杂度
     31 O(nlogn):二分是logn,处理第二种情况是n,所以合起来就是O(nlogn)
     32 
     33 
     34 如何求区间[i..mid]的最大值与区间[mid..j]的最大值,
     35 换句话说,也就是如何求以mid为尾的子序列的最大值 和 以mid为头的子序列的最大值
     36 先说以mid为头的子序列的最大和
     37 也就是[mid],[mid...mid+1],[mid...mid+2]......[mid...mid+j]这些序列里面的最大值
     38 int maxx2=-0x7fffffff;
     39 int sum2=0;
     40 for(int k=mid;k<=j;k++){
     41     sum2+=a[k];
     42     maxx2=max(sum2,maxx2);
     43 }
     44 
     45 求以mid为尾的子序列的最大和
     46 int maxx1=-0x7fffffff;
     47 int sum1=0;
     48 for(int k=mid;k>=i;k--){
     49     sum1+=a[k];
     50     maxx1=max(sum1,maxx1);
     51 }
     52 
     53 maxx1+maxx2-a[mid]
     54 
     55 
     56 递归做分治:
     57 a、递归的终止条件:
     58 因为我们的递归是为了求l到r序列的子序列的最大值,
     59 所以当区间只有一个元素时,就是终止条件,那个元素就是子序列的最大值
     60 b、递归的递推表达式:比较方式1、2、3的最大值。第2种跨越mid值的需要我们去计算,1,3种情况又转化成了子问题
     61 c、递归的返回值:子序列的最大和
     62 
     63 
     64 算法步骤:
     65 1、计算第二种跨越mid情况的序列的最大和
     66 2、比较方式1、2、3的最大值
     67 
     68 
     69 
     70 样例:
     71 4
     72 -1 3 -1 -2
     73 结果是3 
     74 
     75 mid=(1+4)/2 2
     76 ①完全处于序列的左半:l...mid:-1 3  对应的是3
     77 ②跨越序列中间:3+3-3=3
     78 ③完全处于序列的右半:mid+1...r:-1 -2 对应的结果是-1
     79 
     80 -1 3
     81 mid=1
     82 ①完全处于序列的左半:l...mid:-1
     83 ②跨越序列中间:-1+2-(-1)=2
     84 ③完全处于序列的右半:mid+1...r:3
     85 
     86 */
     87 #include <iostream>
     88 #include <algorithm>
     89 using namespace std;
     90 int a[200005];
     91 //分治(二分)求最大连续子序列的和
     92 int find(int l,int r){
     93     if(l==r) return a[l];
     94     int mid=(l+r)/2;
     95     //1、计算第二种跨越mid情况的序列的最大和
     96     //a、求以mid为尾的子序列的最大和
     97     int maxx1=-0x7fffffff;
     98     int sum1=0;
     99     for(int k=mid;k>=l;k--){
    100         sum1+=a[k];
    101         maxx1=max(sum1,maxx1);
    102     }
    103 
    104     //b、求以mid为头的子序列的最大和
    105     int maxx2=-0x7fffffff;
    106     int sum2=0;
    107     for(int k=mid;k<=r;k++){
    108         sum2+=a[k];
    109         maxx2=max(sum2,maxx2);
    110     }
    111 
    112     //2、比较方式1、2、3的最大值
    113     return max(max(find(l,mid),find(mid+1,r)),maxx1+maxx2-a[mid]);
    114 }
    115 
    116 int main(){
    117     int n;
    118     cin>>n;
    119     for(int i=1;i<=n;i++){
    120         cin>>a[i];
    121     }
    122     cout<<find(1,n)<<endl;
    123     return 0;
    124 }

    5、没用好分治创造的信息的分治法

    下面代码是没用用好分治创造的信息的分治法代码,只能过两个点
    而用好分治法创造的信息的分治法代码,可以过所有点

      1 /*
      2 
      3 本题分治优化原理
      4 
      5 比如贪心能够优化,是因为贪心着眼于局部的最优策略,
      6 只枚举了极少的局部的情况,所以贪心法有时候不一定对,但是一般效率都还可以
      7 动态规划能够优化,是因为找准了状态之间的转移关系,并且存储了中间的状态,
      8 减少了大量重复求状态的计算,所以动态规划一般效率非常高
      9 
     10 
     11 为什么这道题目(最大连续子序列和)使用分治能够进行优化,
     12 其实分治本身只是一种策略,告诉我们要如何去枚举,分治本身并不减少枚举的次数
     13 所以分治能够得到正确的解,肯定也是枚举了所有的情况,
     14 那为什么分治就过了所有的点,
     15 也就是对比枚举优化的O(n^2)的算法(也是枚举了所有的情况),
     16 分治为什么能变成O(nlogn),
     17 O(nlogn)相比于O(n^2)肯定是少枚举了很多情况
     18 而我们的分治算法又是对的,
     19 那说明分治算法少枚举的情况都是一些无关紧要的情况,
     20 现在的问题就是,
     21 分治到底少枚举了哪些情况
     22 也就是为什么分治可以优化
     23 
     24 可以从以下两个点来分析
     25 (1)、分治将问题规模变小,将问题的规模变小之后,有些时候需要枚举的情况也会变少,
     26 a、假设分治内部用到的算法是O(n^2),假设n是10,原先的10^2=100>二分后的2*5^2=50
     27 b、假设分治内部用到的算法是O(n),假设n是10,原先的10>=二分后的2*5
     28 a里面看似减少了枚举情况,其实并没有,因为减少的情况跑到②跨越序列中间:i<=mid<=j<=r
     29 所以分治将问题的规模变小并没有减少枚举的情况
     30 
     31 (2)、情况②跨越序列中间的算法是O(n)的算法-->(分治优化的关键)
     32 第二种情况:子序列一定包含mid
     33 (转换成)===>
     34 即求出区间[i..mid]的最大值与区间[mid..j]的最大值,将其合并即可
     35 
     36 那么我们枚举包含mid的子序列的算法是O(n^2)
     37 枚举变量:起点和终点
     38 枚举范围:起点:i...mid,终点:mid...j
     39 
     40 结论:
     41 分治本身不能优化算法,
     42 因为分治还是需要将所有可能的情况枚举出来,选最优解,
     43 而分治真正能够优化算法的是:分治里面应用到的策略
     44 
     45 我们在分治的过程中创造了信息
     46 我们在用分治算法的时候,就创造了下面这些信息
     47 子序列的情况只能是这三种情况里面的一种
     48 ①完全处于序列的左半:l<=i<=j<=mid
     49 ②跨越序列中间:i<=mid<=j<=r
     50 ③完全处于序列的右半:mid<i<=j<=r
     51 
     52 (所以这题就分成三种情况,情况1和3都是递归子问题,
     53 而情况2,利用分治的信息(包含mid),成功的将枚举O(n^2)的算法优化到了O(n),
     54 自然就减少了一些不必要的枚举的情况)
     55 
     56 强调:
     57 分治能够优化,不在与分治这种策略,而是这种分治策略创造了信息,
     58 让我们可以拿这个信息去优化枚举
     59 
     60 我们之前反复强调,优化枚举法,需要就是信息、关系式、等式,
     61 而分治法优化的实质就是分治的过程中给我们创造信息,创造了关系式,
     62 从而减少枚举情况
     63 
     64 
     65 枚举法能够优化的实质是什么
     66 枚举法能够优化的实质是有信息(关系式、等式、条件)可以让我们减少枚举情况(减少枚举范围、减少枚举变量、减少不必要的枚举)
     67 比如在这个题里面,没有信息,我们通过分治就创造了这些信息,从而通过分治法优化了枚举
     68 
     69 
     70 分治能够优化枚举的实质是什么
     71 a、分治其实只是一种枚举策略,只能改变枚举的方式,并不能减少枚举的次数
     72 b、分治能够优化枚举,不在于分治这种策略,而是这种分治策略创造了信息(比如本题第二种情况子序列一定包含mid),让我们可以拿这个信息去优化枚举
     73 
     74 
     75 */
     76 
     77 //下面代码是没用好分治创造的信息的分治法代码,只能过两个点
     78 //而用好分治法创造的信息的分治法代码,可以过所有点
     79 #include <iostream>
     80 #include <algorithm>
     81 using namespace std;
     82 int a[200005];
     83 int s[200005]={0};
     84 //分治(二分)求最大连续子序列的和
     85 int find(int l,int r){
     86     if(l==r) return a[l];
     87     int mid=(l+r)/2;
     88     //1、计算第二种跨越mid情况的序列的最大和
     89     int maxx=-0x7fffffff;
     90     for(int i=l;i<=mid;i++){
     91         for(int j=mid;j<=r;j++){
     92             //2、对每一段进行求和,在这些和里面选出最大的
     93             int sum=s[j]-s[i]+a[i];
     94             if(sum>maxx) maxx=sum;
     95         }
     96     }
     97 
     98     //2、比较方式1、2、3的最大值
     99     return max(max(find(l,mid),find(mid+1,r)),maxx);
    100 }
    101 
    102 int main(){
    103     int n;
    104     cin>>n;
    105     for(int i=1;i<=n;i++){
    106         cin>>a[i];
    107         s[i]=s[i-1]+a[i];
    108     }
    109     cout<<find(1,n)<<endl;
    110     return 0;
    111 }

    6、贪心解法

     1 /*
     2 
     3 贪心是每次选择的是局部最优解,
     4 首先开始,只有a[1],那么这个a[1]就是最优解,也就是连续非空子序列中和最大的
     5 然后我们每增加一个数a[i],
     6 那么对a[i]而言(也就是说如果我们在子序列中必选a[i]的时候),
     7 如果a[i]之前的序列和大于0,a[i]加上它可以组成子序列的和肯定比只有a[i]更大,
     8 也就是a[i]<a[i]+s[i-1]
     9 如果a[i]之前的序列和小于等于0,那么a[i]完全可以不要之前的序列,
    10 也就是a[i]>=a[i]+s[i-1]
    11 
    12 这里表示的a[i]之前的序列,说的是以a[i-1]结尾的连续非空子序列中和最大的序列s[i-1]
    13 
    14 我们这里的对a[i]而言,也就是假设了必定选中a[i]的情况
    15 换言之,我们上面贪心的过程求到的最优解就是以a[i]为结尾的子序列和s[i]最大的的局部最优解
    16 而这里,我们在所有的s[i]中找最大的,即max(s[i]|1<=i<=n),
    17 那么就可以求出a[1]-a[n]所有子序列的全局最优解
    18 所以这个题目是可以用贪心来做的,就是可以由局部最优解得到全局最优解
    19 
    20 
    21 强调一遍 这题为什么可以用贪心来做:
    22 因为局部最优解表示的是以a[i]为结尾的子序列和最大的s[i],
    23 而全局最优解直接在所有的s[i]中找最大的就可以了
    24 我们可以由局部最优解,得到全局最优解,自然就可以用贪心来做
    25 
    26 
    27 一个题目能不能用贪心来做的依据
    28 因为贪心是“鼠目寸光”,求的是局部最优解,
    29 如果局部最优解能够得到全局最优解,那么一个题目就能用贪心来做
    30 
    31 
    32 算法步骤:
    33 1、找到以a[i-1]结尾的连续非空子序列中和最大s[i-1]
    34 2、
    35 如果这个序列s[i-1]为负数,那么以a[i]结尾的连续非空子序列中和最大的序列就是a[i]本身(本身我们可以看做+0)
    36 如果这个序列s[i-1]为正数,那么以a[i]结尾的连续非空子序列中和最大的序列就是a[i]+s[i-1]
    37 3、在所有的s[i]中找最大的,即max(s[i]|1<=i<=n)
    38 
    39 
    40 如果用贪心来求这个问题(最大连续子序列的和),输入的数全是负数怎么办,会不会出错
    41 肯定是不会出错的,因为我们贪心的过程是找所有以a[i]结尾的子序列和最大的s[i],
    42 而得到全局最优解的过程是在s[i]中找最大的,这样囊括了所有的情况,所以肯定是对的
    43 
    44 贪心的时间复杂度:
    45 O(n)
    46 
    47 贪心优化的原理
    48 贪心能够优化枚举法,是因为贪心着眼于局部的最优策略,
    49 只枚举了极少的局部的情况,所以贪心法有时候不一定对,但是一般效率都还可以
    50 
    51 
    52 */
    53 #include <iostream>
    54 #include <algorithm>
    55 using namespace std;
    56 int a[200005];
    57 int main(){
    58     int n;
    59     cin>>n;
    60     for(int i=1;i<=n;i++){
    61         cin>>a[i];
    62     }
    63     int sum=a[1];
    64     int maxx=a[1];
    65     for(int i=2;i<=n;i++){
    66         if(sum<=0) sum=0;
    67         sum+=a[i];
    68         maxx=max(sum,maxx);
    69     }
    70     cout<<maxx<<endl;
    71     return 0;
    72 }

    7、贪心优化

     1 /*
     2 
     3 上面贪心代码从代码层面上来看是可以优化的
     4 1、时间方面优化:循环可以合并(循环方向一致,循环最大值也是,并且两个循环之间没有什么逻辑操作代码)
     5 2、空间方面优化:代码中只用到了a[i],所以a[]数组可以用一个变量来代替
     6 
     7 */
     8 #include <iostream>
     9 #include <algorithm>
    10 using namespace std;
    11 int main(){
    12     int n;
    13     cin>>n;
    14     int sum;
    15     cin>>sum;
    16     int maxx=sum;
    17     for(int i=2;i<=n;i++){
    18         int x;
    19         cin>>x;
    20         if(sum<=0) sum=0;
    21         sum+=x;
    22         maxx=max(sum,maxx);
    23     }
    24     cout<<maxx<<endl;
    25     return 0;
    26 }

     
  • 相关阅读:
    【leetcode】206. 反转链表
    【leetcode】25. K 个一组翻转链表
    【leetcode】160. 相交链表
    【leetcode】92. 反转链表 II
    C# Socket通信
    正则表达式的学习
    python操作数据库sqlite3
    python爬取淘宝商品信息
    《Unix/Linux系统编程》第七、八章学习笔记 20201209戴骏
    ls的实现
  • 原文地址:https://www.cnblogs.com/Renyi-Fan/p/13036824.html
Copyright © 2020-2023  润新知