最长上升(下降)子序列

例题:P1020 [NOIP1999 普及组] 导弹拦截

题目大意

用一个导弹拦截系统拦截导弹,该系统第一发炮弹能到达任意高度,随后每发炮弹不能高于前一发的高度,每个导弹有一个固定的高度。
问该系统最多能拦截多少导弹,以及拦截所有导弹需要多少个系统。

解题思路

第一问就是求数组的最长下降子序列。
\(f[i]\)表示以第\(i\)个数为结尾的最长下降子序列的长度。
那么容易得到转移方程:

\[dp_i = max(1,\ \underset{j<i,h(j)\ge h(i)}{max(dp_j+1)}) \]

然而这种解法的复杂度为\(O(n^2)\),并不能通过本题的所有数据。

显然那个状态转移方程是可以优化的,我们可以令\(f[i]\)表示长度为\(i\)的下降子序列最后一个元素的最大值,容易发现\(f[i]\)是单调不增的(反证法易证),这一我们就可以用二分查找判断每个元素应该接在哪个长度的后面。

第二问考虑贪心,对于每个导弹,如果有若干系统可以拦截它,那么应该选择系统中当前高度(该系统命中最后一个导弹的高度)最低的那个,这样能尽可能的减少浪费。我们用\(f[i]\)表示每个拦截系统,并将它们的当前高度从小到大排序,对于每个导弹,二分查找第一个可以拦截它的系统,容易发现更新\(f[i]\)之后其单调性仍然满足。

参考代码

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
int h[N], f[N], t, cnt;
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    int a;
    while (cin >> a)
    {
        h[cnt++] = a;
    }
    for (int i = 0; i < cnt; ++i)
    {
        int l = 1, r = t, ans = 0;
        while (l <= r)
        {
            int mid = l + r >> 1;
            if (f[mid] >= h[i])
            {
                ans = mid;
                l = mid + 1;
            }
                
            else
                r = mid - 1;
        }
        f[ans + 1] = max(f[ans + 1], h[i]);
        t = max(t, ans + 1);
    }
    cout << t << endl;
    t = 0;
    memset(f, 0x3f, sizeof(f));
    for (int i = 0; i < cnt; ++i)
    {
        int l = 1, r = t, ans = 0;
        while (l <= r)
        {
            int mid = l + r >> 1;
            if (f[mid] < h[i])
            {
                ans = mid;
                l = mid + 1;
            }
                
            else
                r = mid - 1;
        }
        f[ans + 1] = min(f[ans + 1], h[i]);
        t = max(t, ans + 1);
    }
    cout << t;
    return 0;
}

容易发现两问的代码有极高的相似性,第二问其实是在计算最长严格上升子序列。这其实是Dilworth定理:将一个序列剖成若干单调不升子序列的最小个数等于该序列最长上升子序列的元素个数。

参考博客题解 P1020 【[NOIP1999 普及组] 导弹拦截】

posted @ 2023-02-05 22:03  何太狼  阅读(18)  评论(0编辑  收藏  举报