关于排序+单调性类问题的随笔

我们先来一个题解

题目链接:NC26251 小阳买水果

给定一个长度为 \(n\) 的数列 \(\{a_n\}\),尝试找出一个最长的连续子数列,使得其和大于零,并输出这个最长长度。(不存在则输出 \(0\)

\(1\leq n \leq 2*10^6,|a_i|\leq 10^3\)

朴素做法是 \(O(n^3)\) 的,用前缀和可以优化到 \(O(n^2)\)

我们不妨将前缀和数列进行排序(带着自己的下标一起),值相同的情况下,我们就按照下标从小到大排序。这样之后,前面的值必然小于后面的值,那么我们就可以用一个标记来记录前面最小的下标(原数组里面的下标),然后不断维护更新即可。

不过我们需要考虑一下相同值的情况,所以我们在代码里面导入了vector加离散化的搞法。

#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 2000010;
int n;
struct Node {
    int p;
    LL v;
    bool operator < (const Node &rhs) const {
        if (v == rhs.v) return p < rhs.p;
        return v < rhs.v;
    }
} s[N];
//
int tot;
vector<int> e[N];
int main()
{
    //read
    scanf("%d", &n);
    s[0].p = s[0].v = 0;
    for (int i = 1; i <= n; ++i) {
        LL v;
        scanf("%lld", &v);
        s[i].p = i, s[i].v = s[i - 1].v + v;
    }
    //solve
    sort(s, s + n + 1);
    tot = 0;
    e[++tot].push_back(s[0].p);
    for (int i = 1; i <= n; ++i) {
        if (s[i].v != s[i - 1].v) ++tot;
            e[tot].push_back(s[i].p);
    }
    int p = e[1][0], ans = 0;
    for (int i = 2; i <= tot; ++i) {
        ans = max(ans, e[i][e[i].size() - 1] - p);
        p = min(p, e[i][0]);
    }
    printf("%d\n", ans);
    return 0;
}

更加深入的研究:单调性+维护最大值问题

我们再来看一个题目:

对于一个数列 \(\{a_n\}\),尝试找到一个数对 \((i,j)\),要求满足 \(i<j,a_i<a_j\),并尝试输出 \(a_j-a_i\) 的最大值。

\(n\leq 10^6\)

如果暴力枚举的话,复杂度是 \(O(n^2)\),显然过不了。

我们可以考虑引入一手数据结构,对于 \(i\),每次都能够高效查询 \([1,i-1]\) 上面的最小值是多少,随后不断更新答案即可。使用线段树的话,可以将总复杂度压到 \(O(n\log n)\)

不过,我们显然犯不着写线段树:我们直接开一个变量来记录遍历到某一位时的最小值,处理完这一位之后,考虑将这一位的数更新为最小值即可,总复杂度 \(O(n)\),代码如下:

int ans = -INF, Min = a[1];
for (int i = 2; i <= n; ++i) {
    ans = max(ans, a[i] - Min);
    Min = min(Min, a[i]);
}

如果我们变换一下题目,改成求 \(j-i\) 的最大值呢?好像和上面的题目差不多了:对数列排个序,然后依次更新即可。

综上,上面的两个题目,分别属于这两个类型:

给定一个长度为 \(n\) 的数列 \(\{a_n\}\),要求满足 \(i<j,a_i<a_j\),并尝试求出:

  1. \(j-i\) 的最大值
  2. \(a_j-a_i\) 的最大值

显然,数列的下标天然满足单调性,而且两两不相同,所以对于问题 1,我们直接从前到后遍历一遍即可。但是问题二则不同,因为数列的值不一定满足单调性,而且常常有重复值,所以我们需要排序(记录下标的那种),还要顺带处理好重复元素的问题。

带限制下的求解(随手写的

这类问题一般是处理 \(a_j-a_i\) 的,限制的是下标,例如著名的求最大平均值:

给定一个长度为 \(n\) 的数列 \(\{a_n\}\),尝试求出平均数最大的一个区间,且要求区间长度不小于 \(L\)

\(1\leq L\leq n \leq 10^5\)

这是一个二分题,我们二分这个最大的平均数,然后将数列中的每一个数都减去它,然后看看数列中是否存在一个区间和大于等于零的子区间,存在的话则说明存在平均数大于等于它的区间。那么问题就变成了,在区间长度不小于 \(L\) 的情况下,找出区间和的可能的最大值。

其实加上这个限定条件也没啥:我们同样的先做一次前缀和,然后从下标 \(L\) 开始即可。代码很简单,此次不再赘述:

//这个代码仅供提供算法思路,真的想做上面那题,记得把LL改成double
LL s[N];
LL getMax(LL a[], int n, int L) {
    for (int i = 1; i <= n; ++i)
        s[i] = s[i - 1] + a[i];
    LL res = -INF, Min = INF;
    for (int i = L; i <= n; ++i) {
        Min = min(Min, s[i - L]);
        res = max(res, s[i] - Min);
    }
    return res;
}

这段代码里面,更新Minres的顺序颠倒了下,不过无伤大雅(随便改改就好,这个没啥定式)

posted @ 2021-09-08 20:35  cyhforlight  阅读(45)  评论(0编辑  收藏  举报