Loading

洛谷 P8939 B. 去年 11 月卵梦蕾简易钨丝 题解

一道有趣的贪心题目。

题意简述:

给定序列 \(\{a_n\}\),值域为 \(w\),支持两种形如 opt x 操作共 \(q\) 次:
1 x:删除一个数 \(x\),若序列中没有 \(x\),则输出 −1,并跳过本次操作,若有多个 \(x\),则仅删除一个。
2 x:向序列中插入一个数 \(x\)
对于每个未被跳过的操作,试求出 \(a\) 的一个排列 \(p\),最小化 \(\sum \limits_{i=1}^{n} \lvert p_{i+1}-p_i\rvert\) 的值,即最小化 \(\lvert p_2-p_1\rvert+\lvert p_3-p_2\rvert+\dots+\lvert p_{n+1}-p_n\rvert\) 的值,其中 \(p_{n+1}=p_1\)
保证任意时刻序列内至少有 \(1\) 个数。

先考虑最小化的决策问题,观察式子 \(\lvert p_2-p_1\rvert+\lvert p_3-p_2\rvert+\dots+\lvert p_{n+1}-p_n\rvert\)(下记为 \(S\)),这里的若干个绝对值符号给了我们一些思路,我们设想一种特殊情况:

若满足 \(\forall i \in [1, n - 1]\),都有 \(p_{i + 1} \ge p_{i}\),则有:

\[\begin{aligned} S & = p_2 - p_1 + p_3 - p_2 + \dots + p_{k} - p_{k - 1} + p_{k + 1} - p_{k} + \dots + p_n - p_{n - 1} - p_{n + 1} + p_n\\ & = p_n - p_1 + p_n - p_{n + 1}\\ & = 2 (p_n - p_1) \end{aligned}\]

这样,我们就说,当满足序列各项单调递增时,\(S\) 的值为序列中最大值与最小值之差的两倍。

自然,一个投机的想法就是,当序列恰好单调递增时,原式 \(S\) 取得最小值。这就要求我们证明:

设原序列的某个排列依次有(这些数位置上不一定连续) \(\{p_{k - 1}, p_{k}, p_{k + 1}, p_{k + 2}\}\),且满足 \(p_{k - 1} \le p_{k} \le p_{k + 1} \le p_{k + 2}\),调换 \(p_k\)\(p_{k + 1}\) 的位置,记调换前的式子值为 \(S_1\),调换后的式子为 \(S_2\)。求证:\(S_2 \ge S_1\)

证明此题并不复杂,下面给出笔者的拙劣过程:

\(S_1\)\(S_2\) 中有若干相同的项。消去这些项,可知原题等价于

\[\lvert p_{k + 1} - p_{k - 1} \rvert + \lvert p_{k + 2} - p_k \rvert \ge \lvert p_{k + 2} - p_{k + 1} \rvert + \lvert p_k - p_{k - 1} \rvert \]

\(p_{k - 1} \le p_{k} \le p_{k + 1} \le p_{k + 2}\),知绝对值符号内各项均为非负。
直接打开绝对值符号,即要证

\[p_{k + 2} - p_{k - 1} + p_{k + 1} - p_k \ge p_{k + 2} - p_{k - 1} - p_{k + 1} + p_k \]

显然成立,取等条件是 \(p_{k + 1} = p_k\)

对于 \(p_{n + 1}\) 的情况,可以等同于 \(p_1\) 的情况处理。

由此我们知道:对于一个单调递增的序列,交换其中的两项,不可能把答案变小(因为存在相等的情况)

这样,决策的正确性得以证明,应用贪心一路无阻。问题实际上转化为:维护一个数据结构,每次询问添加或删除一个元素,同时能够回答所有元素的最大值和最小值之差的二倍。

本题中,\(n,q \le {10}^6\),每次询问时直接枚举,复杂度 \(\mathcal{O}(qn)\),超时。

注意到值域限制,我们定义一个桶 tot[1000005] 来维护元素,并使用变量 maxnminn 维护最大和最小值。每次添加时,假设新增元素为 \(x\),只需执行 tot[x]++ 即可。但面对删除操作时,这个桶就可能失效。假设此时有元素 \(1\)\(1000000\),反复删除和添加元素 \(1\)\(500000\) 次,复杂度仍然是 \(\mathcal{O}(qn)\),应当会超时,但数据比较友好,笔者的程序仍能通过本题。

hack 数据生成:

#include <bits/stdc++.h>
using namespace std;
int main()
{
    freopen("hack.in", "w", stdout);
    printf("2 1000000\n");
    printf("1 1000000\n");
    for(int i = 1; i <= 500000; i++)
    {
        printf("1 1\n");
        printf("2 1\n");
    }
    return 0;
}

笔者的程序(赛时提交记录):

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int tot[1000005], maxn = 0, minn = 0x7f7f7f7f, n, q, x;
inline ll read()
{
    ll x = 0, w = 1;
    char ch = 0;
    do
    {
        if(ch == '-')
            w = -1;
        ch = getchar();
    }   while(!isdigit(ch));
    do
    {
        x = (x << 3) + (x << 1) + ch - '0';
        ch = getchar();
    }   while(isdigit(ch));
    return x * w;
}
inline void print(ll x)
{
    static int s[35], d = 0;
    do
    {
        s[++d] = x % 10;
        x /= 10;
    }   while(x);
    do
    {
        putchar(s[d--] + '0');
    }   while(d);
    return;
}
int main()
{
    n = read(), q = read();
    for(int i = 1; i <= n; i++)
        x = read(), tot[x]++, maxn = max(maxn, x), minn = min(minn, x);
    for(int i = 1; i <= q; i++)
    {
        int op = read();
        if(op == 1)
        {
            x = read();
            if(tot[x])
            {
                tot[x]--;
                if(!tot[x])
                    if(maxn == x)
                        while(!tot[--maxn]);
                    else if(minn == x)
                        while(!tot[++minn]);
                print((maxn - minn) << 1);
                putchar('\n');
            }
            else
                printf("-1\n");
        }
        else
        {
            x = read(), tot[x]++, maxn = max(maxn, x), minn = min(minn, x);
            print((maxn - minn) << 1);
            putchar('\n');
        }
    }
    return 0;
}

我们当然不能止步于此。众所周知,堆能够以 \(\mathcal{O}(\log n)\) 的时间复杂度插入任意元素、删除堆顶元素,且能以 \(\mathcal{O}(1)\) 的时间复杂度查询最大或最小值,但不能方便地查询某个元素是否存在。为了克服这一弊端,我们可以同时维护一个桶和两个堆(分别维护最大值和最小值),时间复杂度为 \(\mathcal{O}((n + q)\log n)\),足以通过本题。

喜闻乐见的代码环节:

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int tot[1000005], n, q, x;
priority_queue <int, vector<int>, less<int> > q_max;
// 定义优先队列(即堆) q_max 维护最大值。
// "int" 指堆维护的变量类型,"vector<int>" 指堆的底层容器,less<int> 是比较方式(越小越后出堆)
priority_queue <int, vector<int>, greater<int> > q_min;

// 快读模板
inline ll read()
{
    ll x = 0, w = 1;
    char ch = 0;
    do
    {
        if(ch == '-')
            w = -1;
        ch = getchar();
    }   while(!isdigit(ch));
    do
    {
        x = (x << 3) + (x << 1) + ch - '0';
        ch = getchar();
    }   while(isdigit(ch));
    return x * w;
}
inline void print(ll x)
{
    static int s[35], d = 0;
    do
    {
        s[++d] = x % 10;
        x /= 10;
    }   while(x);
    do
    {
        putchar(s[d--] + '0');
    }   while(d);
    return;
}
int main()
{
    n = read(), q = read();
    for(int i = 1; i <= n; i++)
    {
        x = read(), tot[x]++;
        q_max.push(x);
        q_min.push(x);
    }

    for(int i = 1; i <= q; i++)
    {
        int op = read();
        if(op == 1) // 删除操作
        {
            x = read();
            if(tot[x])
            {
                tot[x]--;
                while(!tot[q_max.top()])
                    q_max.pop();    // 去除不存在的最值
                while(!tot[q_min.top()])
                    q_min.pop();
                print((q_max.top() - q_min.top()) << 1);
                putchar('\n');
            }
            else
                printf("-1\n"); // 元素不存在
        }
        else
        {
            x = read(), tot[x]++;
            q_max.push(x);
            q_min.push(x);
            print((q_max.top() - q_min.top()) << 1);
            putchar('\n');
        }
    }
    return 0;
}
posted @ 2023-01-16 15:55  escap1st  阅读(21)  评论(0编辑  收藏  举报