[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 ightarrow 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}(nlog n)),因此整个代码的时间复杂度是 (mathcal{O}(nlog 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
", ans);
    return 0;
}

(mathtt{More})

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

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

原文地址:https://www.cnblogs.com/crab-in-the-northeast/p/luogu-p3143.html