P1020 导弹拦截

原题链接:P1020 [NOIP1999 提高组] 导弹拦截 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
相当好的一道题,用于理解使用 [[狄尔沃斯定理(Dilworth 定理)]]
当然这个定理肯定不止这么简单。

第一问就是让求一个最大不上升子序列,如果用 DP 求解,将是 \(O(n^2)\) 的时间复杂度,而这道题数据范围极限为 \(10^5\),因此必须优化。

如果先放下优化想第二问的话,如果可以很快就可以想出来一种贪心。即维护一个序列,序列上每个值代表一个系统拦截到的最后一个导弹的高度 \(k_i\),每加入一个导弹(高度记为 \(h\)),就在序列中找到那个高度大于等于 \(h\) 的最小的 \(k_i\),如果没有这样的 \(k_i\) 就新加入一个系统 \(k_j\),高度即为 \(h\)
为什么是最小?因为总有一个 \(k_i\) 会变成 \(h\),如果是最小的我们就可以尽量保持别的 \(k_i\) 尽可能的高,从而尽量拦截更多导弹,减少系统的使用个数。

上面说的这部分我们可以二分,维护一个单调不升的序列,这样如果没有符合的 \(k_i\),直接在序列末端加上即可。当然维护成单调不减也行见仁见智,但加入 \(k_j\) 的方式不同。而这个单调的序列可以用单调栈来维护。

这是贪心,可以证明,如果有非贪心方案的最优方案,一定可以通过导弹的调换,变成贪心方案。

我们第二位求的,实际上就是不升子序列最小划分,根据狄尔沃斯定理(Dilworth 定理),它的数目就等于求最长上升子序列的元素数量。而我们第一问求的是最长不升子数列,根据这个定理就相当于求上升子序列的最小划分。也就是说第一二问在这种意义上,解法是一样的。

这题在 Acwing 上是一个 Dp 题,但第二问也是这样做的,差别在于数据范围,Acwing 的容许 \(O(n^2)\) 的第一问做法。

而关于实际操作就看代码吧。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int g[N], cnt[N];
int q[N], tt; // 相当一个栈

int main()
{
    while (scanf("%d", &g[ ++ n]) == 1);
    n -- ;
    
    memset(q, 0x3f, sizeof q); // 防止意外的入栈
    tt = 0; // 相当于栈的指针 
    for (int i = 1; i <= n; i ++ ) 
    {
        int l = 1, r = tt;
        while (l < r)
        {
            int mid = l + r >> 1;
            if (q[mid] < g[i]) r = mid;
            else l = mid + 1;
        }
        
        // 如果说我二分不出来符合条件的,就只能新加一个了
        if (q[l] >= g[i]) q[ ++ tt] = g[i]; 
        else q[l] = g[i];
    }
    
    cout << tt << endl;
    
    memset(q, sizeof 0, sizeof q);
    tt = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int l = 1, r = tt;
        while (l < r)
        {
            int mid = l + r >> 1;
            if (q[mid] >= g[i]) r = mid;
            else l = mid + 1;
        }
        if (q[l] < g[i] || tt == 0) q[ ++ tt] = g[i]; 
        else q[l] = g[i];
    }
    
    cout << tt << endl;
    
    return 0;
    
}
posted @ 2024-05-26 21:08  blind5883  阅读(16)  评论(0编辑  收藏  举报