zoukankan      html  css  js  c++  java
  • 算法与数据结构---4.4、最大子段和-分治优化原理

    算法与数据结构---4.4、最大子段和-分治优化原理

    一、总结

    一句话总结:

    在本题中,分治能够优化枚举的原理就是分治策略创造了信息(比如本题第二种情况子序列一定包含mid),让我们可以拿这个信息将枚举从O(n^2)的算法优化到了O(n)
     1 //下面代码是没用好分治创造的信息的分治法代码,只能过两个点
     2 //而用好分治法创造的信息的分治法代码,可以过所有点
     3 #include <iostream>
     4 #include <algorithm>
     5 using namespace std;
     6 int a[200005];
     7 int s[200005]={0};
     8 //分治(二分)求最大连续子序列的和
     9 int find(int l,int r){
    10     if(l==r) return a[l];
    11     int mid=(l+r)/2;
    12     //1、计算第二种跨越mid情况的序列的最大和
    13     int maxx=-0x7fffffff;
    14     for(int i=l;i<=mid;i++){
    15         for(int j=mid;j<=r;j++){
    16             //2、对每一段进行求和,在这些和里面选出最大的
    17             int sum=s[j]-s[i]+a[i];
    18             if(sum>maxx) maxx=sum;
    19         }
    20     }
    21 
    22     //2、比较方式1、2、3的最大值
    23     return max(max(find(l,mid),find(mid+1,r)),maxx);
    24 }
    25 
    26 int main(){
    27     int n;
    28     cin>>n;
    29     for(int i=1;i<=n;i++){
    30         cin>>a[i];
    31         s[i]=s[i-1]+a[i];
    32     }
    33     cout<<find(1,n)<<endl;
    34     return 0;
    35 }

    1、枚举法能够优化的实质是什么?

    1、枚举法能够优化的实质是有信息(关系式、等式、条件)可以让我们减少枚举情况(减少枚举范围、减少枚举变量、减少不必要的枚举)
    2、比如在这个题里面,没有信息,我们通过分治就创造了这些信息,从而通过分治法优化了枚举

    2、分治能够优化枚举的实质是什么?

    a、分治其实只是一种枚举策略,只能改变枚举的方式,并不能减少枚举的次数
    b、分治能够优化枚举,不在于分治这种策略,而是分治策略创造了信息(比如本题第二种情况子序列一定包含mid),让我们可以拿这个信息去优化枚举


    我们在分治的过程中创造了信息
    我们在用分治算法的时候,就创造了下面这些信息
    子序列的情况只能是这三种情况里面的一种
    ①完全处于序列的左半:l<=i<=j<=mid
    ②跨越序列中间:i<=mid<=j<=r
    ③完全处于序列的右半:mid<i<=j<=r

    二、最大子段和

    博客对应课程的视频位置:4.4、最大子段和-分治优化原理
    https://www.fanrenyi.com/video/27/266

    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 }

  • 相关阅读:
    70.BOM
    69.捕获错误try catch
    68.键盘事件
    523. Continuous Subarray Sum
    901. Online Stock Span
    547. Friend Circles
    162. Find Peak Element
    1008. Construct Binary Search Tree from Preorder Traversal
    889. Construct Binary Tree from Preorder and Postorder Traversal
    106. Construct Binary Tree from Inorder and Postorder Traversal
  • 原文地址:https://www.cnblogs.com/Renyi-Fan/p/13035121.html
Copyright © 2011-2022 走看看