洛谷 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}\),则有:
这样,我们就说,当满足序列各项单调递增时,\(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\) 中有若干相同的项。消去这些项,可知原题等价于
由 \(p_{k - 1} \le p_{k} \le p_{k + 1} \le p_{k + 2}\),知绝对值符号内各项均为非负。
直接打开绝对值符号,即要证
显然成立,取等条件是 \(p_{k + 1} = p_k\)。
对于 \(p_{n + 1}\) 的情况,可以等同于 \(p_1\) 的情况处理。
由此我们知道:对于一个单调递增的序列,交换其中的两项,不可能把答案变小(因为存在相等的情况)。
这样,决策的正确性得以证明,应用贪心一路无阻。问题实际上转化为:维护一个数据结构,每次询问添加或删除一个元素,同时能够回答所有元素的最大值和最小值之差的二倍。
本题中,\(n,q \le {10}^6\),每次询问时直接枚举,复杂度 \(\mathcal{O}(qn)\),超时。
注意到值域限制,我们定义一个桶 tot[1000005]
来维护元素,并使用变量 maxn
和 minn
维护最大和最小值。每次添加时,假设新增元素为 \(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;
}