1、LIS:最长上升子序列问题
LIS问题的描述为:给定一个整数序列array,找到它的所有严格递增子序列中最长的序列,输出其长度。
例:10 9 2 5 3 7 101 18
它的最长上升子序列有:2 5 7 101、2 5 7 18、2 3 7 101、2 3 7 18
长度均为4,因此结果为4。
解决LIS问题用到动态规划思想,令f[i]表示以i结尾的最长上升子序列的长度,LIS问题每一阶段的决策为:对于array[i],是否将其加入前一阶段的最长上升子序列中,但是注意,这个决策是不能由我们主动进行的,因为问题中存在限制条件,即它一定要形成上升子序列。由于子序列不要求连续,因此i位置前的每个位置都可能是它前一阶段的状态,有了状态和决策,下面开始分析本题的状态转移过程:
对于状态f[i],因为i位置前的每个位置都可能是它上一阶段的状态,所以我们需要从0到i遍历array序列,设遍历到的位置为j,那么:
- 如果array[i]>array[j],其能做出的决策有:
- array[i]加入最长上升子序列:则f[i]=max{f[j]+1,0=<j<i,array[i]>array[j]};
- array[i]不加入最长上升子序列:则f[i]=1;
该情况下最终f[i]=max{1,max{f[j]+1,0=<j<i,array[i]>array[j]} },注意到f[j]+1>=1,所以可以简化为f[i]=max{f[j]+1,0=<j<i,array[i]>array[j]};实际上该结论我们能直接凭直觉得到:能加入一定加入,因为它肯定比不加入好。
- 如果array[i]<=array[j],其能做出的决策只能是:
- array[i]不加入最长上升子序列:则f[i]=1;
所以,最终的状态转移方程为:f[i]=max{1, max{f[j]+1, 0=<j<i,array[i]>array[j]}}
但是注意,此时我们求得的是以每个位置i结尾的最长上升子序列长度,而不是整个序列的,最终从这些f[i]找到最大的那个才是整个序列的最长上升子序列长度,示例代码如下:
//可以仔细体会以下代码的写法,有些算法教材中的写法显得过于臃肿,下面的代码则十分简洁明了
public int LIS (int[] array) {
int[] f = new int[array.length];
int ans = 0;
for(int i=0; i<array.length; ++i) {
f[i]=1;//在循环开始前将f[i]赋值为1可以略去后面的比较
//f[i]=max{1, max{f[j]+1, 0=<j<i,array[i]>array[j]}}
for(int j=0; j<i; ++j) {
if(array[i]>array[j])
//此处没有和1比较的原因是在循环开始前已经将f[i]赋值为了1。
f[i]=Math.max(f[i], f[j]+1);
}
//ans = max{f[i], 0=<i<n},找到所有f[i]中最大的值就是最终结果。
ans = Math.max(f[i], ans);
}
return ans;
}
上面的代码需要两重循环,时间复杂度为。
2、LIS问题的二分优化
针对LIS问题,我们有一种优化方案:
若array[i]能接在array[j]的后面,而k<i且array[k]<array[j],则array[i]也一定能接在array[k]的后面,也就是可以用array[k]替代array[j]。在此基础上我们改动f的定义,现在f[cnt]表示最长上升子序列长度为cnt的序列的最小结尾,如上文例子最后得到的长度为4的几个最长上升子序列中,18要小于101,因此f[4]=18而不是101。
改动f定义的意义在于,我们现在的array[i]不用再去和所有的array[j](0<j<i)比较,而直接和f[cnt]比较,因为f[cnt]表示当前的最长上升子序列的长度为cnt,且结尾最小是f[cnt],如果array>f[cnt]这说明array[i]可以接到当前的最长上升子序列后面,最长上升子序列长度加1,即cnt+1,且f[cnt]暂时等于array[i];如果array<=f[cnt],当前的最长上升子序列的长度不会变化,但可能影响最长上升子序列长度为1~cnt的最小结尾,为此我们需要找到f数组中>=array[i]的最小值,这个问题就十分熟悉了,二分查找可以在log(n)的时间内查找完成,一共需要查找n次,总时间复杂度为O(nlogn)。
例:array:10 9 2 5 3 7 101 18 f[0]=-∞
array[0]=10 f[1]=10 、 array[1]=9 f[1]=9;
array[2]=2 f[1]=2 、 array[3]=5 f[2]=5;
array[4]=3 f[2]=3 、 array[5]=7 f[3]=7;
array[6]=101 f[4]=101 、 array[7]=18 f[4]=18;
最终结果为4。
//二分优化的LIS
public int LIS(int[] a) {
int[] f = new int[a.length];
for(int i=0;i<f.length;++i)
f[i]=-100000;
int cnt = 0;
for(int i=0;i<a.length;++i) {
if(a[i]>f[cnt]) {
f[++cnt]=a[i];
continue;
}
//二分查找
f[search(0,cnt,a[i],f)]=a[i];
}
return cnt;
}
public int search(int l, int r, int x, int[] f){
while(l<r) {
int mid=(l+r)>>1;
if(x==f[mid]) return mid;
else if(x>f[mid]) l=mid+1;
else r=mid;
}
return l;
}