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 }

  • 相关阅读:
    MFC 解析xml文件
    数字图像处理-----主成成分分析PCA
    C++设计模式——建造者模式
    总结的文章--未读
    八大排序算法
    C++读取、旋转和保存bmp图像文件编程实现
    数字图像处理-----直方图均衡化
    数字图像处理------中值滤波
    matlab图像基础知识
    MFC最大化显示任务栏
  • 原文地址:https://www.cnblogs.com/Renyi-Fan/p/13035121.html
Copyright © 2011-2022 走看看