最长上升子序列(LIS)问题的解决及优化

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],其能做出的决策有:
  1. array[i]加入最长上升子序列:则f[i]=max{f[j]+1,0=<j<i,array[i]>array[j]};
  2. 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],其能做出的决策只能是:
  1. 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;
    }

上面的代码需要两重循环,时间复杂度为O(n^2)

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;
    } 
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值