最大连续子数组,动态规划经典问题

it2023-08-14  67

动态规划(Dynamic Programming)其中的 Programming指的是一种表格法。 相较于分治,动态规划主要应用于子问题重叠的情况。 重点是找出最优子结构


最大连续子数组问题:

一、 问题描述

二、 几种解决方法:

1 暴力枚举:

最简单也是最耗时的做法就是对其进行枚举,为了找全所有的连续子数组,我们看看至少需要几层循环: 对一个连续的子数组 A [ l . . r ] A[l..r] A[l..r], r r r表示子数组右边的坐标, l l l表示左边的

int smax = -inf; for(int r = 0; r < n; ++r) { // 枚举右端点 for (int l = 0; l <= r; ++l) { // 枚举左端点 int sum = 0; // 表示子数组A[l..r]的和 for (int i = l; i <= r; ++i) { // 对[l,r]遍历求和 sum += A[i]; // } if(sum > smax) smax = sum; //if .... } }

一共有多少种呢: n + C n 2 n+C_n^2 n+Cn2

所以需要三重循环,时间复杂度应该是 O ( n 3 ) O(n^3) O(n3),这种做法做了等于没做,还不如不做。接下来想一想怎么对其优化。

枚举之所以时间复杂度高,是因为它在三重循环的过程中,实际上重复计算了很多元素。 例如:某一步中我们已经求了连续子数组 A [ 1..6 ] A[1..6] A[1..6]的和,做了一次从 A [ 1 ] A[1] A[1] A [ 6 ] A[6] A[6]的遍历,当我们想求 A [ 1..7 ] A[1..7] A[1..7]的时候,我们又遍历了一遍 A [ 1..6 ] A[1..6] A[1..6],这样就对 A [ 1..6 ] A[1..6] A[1..6]的遍历做了重复。

容易想到的一个优化的思路就是记录下每次遍历得到的值,如果需要对这一块再次遍历时,就直接拿出来用,就可以避免掉一次的重复。

2 优化枚举

核心思想: S ( l , r ) = ∑ l ≤ i ≤ r A [ i ] = S ( l , r − 1 ) + A [ r ] S(l,r) = \sum_{l \le i \le r}A[i] = S(l, r-1) + A[r] S(l,r)=lirA[i]=S(l,r1)+A[r]

int smax = -inf; for (int l = 0; l < n; ++l){ int s = 0; for (int r = l; r < n; ++r) { s = s + A[r]; //当前的s=sum(A[l..r]) = s(A[l..r-1]) + A[r] smax = max(smax, s); // } }

两重循环,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

即使这样做了,中间还是有重复计算的: 比如:计算 S ( 1 , 6 ) S(1,6) S(1,6) 和 计算 S ( 2 , 7 ) S(2, 7) S(2,7), 两个都对 S ( 2 , 6 ) S(2,6) S(2,6)进行了重复的遍历求和。

所以优化枚举虽然使得每个 S ( l , r ) S(l,r) S(l,r)只用一次for就解决了,但是不同的 S ( l i , r i ) S(l_i, r_i) S(li,ri)之间还是有一些不必要的重复计算。导致了时间复杂度比较高。

想要进一步降低时间复杂度该怎么办呢?

3 分治算法

为了采用分治算法,我们要将原问题分解为两个规模尽量相等的子问题,分别求解。 也就是将原数组分解成两个大小尽量相等的子数组,对这两个子数组分别求解其最大连续子数组。 然后还要注意的是,还要求出跨中间分界线的最大连续子数组和是多少。 假设 A [ 1... n ] A[1...n] A[1...n] m i d = n 2 mid = \dfrac{n}{2} mid=2n, 将原数组分解成两个子数组: A [ 1.. m i d ] A[1..mid] A[1..mid](最大子数组和为 S l S_l Sl) 和 A [ ( m i d + 1 ) . . . n ] A[(mid+1)...n] A[(mid+1)...n](最大子数组和为 S r S_r Sr), 跨越 m i d mid mid的最大子数组和为 S m i d S_{mid} Smid,那么整体的最大子数组和应该是 m a x ( S l , S r , S m i d ) max(S_l, S_r, S_{mid}) max(Sl,Sr,Smid)

我自己写的有点乱,看看课件有条理的分析:

伪代码:

主伪代码

跨中间的连续最大子数组的和 伪代码,来自《算法导论》

代码实现:(先鸽着,重点是写dp!)

\\咕咕咕

3 动态规划

靠,写了一个小时才写到重点…, 不过自己一步一步思考从从哪里得到的优化思路还是很有收获的。废话少说,赶紧总结完去写别的作业。

D [ i ] D[i] D[i] 表示以A[i]开头的连续最大子数组的和

(怎么想到的?我也不知道now…),

那么可以得到一个递推方程(即状态转移方程):

// 初始化: for (int i = 0; i < n; ++i) D[i] = A[i]; // 既然D[i]是以A[i]开头的balabala,那么D[i]肯定要包含A[i] // 至于加不加上D[i+1],要看D[i+1]加上之后能不能使D[i]变大 if(D[i+1] > 0) D[i] = D[i+1] + A[i]; else D[i] = A[i];

由于这个方程是由 D [ i + 1 ] D[i+1] D[i+1]得到 D [ i ] D[i] D[i],那么我们遍历要从后往前遍历: (或者你就用 D [ i ] D[i] D[i]表示以 A [ i ] A[i] A[i]为结尾的连续最大子数组的和,这样就是从前往后遍历)

伪代码:

这里是用Rec[i] 记录解的位置,然后再返回去查找连续最大子数组的左右下标,好好学习一下

显然时间复杂度为 O ( n ) O(n) O(n), NB!!!

开头说了,DP中的P是指表格,也就是D[i], 这个问题比较简单,就是一个一维的表格。

代码实现

// 洛谷P1115 最大子段和 #include <iostream> using namespace std; int D[200010], A[200010], Rec[200010]; int main() { int n; cin >> n; for (int i = 1; i <= n; i++) { cin >> A[i]; } // 初始化 D[n] = A[n]; Rec[n] = n; // 递推 for (int j = n - 1; j > 0; j--) { if(D[j+1] > 0) { D[j] = A[j] + D[j+1]; Rec[j] = Rec[j+1]; // Rec[j] 记录的是以A[j] 开始的最大连续子数组 右端点的下标 } else { D[j] = A[j]; Rec[j] = j; } } int smax = D[n], l=0, r=0; for (int i = 1; i <= n; ++i) { if (D[i] > smax) { smax = D[i]; l = i; r = Rec[i]; } } cout << smax; return 0; }
最新回复(0)