[luogu p3143] [USACO16OPEN]Diamond Collector S

\(\mathtt{Link}\)

传送门

\(\mathtt{Summarization}\)

给定一个非严格递增序列,找到两段长度和最大,且不交的子序列,并且使得每个子序列都满足极差不严格大于给定参数 \(k\)。(程序要求输出长度和,不要求输出子序列)

\(\mathtt{Solution}\)

你可能有一个问题想问,为什么是给定一个非严格递增序列?题目中明明是乱序随便给的啊。其实你会发现,钻石的顺序并不影响答案,但是将钻石排序可以将题目的“随便抽着放”转化为“连续子序列”。

其实很好理解,因为如果价值是 \(a\) 的钻石和价值是 \(b(a<b)\) 的钻石能放在一个架子上,那么价值为 \(k\) 且满足 \(a \le k \le b\) 的钻石一定能放到这个架子上。因此排序后,如果两个钻石能放在架子上,那么中间的全能放。显然中间只要有一个不放就比中间全放的答案劣,因此要抽我们就要抽一个连续子序列。

懒得加粗提示了,自选重点理解吧(

理解完后我们继续:

既然题目要求子序列不可相交,前者的子序列和后者的子序列中间一定有一个分割线,我们规定这个分割线不能在后面那个序列的首,但是可以在前面那个序列的尾

采用dp思想。

定义两个数组 \(l, r\)

  • \(l_i\) 表示第 \(i\) 个数及其左边的数中,满足极差不严格大于 \(k\) 的最大子序列的长度。
  • \(r_i\) 表示第 \(i\) 个数及其右边的数中,满足极差不严格大于 \(k\) 的最大子序列的长度。

那么最终答案显然就是 \(\operatorname{max}^{n-1}_{i=1}l_i+r_{i+1}\)。问题是怎么求 \(l\)\(r\)

我们可以运用一种指针思想。我们以 \(l\) 数组举例吧,\(r\) 只是换汤不换药罢了。

首先先设初始值:\(l_1 = 1\)(自己成序列)。

设置两个指针 \(i\)\(j\)\(i\)\(2 \rightarrow n\),而 \(j\) 不断从序列首更新,每次 \(i\) 变化时,\(j\) 就不断自增,直到 \(a_i - a_j > k\)。此时 \(i \sim j\) 区间长度 和 \(l_{i-1}\) 的最大值就是 \(l_i\)。(非常具有dp的味道!)

同理求 \(r_i\),最后求 \(\operatorname{max}^{n-1}_{i=1}l_i+r_{i+1}\)

核心算法时间复杂度是 \(\mathcal{O}(n)\),但是排序的时间复杂度已经达到了 \(\mathcal{O}(n\log n)\),因此整个代码的时间复杂度是 \(\mathcal{O}(n\log n)\)

\(\mathtt{Code}\)

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2020-10-21 22:39:05 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2020-10-21 22:47:17
 */
#include <iostream>
#include <cstdio>
#include <algorithm>

const int maxn = 50005;
int a[maxn], l[maxn], r[maxn];

int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int n, k;
    std :: scanf("%d %d", &n, &k);
    for (int i = 1; i <= n; ++i)
        std :: scanf("%d", &a[i]);

    l[1] = r[n] = 1;
    std :: sort(a + 1, a + n + 1);
    for (int i = 2, j = 1; i <= n; ++i) {
        while (a[i] - a[j] > k)
            ++j;
        l[i] = max(l[i - 1], i - j + 1);
    }

    for (int i = n - 1, j = n; i >= 1; --i) {
        while (a[j] - a[i] > k)
            --j;
        r[i] = max(r[i + 1], j - i + 1);
    }

    int ans = 0;
    for (int i = 1; i <= n; ++i)
        ans = max(ans, l[i] + r[i + 1]);
    std :: printf("%d\n", ans);
    return 0;
}

\(\mathtt{More}\)

此题运用了双指针思想,i在前面跑,j在后边追,而整个序列又是单调性,因此时间复杂度就能大大简化。你看本题,核心双指针时间复杂度甚至比前置工作的复杂度还要低!

所以如果i在前面跑,保证j不会以后退的方式更“可行”,那么就可以使用双指针了。

posted @ 2020-10-23 23:29  东北小蟹蟹  阅读(59)  评论(0编辑  收藏