题目:
输出最长递增子序列的长度,如输入 4 2 3 1 5 6,输出 4 (因为 2 3 5 6组成了最长递增子序列)。
暴力破解法:这种方法很简单,两层for循环搞定,时间复杂度是O(N2)。
动态规划:之前我们使用动态规划去解决一般是创建一维数组或者二维数组来构建出dp表,利用之前的历史上dp表中的值进行相关的处理求解出这个过程中的几个最大值,最小值,然后相加减来得出dp表的当前元素的值,所以我们会想,先创建一个一维数组,因为数组中选择的元素的范围在进行变化,所以dp表表示的值为截取到当前范围内最长的递增子序列是多少,但是在填表的过程中发现这种一般方法是不行的。但是这作为一道经典的题目,思路本身是很难的,有人想出来了它的dp解法,我们可以通过借鉴它的思路来帮助我们扩展理解dp算法。
那么它的思路是:dp[i]表示的意思是必须包含以dp[i]结尾的最长递增子序列,那么它的值就是以dp[i]结尾的最长递增子序列长度。依次去扫描dp[i]这个字符之前的字符,假如发现当前字符大于之前的字符那么比较当前的最长子序列的长度与当前dp[i] + 1的值的大小来更新当前最长递增子序列的长度,当扫描完当前字符i之前的字符那么这个时候说明找到了以当前字符结尾的最长递增子序列的长度,把这个值赋值给dp[i]。
需要注意的是最后还需要扫描一下dp数组,看一下哪一个dp[i]最大,因为我们不知道以哪个字符结尾的字符序列为最长的递增子序列,所以需要扫描一下,最后找到dp[i]的最大值返回,这是与之前的dp数组不同的一个点,以前是返回dp数组的最后一个元素就可以了,因为最后一个元素就是我们需要求解的最优解。这个解法时间复杂度也是O(N2)。
那解法三的思路呢:dp[i]表示的是长度为i的最长递增子序列的末尾的那个数。先初始化dp[0]为数组中第一个元素的值,即dp[0] = 4,定义一个指针p用来记录当前递增子序列的位置,从数组的第二个元素开始比较当前arr[i]与当前指针指向的dp数组的位置的值的大小,假如arr[i] > dp[p]那么说明当前的字符可以构成更长的递增子序列那么我们应该更新dp数组,指针往下移动,然后把当前的arr[i]的值赋值给dp[++p],假如arr[i] < dp[p]说明当前字符不能够构成更长的递增子序列,此时需要进行元素的替换,为什么进行替换呢?因为当前更小的元素更有利于最长递增子序列的贡献,所以扫描dp数组指向位置以及之前的位置假如arr[i] 大于dp[p]那么我们将其dp[p] 替换为arr[i]
最后返回的是指针p指向的位置,注意返回最长递增子序列的长度是p指向的下标,这也是与之前动态规划的一般解法不同的点。更进一步的优化就是这种解法在扫描dp数组的过程中可以通过二分查找来降低时间复杂度,整体从O(N2)降低到O(NlgN)。
代码:
package chapter_08_贪心策略与动态规划; public class 最长递增子序列 { static int[] arr = { 4, 2, 3, 1, 5, 6, 4, 8, 5, 9 }; public static void main(String[] args) { System.out.println(f(arr)); // 输出 6 System.out.println(dp(arr)); // 输出 6 System.out.println(dp1(arr)); // 输出 6 } // 暴力破解法 private static int f(int[] arr) { int maxCnt = 0; for (int i = 0; i < arr.length; i++) { int p = i; int cnt = 1; for (int j = i + 1; j < arr.length; j++) { if (arr[j] > arr[p]) { cnt++; p = j; } } // if (cnt>maxCnt){ // maxCnt=cnt; // } maxCnt = Math.max(maxCnt, cnt); } return maxCnt; } static int[] dp = new int[arr.length]; // 解法二 private static int dp(int[] arr) { dp[0] = 1; for (int i = 1; i < arr.length; i++) { int cnt = 1; for (int j = i - 1; j >= 0; j--) { if (arr[i] > arr[j]) { cnt = Math.max(cnt, dp[j] + 1); } } dp[i] = cnt; } int ans = -1; for (int i = 0; i < dp.length; i++) { ans = Math.max(ans, dp[i]); } return ans; } // 解法三 /* 在优化之后,可以达到O(NlgN) */ private static int dp1(int[] arr) { dp = new int[arr.length + 1]; dp[1] = arr[0];// 长度为1的最长递增子序列,初始化为第一个元素 int p = 1;// 记录dp更新的最后位置 for (int i = 1; i < arr.length; i++) { if (arr[i] > dp[p]) { dp[p + 1] = arr[i]; p++; } else { //扫描dp数组,替换第一个比arr[i]大的dp //for (int j = 0; j <= p; j++) { // if (dp[j] > arr[i]) { // dp[j] = arr[i]; // } //} // 二分查找,比上面扫描dp数组耗时少 int indexOfFirstBigger = indexOfFirstBigger(dp, arr[i], 0, p); if (indexOfFirstBigger != -1) dp[indexOfFirstBigger] = arr[i]; } } return p; } /** * 在递增数组中,从左查找第一个比v大的元素的下标 */ public static int indexOfFirstBigger(int[] dp, int v, int l, int r) { while (l <= r) { int mid = (l + r) >> 1; if (dp[mid] > v) { r = mid; // 保留大于v的下标以防这是第一个 } else { l = mid + 1; } if (l == r && dp[l] > v) return l; } return -1; } }