0%

算法设计-最大上升序列SIL

【题目描述】

给定N个数,求这N个数的最长上升子序列的长度

也就是说,对于给定的序列,保持序列的原有输出不变,从中挑选出一个子集,使其可以满足从小到大的顺序,比如说下面的 2 3 就是符号要求的上升序列,但是 3 5 不行,因为顺序发生了交换。

【样例输入】

7
2 5 3 4 1 7 6

【样例输出】

4 // 即 2 3 4 7 或者 2 3 4 6

一共有两种方法,采用的都是动态规划的思想。

第一种方法说的是,用 dp[i] 来表示一个状态,表示前 i 个元素的最长上升序列的长度,对于第 i + 1 个元素,其实就是跟前 i 个元素去比较,如果第 i + 1 个元素比前面某个元素大,比如这个元素是 j ,那么 dp[i+1] 更新为 dp[j] + 1 。相当于原来的 j 形成的序列后又接入了第 i 个元素,即状态转移方程(这个状态转移方程不全,还必须满足元素的大小关系)

那么这种算法的时间复杂度(因为是双重循环)就是 $O(n^2)$ ,代码如下

# include <bits/stdc++.h>

using namespace std;

int dp[1005];
int n;
int a[1005];

int main()
{
    cin >> n;
    for(int i = 0; i < n; i++)
    {
        cin >> a[i];
    } 

    int ans = 0;
    for(int i = 0; i < n; i++)
    {
        dp[i] = 1;
        for(int j = 0; j < i; j++)
        {
            if(a[j] < a[i])
            {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
       
        ans = max(ans, dp[i]);
    }

    cout << ans;
}

这种算法其实是可以优化的,因为在原理的状态转移方程中

实现 max 的算法采用的是内层循环,但是如果采用二分查找来实现找到最大的值的方法,那么就会获得 $O(n\log n)$ 的复杂度,为了实现二分查找,可以采用贪心策略:所以每遇到一个比栈顶元素大的数,就放进栈里,遇到比栈顶元素小的就二分查找前边的元素,找到一个“最应该被换掉的元素”,用新数去更新前边的元素。这个元素可能不是最优解的一部分,但是它可以使得后面还未加入的、比较小的数更有可能进入这个队列。通俗地来说,作为门槛,他本来要大于当前序列的最后一个数才能加进去;就是如果我太大了,我就乖乖呆在末尾;如果前面有一个数比我大,也就是我比你好,既然我在你后面也就是我们两者只能选其一,那我只好把你替换掉了。虽然我这临时临头换的不一定最合适,但是对于后面还有很多的人等着排进来的情况下,我给他们创造了更多机会,使得这个序列的最后一个数有可能变小,让更多的人进来。

具体代码如下:

# include <bits/stdc++.h>

using namespace std;

int stk[1005];
int top;
int n;
int a[1005];

int main()
{
    cin >> n;
    for(int i = 0; i < n; i++)
    {
        cin >> a[i].l >> a[i].w;
    }

    stk[top++] = a[0];

    for(int i = 1; i < n; i++)
    {
        if(stk[top - 1] < a[i])
        {
            stk[top++] = a[i];
        }
        else
        {
            *(lower_bound(stk, stk + top, a[i])) = a[i];
        }
    }

    cout << top;

    return 0;
}

此外,根据dilworth定理,不下降子序列最小个数等于最大上升子序列的长度。