上次面字节AI Lab居然挂在这个上面了,来做一下。我在面试的时候没有想清楚动态规划的思路,匆忙开始做题了,导致没有做出来,一定要先把状态定义和状态转移彻底想清楚才开始做。对于这个题,我们定义dp[i]为考虑前i个元素,以第i个数字结尾的最长上升子序列的长度,注意nums[i]必须被选取!关于这个最后一个元素是否要被选取好像也是有点讲究的。我们从小到大计算dp数组的值,在计算dp[i]之前,我们已经计算出了dp[0…i-1]的值,则状态转移方程为:dp[i] = max(dp[j]) + 1其中0<=j<i且nums[j]<nums[i]即考虑往dp[0…i-1]中最长的上升子序列后面再加一个nums[i]。由于dp[j]代表nums[0…j]中以nums[j]结尾的最长上升子序列,所以如果能从dp[j]这个状态转移过来,那么nums[i]必然要大于nums[j],才能将nums[i]放在nums[j]后面以形成更长的上升子序列。最后,整个数组的最长上升子序列即所有dp[i]中的最大值。LIS_length = max(dp[i])其中0<=i<n。这样做的话时间复杂度比较高,为O(N^2),空间复杂度是O(N)。
下面讲一下另外一个方法,贪心+二分查找。我们着眼于某个上升子序列的结尾的元素,如果已经得到的上升子序列的结尾的数越小,那么遍历的时候后面接上一个数,会有更大的可能构成一个长度更长的上升子序列。既然结尾越小越好,我们可以记录在长度固定的情况下,结尾最小的那个元素的数值,这样定义后容易找到状态转移方程。我们将新的状态数组命名为tail。1,定义新状态:tail[i]表示长度为i+1的所有上升子序列的结尾的最小值。tail[0]表示长度为1的所有上升子序列中,结尾最小的元素的数值。以题目中的示例为例[10, 9, 2, 5, 3, 7, 101, 18]中,容易发现长度为2的所有上升子序列中,结尾最小的是子序列[2, 3],因此tail[1]=3。注意下标和长度有数值为1的偏差。2. 状态转移方程,数组tail是一个严格上升数组,证明用反证法,从略。只需要维护状态数组tail的定义,它的长度就是上升子序列的长度。以下为维护的方法:1. 在遍历数组nums的过程中,看到一个新数num,如果这个数严格大于有序数组tail的最后一个元素,就把num放在有序数组tail的后面,否则进入2。2. 在有序数组tail中查找第一个等于大于num的那个数,试图让它变小,如果有序数组数组中存在等于num的元素,什么都不做,因为以num结尾的最短的上升子序列已经存在;如果有序数组tail中存在大于num的元素,找到第一个,让它变小,这样我们就找到了一个结尾更小的相同长度的上升子序列。
python: