LIS与LCS之LIS篇
LIS(Longest Increasing Subsequence)即最长上升子序列,该类问题的题意大概为给定一个序列,在原序列中任取任意多项,不改变其在原序列中的先后次序,得到的子序列要求长度最长、数值递增。需要注意的是该最长子序列不一定唯一,例如,给定序列[2,1,6,4,7,5,7,4]的最长子序列可为[1,4,5,7]或[2,4,5,7],两个子序列长度均为4,且为最长。
LIS问题是经典的DP问题。该问题有2种经典解法。如下:
一、O(n^2)解法
该解法即简单的二层循环嵌套遍历给定序列,记a[i]表示原序列中第i项的值,dp[i]表示以第i项为结尾的上升子序列的长度。如果存在j,使得j> i 并且a[j]>a[i],那么一定可以让第j项放到第i项后面,组成一个新的更长的上升子序列,即 dp[j] = dp[i] +1
因此,可以得到状态转移方程存在 j>i>1 && a[j]>a[i] 使得 dp[j] =max(dp[j],dp[i]+1
算法C++实现如下:
int dp[MAX_N], a[MAX_N], n;
int ans = 0; // 保存最大值
for (int i = 1; i <= n; ++i) {
dp[i] = 1;
for (int j = 1; j < i; ++j) {
if (a[j] < a[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
ans = max(ans, dp[i]);
}
cout << ans << endl; // ans 就是最终结果
二、O(nlogn)解法
对于第一种解法,我们可以考虑优化,即在dp值相同时,我们选择保留较小的显然会更好。因为,若一个上升子序列,他后面的数也能与当前子序列组成上升子序列,那么比该上升子序列小的一个上升子序列必然也能与这个数组成上升子序列,但反之却不一定。
例如,在序列[2,1,6,4,7,5,7,4]中,a3=6,a4=4的dp值均为2,但a6=5不能与a3=6构成上升子序列却能与a4=4构成上升子序列。因此对于不同的dp值只需要存一个对应的最小值,将这个最小值顺序排列,他们一定是升序的。
那么,我们可以维护一个数组ans,ans[len]保存长度为len的LIS的结尾的最小值,当a[i]>ans[len]时,我们更新ans数组,即使得ans[++len] =a[i]。如果不大于,则在数组ans中找到第一个大于a[i]的元素ans[j],用a[i]去更新ans[j],而对于查找ans数组来说,因为他是有序的,因此,可以采用二分法来快速查找,从而简化时间复杂度。
算法实现如下:
int ans[MAX_N], a[MAX_N], n; // ans 用来保存每个 dp 值对应的最小值,a 是原数组
int len; // LIS 最大值
ans[1] = a[1];
len = 1;
for (int i = 2; i <= n; ++i) {
if (a[i] > ans[len]) {
ans[++len] = a[i];
} else {
int pos = lower_bound(ans, ans + len, a[i]) - ans; // lower_bound是C++中的一个二分法的函数
ans[pos] = a[i];
}
}
cout << len << endl; // len 就是最终结果
至此,LIS算法的基础就此结束。
下面来看一道题——蒜头跳木桩,题目如下:(因计蒜客不能复制题目,这里用截图代替)
该题目就是简单的LIS,只是题目要求的是序列不递增,那么只要把判断条件改成小于等于即可。
AC代码如下:
#include"bits/stdc++.h"
using namespace std;
int main()
{
int n,h[1010];
cin>>n;
for(int i=1;i<=n;i++)
cin>>h[i];
int dp[1010],ans=1;
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
dp[i] =1;
for(int j=1;j<i;j++)
{
if( h[j]>=h[i] )
dp[i] = max(dp[i], dp[j] + 1);
}
ans =max (ans,dp[i]);
}
cout<<ans<<endl;
return 0;
}