算法与数据结构---4.5、最大子段和-贪心解法
一、总结
一句话总结:
贪心求解最大连续子序列的和,其实贪心得到的局部最优解是所有以a[i]结尾的子序列和最大的s[i],在s[i]中找最大的就是全局最优解,时间复杂度为O(n)
算法步骤: 1、找到以a[i-1]结尾的连续非空子序列中和最大s[i-1] 2、 如果这个序列s[i-1]为负数,那么以a[i]结尾的连续非空子序列中和最大的序列就是a[i]本身(本身我们可以看做+0) 如果这个序列s[i-1]为正数,那么以a[i]结尾的连续非空子序列中和最大的序列就是a[i]+s[i-1] 3、在所有的s[i]中找最大的,即max(s[i]|1<=i<=n) #include <iostream> #include <algorithm> using namespace std; int a[200005]; int main(){ int n; cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } int sum=a[1]; int maxx=a[1]; for(int i=2;i<=n;i++){ if(sum<=0) sum=0; sum+=a[i]; maxx=max(sum,maxx); } cout<<maxx<<endl; return 0; }
1、如果用贪心来求这个问题(最大连续子序列的和),输入的数全是负数怎么办,会不会出错?
肯定是不会出错的,因为我们贪心的过程是找所有以a[i]结尾的子序列和最大的s[i],而得到全局最优解的过程是在s[i]中找最大的,这样囊括了所有的情况
2、贪心优化能够优化枚举法的原理是什么?
贪心能够优化枚举法,是因为贪心着眼于局部的最优策略,只枚举了极少的局部的情况,所以贪心法有时候不一定对,但是一般效率都还可以
二、最大子段和
博客对应课程的视频位置:4.5、最大子段和-贪心解法
https://www.fanrenyi.com/video/27/267
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=