ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

LeetCode300最长上升子序列 (相关话题:动态规划、二分法)

2021-07-06 10:07:05  阅读:132  来源: 互联网

标签:arr right LeetCode300 int max 二分法 序列 dp


题目描述

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

解题思路:

一、动态规划解法

dp[i]表示以arr[i]结尾,并且arr[i]是递增子序列最后一个的元素最大递增子序列长度

如果a[j]为上述最大递增子序列里倒数第二个元素的,那么arr[i] > arr[j]。此时只要求出dp[j]+1的最大值即为dp[i]。由此可以得到递推式dp[i] = Math.max(dp[i], dp[j] + 1);

public class LeetCode300最长上升子序列 {

	// 基于二分法
	private static int getMaxIncrSubStr2(Integer arr[]){
		
		//top[]数组用来记录arr[i]所放的堆叠
		//如top[0]=arr[3]表示arr[3]放在第一个堆叠
		//如top[1]=arr[2]表示arr[2]放在第一二个堆叠
		int [] top = new int[arr.length];
		
		int left=0,right=0;
		int mid = 0;

		//堆叠数
		int pfiles = 0;
		for (int i = 0; i < arr.length; i++) {
			
			right = pfiles;
			//无法搜索到目标值,退出去循环后一定left==right
			while (left < right) {
				 mid = (left+right)/2;
				 if(top[mid] < arr[i]){
					 left = mid+1;   //mid+1赋给left是因为(left+right)/2是向下取整,需要补偿left
				 }else if(top[mid] > arr[i]){
					 right = mid;
				 }else{
					 right = mid;
					 break;
				 }
			}
		
			
			//放第一张牌或找不到目标牌叠且大于最后一个堆叠的top值,创建一个堆叠数
			if(pfiles==0 || left == mid+1){
				pfiles++;
				top[pfiles-1] = arr[i];
			}else{
				//找到目标牌叠或找不到目标牌叠且小于第一个堆叠的top值,
				top[right] = arr[i];
			}

		}
		
		System.out.print(pfiles);
		return pfiles;
	}

	// 动态规划
	private static int getMaxIncrSubStr(Integer arr[]) {

		// dp[i]表示以arr[i]结尾,并且arr[i]是递增子序列最后一个的元素最大递增子序列长度,
		int dp[] = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			dp[i] = 1;
		}
		for (int i = 0; i < arr.length; i++) {
			for (int j = 0; j < i; j++) {
				// 倒数第二个a[j]小于a[i]时才计算dp[i]
				// dp[i]是以于a[j]为倒数第二个元素的最大递增子序列里的最大值
				if (arr[i] > arr[j]) {
					dp[i] = Math.max(dp[i], dp[j] + 1);
				}

			}
		}
		// max用来标记最大递增子序列的长度,还需要一个副本max1来记录max的值
		int max = -1, max1 = -1;
		// index用来记录递增子序列的最后一值在arr中的下标
		int index = 0;
		for (int i = 0; i < dp.length; i++) {
			if (max < dp[i]) {
				index = i;
			}
			max = Math.max(max, dp[i]);
			max1 = max;

		}

		
		//下面是寻找具体最大递增子序列的逻辑可以不看值当练手
		//接下来要根据max和index来记录下具体的最长递增子序列
		int[] subStArr = new int[max];
		// 递增子序列的最后一个值
		subStArr[max - 1] = arr[index];
		max1--;
		for (int i = index-1; i > 0; i--) {

			// 符合dp[i] +1 == dp[index]  && arr[i] < arr[index]的值才能放入递增子序列
			// 上一个元素对应的dp值一定要比后面的元素对应的dp值小,且上一个元素一定要比后面的元素小
			if (dp[i] +1 == dp[index]  && arr[i] < arr[index]) {
				subStArr[max1 - 1] = arr[i];
				index=i;
				max1--;
			}
		}

		for (int i = 0; i < subStArr.length; i++) {
			System.out.print(subStArr[i] + " ");
		}

		return max;
	}

	public static void main(String[] args) {
		Integer[] arr = { 10, 9, 2, 5, 3, 7, 101, 18 };
		getMaxIncrSubStr(arr);
	}
}

二、二分查找解法
这个解法的时间复杂度为 O(NlogN),但是说实话,正常人基本想不到这种解法(也许玩过某些纸牌游戏的人可以想出来)。所以大家了解一下就好,正常情况下能够给出动态规划解法就已经很不错了。

根据题目的意思,我都很难想象这个问题竟然能和二分查找扯上关系。其实最长递增子序列和一种叫做 patience game 的纸牌游戏有关,甚至有一种排序方法就叫做 patience sorting(耐心排序)。

为了简单起见,后文跳过所有数学证明,通过一个简化的例子来理解一下算法思路。

首先,给你一排扑克牌,我们像遍历数组那样从左到右一张一张处理这些扑克牌,最终要把这些牌分成若干堆。

poker1

处理这些扑克牌要遵循以下规则:

只能把点数小的牌压到点数比它大的牌上;如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去;如果当前牌有多个堆可供选择,则选择最左边的那一堆放置。

比如说上述的扑克牌最终会被分成这样 5 堆(我们认为纸牌 A 的牌面是最大的,纸牌 2 的牌面是最小的)。

poker2

为什么遇到多个可选择堆的时候要放到最左边的堆上呢?因为这样可以保证牌堆顶的牌有序(2, 4, 7, 8, Q),证明略。

poker3

按照上述规则执行,可以算出最长递增子序列,牌的堆数就是最长递增子序列的长度,证明略。

LIS

我们只要把处理扑克牌的过程编程写出来即可。每次处理一张扑克牌不是要找一个合适的牌堆顶来放吗,牌堆顶的牌不是有序吗,这就能用到二分查找了:用二分查找来搜索当前牌应放置的位置。

public class LeetCode300最长上升子序列 {
	// 基于二分法
	private static int getMaxIncrSubStr2(Integer arr[]){
		
		//top[]表示堆叠的最后一张牌
		int [] top = new int[arr.length];
		

		//堆叠数
		int pfiles = 0;
		int left=0,right=pfiles;
		int mid = 0;

		for (int i = 0; i < arr.length; i++) {
			
			right = pfiles;
			//为什么要用left < right 
			//没有堆叠时不进行二分搜索
			//只有一个堆叠时为搜索范围为[0,1)
			//两个堆叠时为搜索范围为[0,2)
			//无法搜索到目标值,退出去循环后一定left==right
			while (left < right) {
				 mid = (left+right)/2;
				 if(top[mid] < arr[i]){
					 left = mid+1;    
				 }else if(top[mid] > arr[i]){
					//不包含右边界
					 right = mid; 
				 }else{
					 //向左侧靠拢搜索
					 right = mid;
					 break;
				 }
			}
		
			
			//1.第一次时不进行二分搜索初始化堆叠为1
		    //2.第二次以后比top中所有的值都来的大,创建一个堆叠
			if(left == pfiles){
				pfiles++;
			} 
			//把这张牌放在堆顶
			top[left] = arr[i];

		}
		
		System.out.print(pfiles);
		return pfiles;
	}
	 

	public static void main(String[] args) {
		Integer[] arr = { 10, 9, 2, 5, 3, 7, 101, 18 };
		getMaxIncrSubStr2(arr);
	}
}

这个解法确实很难想到。首先涉及数学证明,谁能想到按照这些规则执行,就能得到最长递增子序列呢?其次还有二分查找的运用,要是对二分查找的细节不清楚,给了思路也很难写对。

所以,这个方法作为思维拓展好了。但动态规划的设计方法应该完全理解:假设之前的答案已知,利用数学归纳的思想正确进行状态的推演转移,最终得到答案。

变形题(俄罗斯套娃信封问题)

给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。注意:不允许旋转信封。

示例 1:

输入:envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出:3
解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
示例 2:

输入:envelopes = [[1,1],[1,1],[1,1]]
输出:1

思路分析

先对宽度 w 进⾏升序排序,如果遇到 w 相同的情况,则按照⾼度 h 降序排序(这样做目的是为了保证宽度一样时即使高度符合要求也无法套入)。之后把所有的 h 作为⼀个数组,在这个数组上计算 最长上升子序列的⻓度就是答案。

 

class Solution {

    public int maxEnvelopes(int[][] envelopes) {
     // 按宽度升序排列,如果宽度⼀样,则按⾼度降序排列
        Arrays.sort(envelopes, (t1, t2) -> {
            if (t1[0] < t2[0])
                return -1;
            else if (t1[0] == t2[0]) {
                if (t1[1] > t2[1])
                    return -1;
                else if (t1[1] == t2[1])
                    return 0;
            }
            return 1;
        });

        // 对⾼度数组寻找 LIS
        Integer[] arr = new Integer[envelopes.length];
        for (int i = 0; i < envelopes.length; i++) {
        	arr[i]=envelopes[i][1];
		}
        return getMaxIncrSubStr(arr);
    }


    // 动态规划
	private  int getMaxIncrSubStr(Integer arr[]) {

		// dp[i]表示以arr[i]结尾,并且arr[i]是递增子序列最后一个的元素最大递增子序列长度,
		int dp[] = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			dp[i] = 1;
		}
		for (int i = 0; i < arr.length; i++) {
			for (int j = 0; j < i; j++) {
				// 倒数第二个a[j]小于a[i]时才计算dp[i]
				// dp[i]是以于a[j]为倒数第二个元素的最大递增子序列里的最大值
				if (arr[i] > arr[j]) {
					dp[i] = Math.max(dp[i], dp[j] + 1);
				}

			}
		}
		// max用来标记最大递增子序列的长度,还需要一个副本max1来记录max的值
		int max = -1;
		for (int i = 0; i < dp.length; i++) {
			max = Math.max(max, dp[i]);
		}


		return max;
	}
}

 

标签:arr,right,LeetCode300,int,max,二分法,序列,dp
来源: https://blog.51cto.com/u_13270164/2985915

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有