1 条题解

  • 0
    @ 2025-8-24 21:56:04

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar OMG_wc
    幻想家协会会长

    搬运于2025-08-24 21:56:04,当前版本为作者最后更新于2020-08-13 15:39:09,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    写一个比较自然的题解,不需要利用什么性质。分别给出了用树状数组、线段树实现的方法,时间复杂度皆为 O(nlogn)O(n\log n)

    由数据范围 0Ain10\le A_i\le n-1,我们想到枚举每个种类的数,分别计算贡献。

    对当前选中的数 ww,设 SiS_i 为前 ii 个数中 ww 的个数 ,那么对于一段区间 [l+1,r][l+1,r]ww 能成为绝对众数(数量严格超过一半的众数)的条件是 SrSl>rl(SrSl)S_r-S_l>r-l-(S_r-S_l)

    移项得到 2Srr>2Sll2S_r-r>2S_l-l,那问题就变成了对每个 rr ,统计 l[0,r1]l\in[0,r-1] 的范围内有多少满足 2Srr>2Sll2S_r-r>2S_l-l。(可以发现, 2Sii2S_i-i 其中就是前缀里 ww 的个数减去非 ww 的个数)如果用树状数组来维护每个 2Sii2S_i-i 的个数,时间复杂度度 O(n2log(n))O(n^2log(n)) ,只能骗部分分。当然有些细节要注意,比如 2Sii2S_i-i 的值域范围是 [n,n][-n,n] ,你需要加个 n+1n+1 的偏移量才能用数据结构维护。

    为了方便叙述,记 Pi=2SiiP_i=2S_i-i

    然后观察对某个选中的数 ww,其 PiP_i 的变化情况。可以发现如果有 kkww,那么可以分成 k+1k+1 个区间(以 00 位置及每个 ww 为开头到下个 ww 之前的数为止),每个区间内部的 PiP_i 值都是公差为 1-1 的等差数列。

    举个例子:

    这样如果我们能用某种方法把每段里的数同时处理,那么总共需要处理的段数就是 O(n)O(n) 的了。

    具体来说,假设当前这段的等差数列是 y,y1,y2,,xy,y-1,y-2,\dots ,x ,用数据结构来维护权值 cic_i,那么也就是对区间 [x,y][x,y]11

    并且可以发现同一段是不会有任何贡献的(因为是递减的),只需查询前面所有段的 PiP_i 值带来的影响,所以在更新操作之前,我们应该先做查询,

    TiT_i 表示权值的前缀和,即 Ti=j=1icjT_i=\sum\limits_{j=1}^{i}c_j。对段内每个位置的 PiP_i ,我们得到的贡献是 TPi1T_{P_i-1}。也就是说,对整个段内 [x,y][x,y] 这些数,总贡献是 i=x1y1Ti\sum\limits_{i=x-1}^{y-1}T_i。记 GiG_i 表示权值的前缀和的前缀和,即 Gi=j=1iTjG_i=\sum\limits_{j=1}^{i}T_j,那么总贡献可以表示为 Gy1Gx1G_{y-1}-G_{x-1}

    现在问题简化为区间加一个数和求二阶前缀和,下面分别用树状数组和线段树解决这个问题:


    树状数组

    想必你用树状数组做过 “区间加一个数,区间求和”的问题,本质单点更新,是求二阶前缀和。

    事实上,使用 nn 个树状数组,可以实现单点更新,查询 nn 阶前缀和:

    比如 n=3n=3 时,

    ccbb 的前缀和, bbaa 的前缀和,aadd 的前缀和,即 ccdd 的三阶前缀和,那么有

    $$\begin{aligned} &c_x=\sum\limits_{i=1}^{x} b_i\\ &=\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i} a_j\\ &=\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}\sum\limits_{k=1}^{j}d_k\\ &=\sum\limits_{k=1}^{x}\frac{(x+2-k)(x+1-k)}{2}d_k\\ &=\frac{(x+2)(x+1)}{2}\sum\limits_{k=1}^{x}d_k-\frac{2x+3}{2}\sum\limits_{k=1}^{x}d_k\cdot k+\frac{1}{2}\sum\limits_{k=1}^{x}d_k\cdot k^2 \end{aligned} $$

    对这题而言,现在区间更新可以转换为差分数组 dd 的两个单点更新,原数组的二阶前缀和变为差分数组 dd 的三阶前缀和。

    那么只要用三个树状数组维护 di,di×i,di×i2d_i,d_i\times i,d_i\times i^2 即可,完整代码如下:

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    const int INF = 0x3f3f3f3f;
    const LL mod = 1e9 + 7;
    const int N = 500005;
    // 修改差分 来维护前缀和的前缀和
    // c1 为差分d c2为d*i  c3 为d*i*i
    LL c1[N * 2], c2[N * 2], c3[N * 2];
    LL sum(int x) {
        LL res = 0;
        for (int i = x; i > 0; i -= i & -i) {
            res += c1[i] * (x + 2) * (x + 1) - c2[i] * (2 * x + 3) + c3[i];
        }
        return res / 2;
    }
    void add(int x, LL d, int n) {
        for (int i = x; i <= n; i += i & -i) {
            c1[i] += d;
            c2[i] += d * x;
            c3[i] += d * x * x;
        }
    }
    int a[N];
    vector<int> b[N];
    int main() {
        int n;
        scanf("%d%*d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i]);
            b[a[i]].push_back(i);
        }
        const int wc = n + 1;  // 偏移量,把[-n,n] 平移到 [1,2n+1]
        LL ans = 0;
        for (int i = 0; i < n; i++) {
            b[i].push_back(n + 1);
            int last = 0;
            for (int j = 0; j < b[i].size(); j++) {
                int y = 2 * j - last + wc, x = 2 * j - (b[i][j] - 1) + wc;
                // 查询 sum([1,t-1] 的权值和), 其中t在[x,y]范围内,
                ans += sum(y - 1) - (x >= 3 ? sum(x - 2) : 0);
                // [x,y] 这些数的权值+1
                add(x, 1, 2 * n + 1);
                add(y + 1, -1, 2 * n + 1);
                last = b[i][j];
            }
            last = 0;
            for (int j = 0; j < b[i].size(); j++) {
                int y = 2 * j - last + wc, x = 2 * j - (b[i][j] - 1) + wc;
                add(x, -1, 2 * n + 1);
                add(y + 1, 1, 2 * n + 1);
                last = b[i][j];
            }
        }
        printf("%lld\n", ans);
        return 0;
    }
    

    线段树

    其实也可以用两棵线段树来求二阶前缀和,但这就没有意思了,和树状数组维护三阶前缀和没区别。

    我的方法是用线段树维护权值的前缀和 TiT_i ,这样在 [x,y][x,y] 上的区间加 11 就变成了:在 [x,y][x,y] 上加等差数列 1,2,3,,yx+11,2,3,\ldots,y-x+1,在 [y+1,2n+1][y+1,2n+1] 上加上 yx+1y-x+1。后者也可看成是公差为 00 的等差数列。

    那么线段树怎么维护等差数列呢?

    我们可以发现任意个等差数列的和都是等差数列,只是公差和首项发生了变化。

    那么只要把公差和首项作为延迟标记,来维护区间和就好了,具体实现见代码。

    因为维护的是 TiT_i ,查询也只是简单的区间查询了,注意一些边界问题就OK了。

    完整代码如下:

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    const int INF = 0x3f3f3f3f;
    const LL mod = 1e9 + 7;
    const int N = 500005;
    
    // sx,gc 表示首项和公差
    LL val[N << 3], sx[N << 3], gc[N << 3];
    inline void push_up(int u) {
        val[u] = val[u << 1] + val[u << 1 | 1];
    }
    inline void gx(int u, LL v, LL d, int len) {
        val[u] += (v + v + (len - 1) * d) * len / 2;
        sx[u] += v;
        gc[u] += d;
    }
    inline void push_down(int u, int len1, int len2) {
        if (sx[u] || gc[u]) {
            gx(u << 1, sx[u], gc[u], len1);
            gx(u << 1 | 1, sx[u] + gc[u] * len1, gc[u], len2);
            sx[u] = gc[u] = 0;
        }
    }
    void update(int u, int l, int r, int x, int y, int v, int d) {
        if (x <= l && r <= y) {
            gx(u, v + (l - x) * d, d, r - l + 1);
            return;
        }
        int mid = l + r >> 1;
        push_down(u, mid - l + 1, r - mid);
        if (x <= mid) update(u << 1, l, mid, x, y, v, d);
        if (y > mid) update(u << 1 | 1, mid + 1, r, x, y, v, d);
        push_up(u);
    }
    LL query(int u, int l, int r, int x, int y) {
        if (x <= l && r <= y) {
            return val[u];
        }
        int mid = l + r >> 1;
        push_down(u, mid - l + 1, r - mid);
        LL res = 0;
        if (x <= mid) res += query(u << 1, l, mid, x, y);
        if (y > mid) res += query(u << 1 | 1, mid + 1, r, x, y);
        return res;
    }
    int a[N];
    vector<int> b[N];
    int main() {
        int n;
        scanf("%d%*d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i]);
            b[a[i]].push_back(i);
        }
        const int wc = n + 1;  // 偏移量,把[-n,n] 平移到 [1,2n+1]
        LL ans = 0;
        for (int i = 0; i < n; i++) {
            b[i].push_back(n + 1);
            int last = 0;
            for (int j = 0; j < b[i].size(); j++) {
                int y = 2 * j - last + wc, x = 2 * j - (b[i][j] - 1) + wc;
                // 查询 sum([1,t-1] 的权值和), 其中t在[x,y]范围内
                ans += query(1, 1, 2 * n + 1, max(x - 1, 1), y - 1);
                // [x,y] 这些数的权值+1,真正要维护的是它们的前缀和,
                // 在[x,y] 上是加上一段等差数列 1,2,3,...,y-x+1,[y+1,2n+1] 上每个数+y-x+1
                update(1, 1, 2 * n + 1, x, y, 1, 1);
                if (x + 1 <= 2 * n + 1) update(1, 1, 2 * n + 1, y + 1, 2 * n + 1, y - x + 1, 0);
                last = b[i][j];
            }
            last = 0;
            for (int j = 0; j < b[i].size(); j++) {
                int y = 2 * j - last + wc, x = 2 * j - (b[i][j] - 1) + wc;
                update(1, 1, 2 * n + 1, x, y, -1, -1);
                if (x + 1 <= 2 * n + 1) update(1, 1, 2 * n + 1, y + 1, 2 * n + 1, -(y - x + 1), 0);
                last = b[i][j];
            }
        }
        printf("%lld\n", ans);
        return 0;
    }
    

    最后,同样时间复杂度是 O(nlogn)O(n\log n),但是线段树跑的时间是树状数组的三倍。

    • 1

    信息

    ID
    3024
    时间
    4000ms
    内存
    500MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者