2
2
3
3
这是 LeetCode 上的 ** [ 978. 最长湍流子数组] ( https://leetcode-cn.com/problems/longest-turbulent-subarray/solution/xiang-jie-dong-tai-gui-hua-ru-he-cai-dp-3spgj/ ) ** ,难度为 ** 中等** 。
4
4
5
- Tag : 「序列 DP」
5
+ Tag : 「线性 DP」、「 序列 DP」
6
6
7
7
8
8
9
- 当 A 的子数组 A[ i] , A[ i+1] , ..., A[ j] 满足下列条件时,我们称其为湍流子数组:
9
+ 当 ` A ` 的子数组 $ A[ i] , A[ i+1] , ..., A[ j] $ 满足下列条件时,我们称其为湍流子数组:
10
10
11
- * 若 i <= k < j,当 k 为奇数时, A[ k] > A[ k+1] ,且当 k 为偶数时,A[ k] < A[ k+1] ;
12
- * 或 若 i <= k < j,当 k 为偶数时,A[ k] > A[ k+1] ,且当 k 为奇数时, A[ k] < A[ k+1] 。
11
+ * 若 $ i <= k < j$ ,当 $k$ 为奇数时,$ A[ k] > A[ k+1] $ ,且当 $k$ 为偶数时,$ A[ k] < A[ k+1] $ ;
12
+ * 若 $ i <= k < j$ ,当 $k$ 为偶数时,$ A[ k] > A[ k+1] $ ,且当 $k$ 为奇数时,$ A[ k] < A[ k+1] $ 。
13
13
14
14
也就是说,如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组。
15
15
16
- 返回 A 的最大湍流子数组的长度。
16
+ 返回 ` A ` 的最大湍流子数组的长度。
17
17
18
18
示例 1:
19
19
```
@@ -33,58 +33,55 @@ Tag : 「序列 DP」
33
33
```
34
34
35
35
提示:
36
- * 1 <= A.length <= 40000
37
- * 0 <= A[ i] <= $ 10^9$
36
+ * $ 1 <= A.length <= 40000$
37
+ * $ 0 <= A[ i] <= 10^9$
38
38
39
39
---
40
40
41
- ### 基本分析思路
41
+ ### 基本思路
42
42
43
43
本题其实是要我们求最长一段呈 ` ↗ ↘ ↗ ↘ ` 或者 ` ↘ ↗ ↘ ↗ ` 形状的数组长度。
44
44
45
- 看一眼数据范围,有 40000,那么枚举起点和终点,然后对划分出来的子数组检查是否为「湍流子数组」的朴素解法就不能过了。
45
+ 看一眼数据范围,有 $ 40000$ ,那么枚举起点和终点,然后对划分出来的子数组检查是否为「湍流子数组」的朴素解法就不能过了。
46
46
47
47
朴素解法的复杂度为 $O(n^3)$ ,直接放弃朴素解法。
48
48
49
49
复杂度往下优化,其实就 $O(n)$ 的 DP 解法了。
50
50
51
- ***
51
+ ---
52
52
53
53
### 动态规划
54
54
55
- 至于 DP 如何分析,通过我们会先考虑一维 DP 能否求解,不行再考虑二维 DP ...
55
+ 至于 DP 如何分析,通过我们会先考虑一维 DP 能否求解,不行再考虑二维 DP。
56
56
57
57
对于本题,** 由于每个位置而言,能否「接着」上一个位置** 形成「湍流」,取决于上一位置是由什么形状而来。
58
58
59
59
举个例子,对于样例 ` [3,4,2] ` ,从 4 -> 2 已经确定是 ` ↘ ` 状态,那么对于 2 这个位置能否「接着」4 形成「湍流」,要求 4 必须是由 ` ↗ ` 而来。
60
60
61
61
** 因此我们还需要记录某一位是如何来的(` ↗ ` 还是 ` ↘ ` ),需要使二维 DP 来求解 ~ **
62
62
63
- 我们定义 f(i,j) 代表以位置 i 为结尾,而结尾状态为 j 的最长湍流子数组长度(0:上升状态 / 1:下降状态)
64
-
65
- * PS. 这里的状态定义我是猜的,这其实是个技巧。通常我们做 DP 题,都是先猜一个定义,然后看看这个定义是否能分析出状态转移方程帮助我们「不重不漏」的枚举所有的方案。一般我是直接根据答案来猜定义,这里是求最长子数组长度,所以我猜一个 f(i,j) 代表最长湍流子数组长度*
63
+ 我们定义 $f[ i] [ j ] $ 代表以位置 $i$ 为结尾,而结尾状态为 $j$ 的最长湍流子数组长度(0:上升状态 / 1:下降状态)
66
64
67
- 那么 ` f[i][j] ` 该如何求解呢?
65
+ > PS. 这里的状态定义我是猜的,这其实是个技巧。通常我们做 DP 题,都是先猜一个定义,然后看看这个定义是否能分析出状态转移方程帮助我们「不重不漏」的枚举所有的方案。一般我是直接根据答案来猜定义,这里是求最长子数组长度,所以我猜一个 f(i,j) 代表最长湍流子数组长度
68
66
69
- 我们知道位置 ` i ` 是如何来是唯一确定的(取决于 ` arr[i] ` 和 ` arr[i - 1] ` 的大小关系),而只有三种可能性:
67
+ 不失一般性考虑 $f [ i ] [ j ] $ 该如何求解, 我们知道位置 $i$ 是如何来是唯一确定的(取决于 $ arr[ i] $ 和 $ arr[ i - 1] $ 的大小关系),而只有三种可能性:
70
68
71
- * ` arr[i - 1] < arr[i] ` :该点是由上升而来,能够「接着」的条件是 ` i - 1 ` 是由下降而来。则有:` f[i][0] = f[i - 1][1] + 1 `
72
- * ` arr[i - 1] > arr[i] ` :改点是由下降而来,能够「接着」的条件是 ` i - 1 ` 是由上升而来。则有:` f[i][1] = f[i - 1][0] + 1 `
73
- * ` arr[i - 1] = arr[i] ` :不考虑,不符合「湍流」的定义
69
+ * $ arr[ i - 1] < arr[ i] $ :该点是由上升而来,能够「接着」的条件是 $ i - 1$ 是由下降而来。则有:$ f[ i] [ 0 ] = f[ i - 1] [ 1 ] + 1$
70
+ * $ arr[ i - 1] > arr[ i] $ :改点是由下降而来,能够「接着」的条件是 $ i - 1$ 是由上升而来。则有:$ f[ i] [ 1 ] = f[ i - 1] [ 0 ] + 1$
71
+ * $ arr[ i - 1] = arr[ i] $ :不考虑,不符合「湍流」的定义
74
72
75
73
代码:
76
- ``` java
74
+ ``` Java
77
75
class Solution {
78
76
public int maxTurbulenceSize (int [] arr ) {
79
- int n = arr. length;
80
- int ans = 1 ;
77
+ int n = arr. length, ans = 1 ;
81
78
int [][] f = new int [n][2 ];
82
- for ( int i = 0 ; i < 2 ; i ++ ) f[0 ][i ] = 1 ;
79
+ f[ 0 ][ 0 ] = f[0 ][1 ] = 1 ;
83
80
for (int i = 1 ; i < n; i++ ) {
84
- for ( int j = 0 ; j < 2 ; j ++ ) f[i][j ] = 1 ;
85
- if (arr[i - 1 ] < arr[i]) f[i][0 ] = f[i - 1 ][1 ] + 1 ;
86
- if (arr[i - 1 ] > arr[i]) f[i][1 ] = f[i - 1 ][0 ] + 1 ;
87
- for ( int j = 0 ; j < 2 ; j ++ ) ans = Math . max(ans, f[i][j]);
81
+ f[i][ 0 ] = f[i][1 ] = 1 ;
82
+ if (arr[i] > arr[i - 1 ]) f[i][0 ] = f[i - 1 ][1 ] + 1 ;
83
+ else if (arr[i] < arr[i - 1 ]) f[i][1 ] = f[i - 1 ][0 ] + 1 ;
84
+ ans = Math . max(ans, Math . max( f[i][0 ], f[i][ 1 ]));
88
85
}
89
86
return ans;
90
87
}
@@ -93,28 +90,28 @@ class Solution {
93
90
* 时间复杂度:$O(n)$
94
91
* 空间复杂度:$O(n)$
95
92
96
- ***
93
+ ---
97
94
98
- ### 动态规划( 空间优化:奇偶滚动)
95
+ ### 空间优化:奇偶滚动
99
96
100
- 我们发现对于 ` f[i][j] ` 状态的更新只依赖于 ` f[i - 1][j] ` 的状态。
97
+ 我们发现对于 $ f[ i] [ j ] $ 状态的更新只依赖于 $ f[ i - 1] [ j ] $ 的状态。
101
98
102
- 因此我们可以使用「奇偶滚动」方式来将第一维从 ` n ` 优化到 2 。
99
+ 因此我们可以使用「奇偶滚动」方式来将第一维从 $n$ 优化到 $2$ 。
103
100
104
- 修改的方式也十分机械,只需要改为「奇偶滚动」的维度直接修改成 2 ,然后该维度的所有访问方式增加 ` %2 ` 即可:
101
+ 修改的方式也十分机械,只需要改为「奇偶滚动」的维度直接修改成 $2$ ,然后该维度的所有访问方式增加 ` %2 ` 或者 ` &1 ` 即可:
105
102
106
- ``` java
103
+ 代码:
104
+ ``` Java
107
105
class Solution {
108
106
public int maxTurbulenceSize (int [] arr ) {
109
- int n = arr. length;
110
- int ans = 1 ;
107
+ int n = arr. length, ans = 1 ;
111
108
int [][] f = new int [2 ][2 ];
112
- for ( int i = 0 ; i < 2 ; i ++ ) f[0 ][i ] = 1 ;
109
+ f[ 0 ][ 0 ] = f[0 ][1 ] = 1 ;
113
110
for (int i = 1 ; i < n; i++ ) {
114
- for ( int j = 0 ; j < 2 ; j ++ ) f[i % 2 ][j ] = 1 ;
115
- if (arr[i - 1 ] < arr[i]) f[i % 2 ][0 ] = f[(i - 1 ) % 2 ][1 ] + 1 ;
116
- if (arr[i - 1 ] > arr[i]) f[i % 2 ][1 ] = f[(i - 1 ) % 2 ][0 ] + 1 ;
117
- for ( int j = 0 ; j < 2 ; j ++ ) ans = Math . max(ans, f[i % 2 ][j] );
111
+ f[i % 2 ][ 0 ] = f[i % 2 ][1 ] = 1 ;
112
+ if (arr[i] > arr[i - 1 ]) f[i % 2 ][0 ] = f[(i - 1 ) % 2 ][1 ] + 1 ;
113
+ else if (arr[i] < arr[i - 1 ]) f[i % 2 ][1 ] = f[(i - 1 ) % 2 ][0 ] + 1 ;
114
+ ans = Math . max(ans, Math . max( f[i % 2 ][0 ], f[i % 2 ][ 1 ]) );
118
115
}
119
116
return ans;
120
117
}
@@ -123,38 +120,30 @@ class Solution {
123
120
* 时间复杂度:$O(n)$
124
121
* 空间复杂度:使用固定 ` 2 * 2 ` 的数组空间。复杂度为 $O(1)$
125
122
126
- ***
123
+ ---
127
124
128
- ### 动态规划( 空间优化:维度消除)
125
+ ### 空间优化:维度消除
129
126
130
127
既然只需要记录上一行状态,能否直接将行的维度消除呢?
131
128
132
- 答案是可以的,当我们要转移第 ` i ` 行的时候,` f ` 数组装的就已经是 ` i - 1 ` 行的结果。
129
+ 答案是可以的,当我们要转移第 $i$ 行的时候,$f [ i ] $ 装的就已经是 $ i - 1$ 行的结果。
133
130
134
131
这也是著名「背包问题」的一维通用优手段。
135
132
136
133
但相比于「奇偶滚动」的空间优化,这种优化手段只是常数级别的优化(空间复杂度与「奇偶滚动」相同),而且优化通常涉及代码改动。
137
134
138
- ``` java
135
+ 代码:
136
+ ``` Java
139
137
class Solution {
140
138
public int maxTurbulenceSize (int [] arr ) {
141
- int n = arr. length;
142
- int ans = 1 ;
139
+ int n = arr. length, ans = 1 ;
143
140
int [] f = new int [2 ];
144
- for ( int i = 0 ; i < 2 ; i ++ ) f[i ] = 1 ;
141
+ f[ 0 ] = f[ 1 ] = 1 ;
145
142
for (int i = 1 ; i < n; i++ ) {
146
- int dp0 = f[0 ], dp1 = f[1 ];
147
- if (arr[i - 1 ] < arr[i]) {
148
- f[0 ] = dp1 + 1 ;
149
- } else {
150
- f[0 ] = 1 ;
151
- }
152
- if (arr[i - 1 ] > arr[i]) {
153
- f[1 ] = dp0 + 1 ;
154
- } else {
155
- f[1 ] = 1 ;
156
- }
157
- for (int j = 0 ; j < 2 ; j++ ) ans = Math . max(ans, f[j]);
143
+ int a = f[0 ], b = f[1 ];
144
+ f[0 ] = arr[i - 1 ] < arr[i] ? b + 1 : 1 ;
145
+ f[1 ] = arr[i - 1 ] > arr[i] ? a + 1 : 1 ;
146
+ ans = Math . max(ans, Math . max(f[0 ], f[1 ]));
158
147
}
159
148
return ans;
160
149
}
@@ -163,17 +152,11 @@ class Solution {
163
152
* 时间复杂度:$O(n)$
164
153
* 空间复杂度:$O(1)$
165
154
166
- ***
167
-
168
- ### 其他
169
-
170
- 如果你对「猜 DP 的状态定义」还没感觉,这里有道类似的题目可以瞧一眼:[ 45. 跳跃游戏 II] ( https://leetcode-cn.com/problems/jump-game-ii/solution/xiang-jie-dp-tan-xin-shuang-zhi-zhen-jie-roh4/ )
171
-
172
155
---
173
156
174
157
### 最后
175
158
176
- 这是我们「刷穿 LeetCode」系列文章的第 ` No.978 ` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完 。
159
+ 这是我们「刷穿 LeetCode」系列文章的第 ` No.978 ` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完 。
177
160
178
161
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
179
162
0 commit comments