文章目录
1.区间动态规划
- 这类动态规划的特点是求出最值的序列,往往
dp[i][j]或者dp[j]
中的j表示以j结尾的动态规划,这种题目的代表题目是求最长上升子序列,最长的公共子串,子序列的问题等等。
2.最长上升子序列问题
2.1 数据量比较小O(N^2)
2.1.1 分析
- 初始化:
vector<int> dp(n,1);
。 - 状态转移方程:
dp[i]=max(dp[i],dp[j]+1);
。 - 核心代码:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(),1);
int res=0;
for(int i=0;i<nums.size();i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i]=max(dp[i],dp[j]+1);
}
}
res=max(res,dp[i]);
}
return res;
}
2.1.2 标记路径
- 如果题目需要求出路径的话,我们可以同时标记路径,例如序列:
{7,9,6,10,7,1,3)
的pre分别为{0,0,2,1,2,5,5}
,刚开始pre全部初始化为自己的下标。最大的dp值为3,所以最长子序列长度为3, 末尾的元素在3位置。追踪其路径为:3->pre[3] ,1->pre[1],0->pre[0](停止)
,因此路径为4,2,1,这个为倒序的,因为从后往前找的。
2.2 数据量比较大O(nlogn)
2.2.1 分析
- 分析:新建一个 low 数组,low[i]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护 low 数组,对于每一个a[i],如果a[i]>low[当前最长的LIS长度],直接把a[i]接到当前最长的LIS后面,即low [++当前最长的LIS长度]=a[i]。 那么,怎么维护 low 数组呢?对于每一个a [ i ],如果a [i]能接到 LIS 后面,就接上去;否则,就用a[i] 取更新low数组。具体方法是,在low数组中找到第一个大于等于 a [i]的元素low[j],用a[i]去更新low[j]。如果从头到尾扫一遍low数组的话,时间复杂度仍是O(n^2)。我们注意到 low数组内部一定是非递减的,因此可以二分low数组,找出第一个大于等于a[i]的元素。一次二分low数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
- 举例1:有以下序列A[ ]={3,1,2,6,4,5,10,7},求LIS长度?首先我们定义一个B[i]来储存可能的排序序列,len为LIS长度。我们依次把A[ i ]有序地放进B[ i ]里。
A[1]=3,把3放进B[1],此时B[1] = 3,此时len = 1,最小末尾是3。
A[2]=1,因为1比3小,所以可以把B[1]中的3替换为1,此时B[1]=1,此时len=1,最小末尾是1。
A[3]=2,2大于1,就把2放进B[2] = 2,此时B[ ]={1,2},len=2。
同理,A[4]=6,把6放进B[3] = 6,B[ ]={1,2,6},len=3。
A[5]=4,4在2和6之间,比6小,可以把B[3]替换为4,B[ ]={1,2,4},len=3。
A[6]=5,B[4]=5,B[ ]={1,2,4,5},len=4。
A[7]=10,B[5]=10,B[ ]={1,2,4,5,10},len=5。
A[8]=7,7在5和10之间,比10小,可以把B[5]替换为7,B[ ]={1,2,4,5,7},len=5。
- 举例1总结:最终我们得出LIS长度为5,但是,但是B[ ]中的序列并不一定是正确的最长上升子序列。在这个例子中,我们得到的1 2 4 5 7 恰好是正确的最长上升子序列。
- 举例2:有以下序列A[ ] = {1,4,7,2,5,9,10,3},求LIS长度。
A[1] = 1,把1放进B[1],此时B[1] = 1,B[ ] = {1},len = 1。
A[2] = 4,把4放进B[2],此时B[2] = 4,B[ ] = {1,4},len = 2。
A[3] = 7,把7放进B[3],此时B[3] = 7,B[ ] = {1,4,7},len = 3。
A[4] = 2,因为2比4小,所以把B[2]中的4替换为2,此时B[ ] = {1,2,7},len = 3。
A[5] = 5,因为5比7小,所以把B[3]中的7替换为5,此时B[ ] = {1,2,5},len = 3。
A[6] = 9,把9放进B[4],此时B[4] = 9,B[ ] = {1,2,5,9},len = 4。
A[7] = 10,把10放进B[5],此时B[5] = 10,B[ ] = {1,2,5,9,10},len = 5。
A[8] = 3,因为3比5小,所以把B[3]中的5替换为3,此时B[ ] = {1,2,3,9,10},len = 5。
- 举例2总结:最终我们得出LIS长度为5。但是,但是这里的1 2 3 9 10很明显并不是正确的最长上升子序列。因此,B序列并不一定表示最长上升子序列,它只表示相应最长子序列长度的排好序的最小序列。这有什么用呢?我们最后一步3替换5并没有增加最长子序列的长度,而这一步的意义,在于记录最小序列,代表了一种最可能性,只是此种算法为计算LIS而进行的一种替换。假如后面还有两个数据12和15,那么B[ ]将继续更新。
- 总结:因为在B中插入的数据是有序的,不需要移动,只需要替换,所以可以用二分查找插入的位置,那么插入n个数的时间复杂度为〇(logn),这样我们会把这个求LIS长度的算法复杂度降为O(nlogn)。
- 核心代码:
int lengthOfLIS(vector<int>& nums) {
vector<int> low(1);
int k=1;
low[0]=