版权声明:本文为 DouMiaoO_Oo 原创文章,未经允许不得转载!转载请注明出处 https://blog.csdn.net/DouMiaoO_Oo/article/details/75670424
本章为了说明算法设计的重要性。这里引出一个 一维模式识别问题,也就是最大连续子串和。在最大连续子串和这个问题中,通过比较不同实现方法下的效率来说明这个问题。
相关题目可以参考 LeetCode maximum-subarray、牛客网 连续子数组的最大和 或者杭电OJ Max Sum。
书中列举的5种方法
(1)枚举i, j,再累加[i, …, j]的元素,复杂度O(n^3)。
for i = [0, 1, ..., n-1]:
for j = [i+1, ..., n-1]:
cur = 0
for k = [i, i+1, ..., j]:
cur += arr[k]
ans = max(ans, cur)
(2)枚举i,再从arr[i]开始依次累加元素直到到arr[n-1],每次累加尝试判断是不是需要更新最优解。
ans = arr[0]
for i = [0, 1, ..., n-1]:
cur = arr[i]
for j = [i+1, ..., n-1]:
cur += arr[j]
ans = max(ans, cur)
(3) sum[k]代表 arr[0, …, k]的连续元素的和(这一步时间复杂度为O(n)),然后枚举i, j 计算 sum[j] - sum[i-1] 来得到[i, j]的和,时间复杂度为O(n^2)
sum[-1] = 0
sum[0] = arr[0]
for i = [1, ..., n-1]:
sum[i] = sum[i-1] + arr[i]
ans = arr[0]
for i = [0, ..., n-1]:
for j = [i+1, ..., n-1]:
ans = max(ans, sum[j]-sum[i-1])
(4) 动态规划 时间复杂度为O(n)
cur = arr[0]
ans = arr[0]
for i = [1, ..., n-1]:
cur = max(cur+arr[i], arr[i])
ans = max(cur, ans)
(5) 书中还提到一个分治的算法。时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
思路就是每次把数组均等分为两半,最大连续子串和要么在左半边,要么在右半边,或者在包含了中心点的连续子序列中。
int maxsum(vector<int>& arr, int l, int r){
if(l == r) return arr[l];
int m = l + (r-l)/2; // 中心点
int cur = arr[m], lmax = arr[m], rmax = arr[m];
for(int i = m-1; i >= l; --i){
cur += arr[i];
lmax = max(lmax, cur);
}
cur = arr[m];
for(int i = m+1; i <= r; ++i){
cur += arr[i];
rmax = max(rmax, cur);
}
int ans = lmax+rmax-arr[m]; // 包含中心点的子序列对应的最优解
if(l <= m-1) ans = max(ans, maxsum(arr, l, m-1)); // 这里也可以maxsum(arr, l, m)
if(m+1 <= r) ans = max(ans, maxsum(arr, m+1, r));
return ans;
}
int FindGreatestSumOfSubArray(vector<int> arr) {
if(arr.size() == 0) return 0;
return maxsum(arr, 0, arr.size()-1);
}
现在我们分析一下这个方法。
(1)对于处于相同递归深度的所有函数,在判断包含中心点的最大子序列和的过程中,这些函数遍历的元素个数之和其实就是整个数组,所以对应的操作的时间复杂度为
O
(
n
)
O(n)
O(n)。在这里举例来说,对于第一层的递归,只有一个函数遍历了整个数组的
n
n
n个元素;对于第二层的函数来说,一个函数遍历了左半边
n
/
2
n/2
n/2个元素,另一个函数遍历了右半边
n
/
2
n/2
n/2个元素,所以共计遍历了
n
n
n个元素;以此类推。
(2)在递归中,每次子问题的规模减半,所以递归的深度是
O
(
l
o
g
n
)
O(log n)
O(logn)的。
综上,该方法的时间复杂度是O(n logn),分析的方法类似于归并排序。
若用T(n)表示解决规模为n的该问题所用的时间,则该分治方法可以得到如下的递推关系:T(n) = 2T(n/2)+O(n), 且T(1) = O(1)。
课后习题
第9题
我们将负数数组的最大连续子序列的和定义为0,即空向量的和。假设我们重新定义,将最大子向量的和定义为最大元素的值,那么应该如何修改各个程序?
解答:在之前的题目要求下, 一开始我们将最大连续子序列的和(ans)定义为0,然后在程序中尝试修改它。在更新的要求下,我们把ans初始化为给定序列arr中的某个元素即可,例如初始化ans=arr[0]即可。事实上,我在上文中给出的代码就是按照这个更新版本的要求来写的。
第10题
(1)假设我们想要查找的是总和最接近0的连续子序列,你能设计出的最有效的算法是什么,可以应用哪些算法设计技术?(2)假设我们想要查找的是总和最接近
t
t
t 的连续子序列,结果又将怎样?
解答:这个问题需要用到之前的预处理技术,构造
s
u
m
[
i
]
sum[i]
sum[i] 来表示
a
r
r
[
0
,
.
.
.
,
i
]
arr[0, ..., i]
arr[0,...,i] 的和。此时要找连续子序列的和接近于
t
t
t,就意味着要找到
s
u
m
[
j
]
−
s
u
m
[
i
−
1
]
sum[j] - sum[i-1]
sum[j]−sum[i−1] 最接近于
t
t
t, 等价于arr[i, …, j]的和最接近于
t
t
t。我们需要先构造sum[i] (时间复杂度O(n)),再对sum[i]排序(O(nlogn)),最后还需要用双指针法找到
i
i
i 和
j
j
j (O(n)). 注:为了在排序sum之后能得到原来序列的下标,sum[i]需要是个结构体,即保存了arr[0, …, i]的和以及当前的下标
i
i
i。
类似题目 https://leetcode.com/problems/subarray-sum-equals-k/
(0)构造sum数组并排序
sum[0] = arr[0]
for(int i = 1; i < n; ++i){
sum[i] = sum[i-1] + arr[i];
} sort(sum, sum+n);
(1) 当 t t t 等于0时,我们只需要判断排好序的sum数组中,所有相邻的两个元素即可,找到 i i i 使得sum[i-1]和sum[i]的差距最小。
(2) 对于一般情况下的 t > 0 t > 0 t>0,使用双指针法。
/* 这个代码没有验证过,仅写了大概思路 */
int i = 0, j = 1;
diff = sum[j] - sum[i];
while(1){
int cur = sum[j]-sum[i];
if(cur == t){
diff = 0;
break; // 找到答案并退出
}
else if(cur < t){
++j;
} else{ // cur > t
++i;
if(i == j) {
++j;
}
} if(j == n) break;
}
在这个双指针算法中, i i i 和 j j j 都是最多遍历数组一遍,所以这个过程的复杂度是O(n)的。
第13题
在最大子数组问题中,给定m×n的实数数组,我们需要求出矩形子数组的最大总和。该题的复杂度如何?
解答:
O
(
n
3
)
O(n^3)
O(n3), 这是一道经典的题,思路就是构造出二维的sum[i, j] 代表从原点(0, 0) 到 (i, j)矩形区域的和。
相同题目可见 牛客网 最大和子矩阵
第14题
给定整数m和大小为n的数组x,请找出使总和x[i]+…+x[i+m]最接近0的整数i (0 ≤ i < n-m)
解答:感觉这题就是滑动窗口,窗口的大小为m。还是用预处理计算,只不过每次滑到一个新的元素时,需要加上右边的元素,减去左边的元素,维持窗口内的元素个数为m。
sum = x[0]
for(int i = 1; i < m; ++i){
sum += x[i];
}
diff = abs(sum - t);
ans = m-1;
for(int i = m; i < n; ++i){
sum = sum - x[i-m] + x[i];
if(abs(sum-t) < diff) {
diff = abs(sum-t);
ans = i;
}
}
第15题
当T(1) = 0且n为2的幂时,递推公式T(n) = 2T(n/2)+cn的解是什么?如果T(1) = c,结果又怎么样?
解答:
(1)当T(1) = 0时,
T(2) = 2T(1)+2c = 2c =
2
l
o
g
2
(
2
)
c
2log_2(2)c
2log2(2)c
T(4) = 2T(2)+4c = 8c =
4
l
o
g
2
(
4
)
c
4log_2(4)c
4log2(4)c
T(8) = 2T(4)+8c = 24c =
8
l
o
g
2
(
8
)
c
8log_2(8)c
8log2(8)c
T(16) = 2T(8)+16c = 64c =
16
l
o
g
2
(
16
)
c
16log_2(16)c
16log2(16)c
所以此时T(n) =
n
l
o
g
2
(
n
)
c
nlog_2(n)c
nlog2(n)c
(2) 当T(1) = c时,
我们设
n
=
2
k
n = 2^k
n=2k, 则
T
(
n
)
=
T
(
2
k
)
=
2
T
(
2
k
−
1
)
+
c
n
T(n)= T(2^k) = 2T(2^{k-1})+cn
T(n)=T(2k)=2T(2k−1)+cn
=
2
{
2
T
(
2
k
−
2
)
+
(
c
n
/
2
)
}
+
c
n
= 2\{2T(2^{k-2})+(cn/2)\}+cn
=2{2T(2k−2)+(cn/2)}+cn
=
2
2
T
(
2
k
−
2
)
+
2
c
n
= 2^2T(2^{k-2}) +2cn
=22T(2k−2)+2cn
=
2
k
T
(
2
0
)
+
k
c
n
= 2^kT(2^{0}) +kcn
=2kT(20)+kcn
所以此时T(n) =
c
n
+
l
o
g
2
(
n
)
c
n
cn+log_2(n)cn
cn+log2(n)cn
验证一下:
T(2) = 2T(1)+2c = 2c+2c = 4c
T(4) = 2T(2)+4c = 12c
T(8) = 2T(4)+8c = 32c
T(16) = 2T(8)+16c = 80c
版权声明:本文为 DouMiaoO_Oo 原创文章,未经允许不得转载!转载请注明出处 https://blog.csdn.net/DouMiaoO_Oo/article/details/75670424