转自:点击打开链接
最长子序列可以说是刚接触动态规划的人经常遇见也不得不解决的问题,最常见的有两种,一种是最长公共子序列(LCS),还有一个是最长上升子序列(LIS)。今天我就总结下这两个的做法。一:最长公共子序列(LCS)
题目描述:给你两个数组,可以是数字的,也可以是字符串,我们假设是数字的!举个例子:
X = 1, 5, 6, 4, 1, 3, 7
Y = 1, 1, 6, 8, 3, 4, 7
求一个新的数组S,该数组中的每个数均是X和Y数组中的公共数,并满足原数组中数字的前后关系,这样的数组有很多个,比如说(1,1),(1,1,3,7),(1,6,7)等。同时S数组要是长度最长的那个,像上面的(1,1,3,7),长度是4,那么即为所求解!
做DP题目最重要的一点是能正确的构造出DP函数,如果能很好的构造出来,就成功一半了。
dp[i][j] 表示X数组的前i位和Y数组的前j位之前的LCS,那么它可以由前面三个状态推出来。
0 if(i=0 || j=0) --初始化,不难理解,不管是X还是Y数组,只要有一个长度是0,那么S数组就是0
dp[i][j] = max(dp[i-1][j],dp[i][j-1] if(i,j>0 && X[i] != Y[j]) --S数组没有添加数字,那么只能从前面继承来,一个从X,一个从Y,看那个大
dp[i-1][j-1] + 1 if(i,j>0 && X[i] == Y[j]) --如果是两个相同,当然是把S数组加1,可以看成X和Y都继承了
代码很简单,两个for循环就可以了。
二:最长递增子序列(LIS)
题目描述:给你一个数组,如X数组,X = 1,2,3,6,5,7,4,8,6。
求一个新的数组S,该数组中的每个数均是X数组中的数,且对S中任意两个数S[i],S[j]满足 j>i && s[j]>s[i]。同时S数组要是长度最长的那个,像(1,2,3,6,7,8)和(1,2,3,5,7,8),长度是6,即为所求解。
dp[i]表示以X数组中的第i个元素为S数组的底元素的最长子序列的长度。这样又可以变成子问题来求解了,
dp[i] = max(dp[j]) + 1 0<j<i && X[i] > num[j]。就是在i前面找一个符合条件的最长子序列。
对这个题目来说,dp方程还是比较容易推出来和理解的,但你可以发现,时间复杂度是O(n^2)的,这对许多题目都是不能接受的,所以你必须进行优化。这就牵扯到一个问题,最长递增子序列的O(n*lgn)做法。
做法如下:首先给你一个数组stack,用来存储最长递增子序列(按照递增的要求,即数组的最顶端的数最大,最尾的数最小,如果说用栈的话你也许更好理解),对X数组进行线扫,如果X[i]大于数组的顶端元素(最大值),那么把这个数加入到stack数组中来,如果比顶端元素小,那么在stack数组中找到第一个大于X[i]的数,并用X[i]替换掉。因为stack数组是有序的,在你找第一个大于X[i]的数的时候你就可以用二分查找来做,这就是优化所在!
这两个都是很简单的dp题目,但也包含了动态规划思想,在后面的很多题目中你会发现都会不经意中用到这种思想,所以很好的理解他们是必须的。但动态规划博大精深,就因为他没有固定的程序,许多都是思想,所以仅仅知道这两个是远远不够的!
O(n^2)的算法:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <climits>
using namespace std;
const int maxn = 1010;
int n;
int array[maxn], maxv[maxn], lis[maxn];
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; i++){
scanf("%d", &array[i]);
lis[i] = 1;
}
lis[0] = -1;
maxv[1] = array[1];
maxv[0] = INT_MIN;
int maxlis = 1;
for(int i = 1; i <= n; i++)
{
int j;//mav[i]表示长度为i的最长递增子序列中的最大元素中的最小值
for(j = maxlis; j >= 1; j--)
{
if(array[i] > maxv[j])
{
lis[i] = j + 1;
break;
}
}
if(lis[i] > maxlis){
maxlis = lis[i];
maxv[maxlis] = array[i];
}
else if(maxv[j] < array[i] && array[i] < maxv[j + 1])
{
maxv[j + 1] = array[i];
}
}
printf("%d\n", maxlis);
return 0;
}
for(j = maxlis; j >= 0; j--)
{
if(array[i] > maxv[j])
{
lis[i] = j + 1;
break;
}
}
在求那个j时,改用二分搜索查询,可以把O(n^2)降为O(nlogn)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <climits>
using namespace std;
const int maxn = 1010;
int n;
int array[maxn], maxv[maxn], lis[maxn];
int binarysearch(int low, int high, int index);
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &array[i]);
lis[i] = 1;
}
lis[0] = -1;
maxv[1] = array[1];
maxv[0] = INT_MIN;
int maxlis = 1;
for(int i = 1; i <= n; i++)
{
int j = binarysearch(1, maxlis, i);//找到前面长度最长的并且满足最后一个元素小于array[i]。
if(j != -1)
lis[i] = j + 1;
else
j = 0;
if(lis[i] > maxlis)
{
maxlis = lis[i];
maxv[maxlis] = array[i];
}
else if(maxv[j] < array[i] && array[i] < maxv[j + 1])
{
maxv[j + 1] = array[i];
}
}
printf("%d\n", maxlis);
return 0;
}
int binarysearch(int low, int high, int index)
{
int pre = -1;
while(low <= high)
{
int mid = (low + high) >> 1;
if(array[index] > maxv[mid])
{
pre = mid;
low = mid + 1;
}
else
high = mid - 1;
}
return pre;
}