信息学奥赛一本通 1281:最长上升子序列 | OpenJudge NOI 2.6 1759:最长上升子序列

本文介绍了如何使用动态规划和贪心策略解决最长上升子序列问题,包括两种动态规划方法(以i为结尾和以i为起始的最长上升子序列)及贪心算法的细节,涉及状态定义、状态转移方程和复杂度分析,并给出了相应的C++代码实现。
摘要由CSDN通过智能技术生成

【题目链接】

ybt 1281:最长上升子序列
OpenJudge NOI 2.6 1759:最长上升子序列

【题目考点】

1. 动态规划:线性动规
  • 最长上升子序列
    动规方法:复杂度: O ( n 2 ) O(n^2) O(n2)
    贪心方法:复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

【解题思路】

解法1:动态规划

方法1: 状态定义为以i为结尾的最长上升子序列的长度
1. 确定状态

分析状态:
阶段:子序列存在的区间
决策:每一次是否选择一个元素
策略集合:上升的子序列
条件:序列长度最长
统计量:长度

状态定义
dp[i]:以第i元素为结尾的最长上升子序列的长度。

2. 确定状态转移方程
  • 分割集合:以第i元素为结尾的上升子序列构成的集合。
    • 子集1:对所有满足j < i的j, 如果第i元素大于第j元素,则以第j元素为结尾的上升子序列加上第i元素,形成新的上升子序列。
    • 子集2:否则,只有一个第i元素构成上升子序列。
  • 分析状态变量:
    • 子集1:dp[i]为:所有满足j<i且第j元素小于第i元素的j取dp[j]+1
    • 子集2:dp[i]为1
      dp[i]为所有可能的状态值中的最大值。

题目要求最长上升子序列,那么就是求以每个位置为结尾的最长上升子序列中最长的长度,即求dp数组中的最大值。

方法2:状态定义为以i为起始的最长上升子序列的长度
1. 确定状态

状态定义
dp[i]:以第i元素为起始的最长上升子序列的长度。

2. 确定状态转移方程
  • 分割集合:以第i元素为起始的上升子序列构成的集合。
    • 子集1:对所有满足j > i的j, 如果第i元素小于第j元素,则以第j元素为起始的上升子序列前面加上第i元素,形成新的上升子序列。长度为dp[i]=dp[j]+1
    • 子集2:否则,只有一个第i元素构成上升子序列。长度为:dp[i]=1

dp[i]为所有可能的状态值中的最大值。

题目要求最长上升子序列,那么就是求以每个位置为结尾的最长上升子序列中最长的长度,即求dp数组中的最大值。
以上两种方法的复杂度为 O ( n 2 ) O(n^2) O(n2)

解法2:贪心

a[i]为第i个数字。记d[i]表示长度为i的上升子序列最后一个数字可能的最小值。len为d数组的长度。
初始情况,len为0。d数组下标为1~len,d[len]为d数组最后一个元素。
先做一次d[++len] = a[1]
i从2到n遍历数组a

  • 如果a[i]大于d[len],那么d[++len] = a[i]
  • 如果a[i]小于等于d[len],那么通过二分查找在d[1]~d[len]中找到大于等于a[i]的最小值的下标为l,让d[l] = a[i]
  • 最后len的值即为最长上升子序列的长度。

解析:
该算法的整体思路是:

  • d[len]尽量小,这样后面遇到的a[i]就有更大可能接在以d[len]为末尾的长为len的上升子序列的后面,延长上升子序列。
  • 而要使d[len]尽可能小,则需要使d[len-1]尽可能小。如果可能的话,把d[len-1]更新得更小,后面才可能把d[len]更新得更小。
  • 继续推广,就是要尽量把d中每个元素都更新得更小。所以d[len]存储的是长为len的上升子序列的末尾元素的最小值。

分步解析:

  • 如果a[i]大于d[len],那么可以在长为len的以d[len]为结尾的上升子序列后面,添加一个a[i],形成长为len+1的上升子序列,末尾元素为a[i]。所以有d[++len] = a[i]
  • 如果a[i]小于等于d[len],考虑长为几的上升子序列能够以a[i]为结尾。由于d[i]是长为i的上升子序列末尾的最小值,所以要让d数组的元素尽量小。
    根据第1种向d添加元素的方式,d数组一定是升序的。
    • l较小时,a[i] > d[l-1]a[i] > d[l],此时不需要a[i]来替换哪个元素。
    • a[i] > d[l-1]a[i] <= d[l]时,此时存在一个以d[l-1]为结尾的长为l-1的上升子序列,可以把a[i]添加在这个上升子序列的后面,构成一个以a[i]为结尾的长为l的上升子序列,即d[l] = a[i],做这步操作可以使d[l]变小。
    • 由于d[l-1] < a[i] <= d[l] < d[l+1],所以更新后d数组仍然是升序的。

注意:d数组并非一个上升子序列,它只是每个上升子序列的末尾元素。
设下标o < p < q < i
假设d[l-1]值为a[o]
d[l]是在d[l-1]表示的上升子序列末尾添加了a[p]得到的,即d[l]=a[p]
d[l+1]是在d[l]表示的上升子序列末尾添加了a[q]得到的,即d[l+1]=a[q]
此时有数字a[i],根据上述更新规则,发现有d[l-1] < a[i] <= d[l],所以更新d[l] = a[i]
此时d[l]表示的上升子序列的最后两个元素为a[o] a[i]
d[l+1]表示的上升子序列的最后两个元素不变,与a[i]无关,仍然是a[o] a[p] a[q]

由于 d数组是升序的,那么就可以通过二分查找在d[1]~d[len]中找到大于等于a[i]的最小值。
该方法的总体复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

【题解代码】

解法1.1:状态定义为以i为结尾的最长上升子序列的长度
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int a[N], dp[N];//a[i]:第i个数 dp[i]:以i为结尾的最长上升子序列的长度 
int main()
{
    int n, mxlen = 0;
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    for(int i = 1; i <= n; ++i)
    {
        dp[i] = 1;//第j元素自己构成上升子序列 
        for(int j = 1; j < i; ++j)
            if(a[i] > a[j])
                dp[i] = max(dp[i], dp[j]+1);
        mxlen = max(mxlen, dp[i]);
    }
    cout << mxlen;
    return 0;
}
解法1.2:状态定义为以i为起始的最长上升子序列的长度
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int a[N], dp[N];//dp[i]:以i为起始的最长上升子序列的长度 
int main()
{
    int n, mxlen = 0;
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    for(int i = n; i >= 1; --i)
    {
        dp[i] = 1;
        for(int j = i+1; j <= n; ++j)
            if(a[i] < a[j])
                dp[i] = max(dp[i], dp[j] + 1);
        mxlen = max(mxlen, dp[i]);
    }
    cout << mxlen;
    return 0;
}
解法2:贪心
  • 写法1:手写二分查找
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int n, a[N], d[N], len;//d[i]:长为i的上升子序列最后一个数字可能的最小值
int main()
{
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    d[++len] = a[1];
    for(int i = 2; i <= n; ++i)
    {
        if(a[i] > d[len])
            d[++len] = a[i];
        else
        {//找d中大于等于a[i]的最小值的下标
            int l = 1, r = len;
            while(l < r)
            {
                int m = (l+r)/2;
                if(d[m] >= a[i])
                    r = m;
                else
                    l = m+1;
            }
            d[l] = a[i];
        }
    }
    cout << len;
    return 0;
}
  • 写法2:使用stl lower_bound
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int n, a[N], d[N], len;//d[i]:长为i的上升子序列最后一个数字可能的最小值
int main()
{
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    d[++len] = a[1];
    for(int i = 2; i <= n; ++i)
    {
        if(a[i] > d[len])
            d[++len] = a[i];
        else
        {//找d中大于等于a[i]的最小值的下标
            int l = lower_bound(d+1, d+1+len, a[i])-d;
            d[l] = a[i];
        }
    }
    cout << len;
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值