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

本文深入探讨了最长上升子序列(LIS)问题,通过动态规划的方法详细解析了其解决方案。首先,介绍了基本动态规划思路,给出了一段简洁的Java代码实现。接着,文章提出了对LIS问题的二分优化方案,通过改变状态转移方程,利用二分查找降低时间复杂度至O(nlogn),并展示了优化后的代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
    } 
### C++ 实现最长上升子序列 (LIS) 算法 #### 示例代码解释 对于给定的一个整数数组 `a`,目标是找出其中最长的严格递增子序列并返回该序列的最大长度。下面展示了一种基于动态规划的方法来解决问题。 ```cpp #include <iostream> #include <algorithm> // For std::max function using namespace std; int main() { int a[1005], dp[1005], ans = 0; int n; cin >> n; // 输入数组大小 for (int i = 1; i <= n; ++i) { cin >> a[i]; // 输入数组元素 } for (int i = 1; i <= n; ++i) { dp[i] = 1; // 初始化dp数组,默认每个位置至少可以构成长度为1的上升子序列 for (int j = 1; j < i; ++j) { if (a[j] < a[i]) { // 如果存在更早的位置上的数值小于当前位置,则更新dp值 dp[i] = max(dp[i], dp[j] + 1); } } ans = max(ans, dp[i]); // 更新最大上升子序列长度 } cout << ans; // 输出最终的结果 return 0; } ``` 这段程序首先读取输入的数据到数组 `a[]` 中,并初始化另一个同样大小的辅助数组 `dp[]` 来记录到达每一个索引处所能形成的最长上升子序列的长度。遍历整个数组,在每一步都尝试寻找之前已经访问过的所有可能形成新的上升子序列的情况,并据此调整当前节点对应的 `dp[]` 值。最后输出的是在整个过程中发现的最大上升子序列长度[^1]。 此方法的时间复杂度为 O(),适用于较小规模的数据集(n ≤ 1e4)。当面对更大范围内的数据时,建议采用更加高效的算法比如贪心加二分查找的方式来进行优化处理[^5]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值