在相思树下 III 题解
前言
题目链接:洛谷。
赛时脑子坨成一坨了,估计是 T1 的影响,写一篇题解来理清思路。
题意简述
给你一个长为 \(n\) 的序列 \(a_{1\dots n}\),你需要对它进行两种操作共 \(n-1\) 次。
对一个长度为 \(l\) 的序列 \(b_{1\dots l}\) 进行一次操作将会把序列变为一个长为 \(l-1\) 的序列 \(c_{1\dots l-1}\):
- 操作一中,\(\forall i\in[1,l),c_i=\max(b_i,b_{i+1})\);
- 操作二中,\(\forall i\in[1,l),c_i=\min(b_i,b_{i+1})\)。
给定整数 \(m\),你只能进行至多 \(m\) 次操作一。进行 \(n-1\) 次操作后序列 \(a\) 的长度变为 \(1\)。你可以任意安排操作的顺序,求最终剩余的数 \(a_1\) 的最大值。
题目分析
方法 \(1\):二分 + 差分
很容易想到二分,把序列中 \(\geq mid\) 的统统看成 \(1\),反之是 \(0\),问题变成了最后留下的那一个数是不是 \(1\)。
我们不需要考虑 \(1\) 之间,\(0\) 之间的相互操作,应为无论是 \(\min\) 或 \(\max\),\(\geq mid\) 的永远不会 \(< mid\),反之亦然。所以,我们把序列分成若干极长的 \(\tt 01\) 段,考虑这两个操作对两段交界处的影响。
对于取 \(\max\),会把 \(\tt 01\) 变成 \(\tt 11\);对于取 \(\min\),会把 \(\tt 10\) 变成 \(\tt 00\)。由于我们关心 \(1\),所以,前者是把所有极长的 \(1\) 段左边界向左扩展,后者是对所有极长的 \(1\) 段右边界向左收缩。
考虑扩展和收缩的先后顺序。在答案的操作序列中,对于相邻的两个操作,把先收缩再扩展的,变成先扩展再收缩一定不劣。原因是,先收缩可能会完全消除某一个长度为 \(1\) 的段,但是后者不会。邻项交换证明得,答案就是先扩展,再收缩。显然,多用扩展一定不劣。所以,我们确定了答案的操作序列。
先扩展 \(m\) 次,再收缩 \(n - m - 1\) 次,相当于我们对于初始的极长 \(1\) 段的左端点 \(l\),把 \([l - m, l - 1]\) 覆盖成 \(1\),最后看看 \([1, n - m]\) 是否均为 \(1\)。这些操作直接差分就行了。因为我们只用知道那些地方一次都没被覆盖过。
时间复杂度:\(\Theta(n \log V)\),当然离散化后 \(V = \mathcal{O}(n)\)。
方法 \(2\):单调队列
能否优化呢?我们可以从另一种思路来分析。感觉是贪心,考虑答案操作序列的相邻两个操作,是先取 \(\max\) 还是先取 \(\min\)。不妨列出式子,对于之前连续的三个数 \(a, b, c\),操作两次后得到的 \(a'\) 分别为 \(\max \lbrace \min \lbrace a, b \rbrace, \min \lbrace b, c \rbrace \rbrace\),\(\min \lbrace \max \lbrace a, b \rbrace, \max \lbrace b, c \rbrace \rbrace\)。然后凭借你的数学直觉……额,看不出来……没关系!枚举 \(1 \sim 3\) 的全排列来看看:
\(a, b, c\) | 先 \(\min\) 再 \(\max\) | 先 \(\max\) 再 \(\min\) |
---|---|---|
\(1, 2, 3\) | \(\max \Big\lbrace \min \lbrace 1, 2 \rbrace, \min \lbrace 2, 3 \rbrace \Big\rbrace = 2\) | \(\min \Big\lbrace \max \lbrace 1, 2 \rbrace, \max \lbrace 2, 3 \rbrace \Big\rbrace = 2\) |
\(1, 3, 2\) | \(\max \Big\lbrace \min \lbrace 1, 3 \rbrace, \min \lbrace 3, 2 \rbrace \Big\rbrace = 2\) | \(\min \Big\lbrace \max \lbrace 1, 3 \rbrace, \max \lbrace 3, 2 \rbrace \Big\rbrace = 3\) |
\(2, 1, 3\) | \(\max \Big\lbrace \min \lbrace 2, 1 \rbrace, \min \lbrace 1, 3 \rbrace \Big\rbrace = 1\) | \(\min \Big\lbrace \max \lbrace 2, 1 \rbrace, \max \lbrace 1, 3 \rbrace \Big\rbrace = 2\) |
\(2, 3, 1\) | \(\max \Big\lbrace \min \lbrace 2, 3 \rbrace, \min \lbrace 3, 1 \rbrace \Big\rbrace = 2\) | \(\min \Big\lbrace \max \lbrace 2, 3 \rbrace, \max \lbrace 3, 1 \rbrace \Big\rbrace = 3\) |
\(3, 1, 2\) | \(\max \Big\lbrace \min \lbrace 3, 1 \rbrace, \min \lbrace 1, 2 \rbrace \Big\rbrace = 1\) | \(\min \Big\lbrace \max \lbrace 3, 1 \rbrace, \max \lbrace 1, 2 \rbrace \Big\rbrace = 2\) |
\(3, 2, 1\) | \(\max \Big\lbrace \min \lbrace 3, 2 \rbrace, \min \lbrace 2, 1 \rbrace \Big\rbrace = 2\) | \(\min \Big\lbrace \max \lbrace 3, 2 \rbrace, \max \lbrace 2, 1 \rbrace \Big\rbrace = 2\) |
容易发现,由于大的数在取最值得时候没取到,前者是不优的。这也恰好对应了我们方法 \(1\) 中的结论。
所以,依然得出了先弄 \(m\) 次操作一,再弄 \(n - m - 1\) 次操作二。
在前 \(m\) 次操作后,对于一个数 \(a_i\),它最终的值便是 \(\max \lbrace a_{i \dots i + m} \rbrace\),使用 ST 表,线段树都行,或者滑动窗口用单调队列。对于后 \(n - m - 1\) 次操作同理,不过我们只要知道 \(a_1\),所以求一遍 \(a_{1 \dots n - m}\) 的 \(\min\) 即可。
时间复杂度:\(\Theta(n)\)。
代码
方法 \(1\):二分 + 差分
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 1000010;
int n, m, val[N];
int yzh[N];
inline bool check(int x) {
for (int i = 1; i <= n; ++i) yzh[i] = (val[i] >= x) - (val[i + 1] >= x);
for (int i = n; i >= 1; --i) {
if (val[i] >= x && val[i - 1] < x)
--yzh[max(0, i - m - 1)], ++yzh[i - 1];
yzh[i] += yzh[i + 1];
}
for (int i = 1; i <= n - m; ++i) if (yzh[i] <= 0) return false;
return true;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", &val[i]);
int l = 1, r = 1e9, ans = 736520;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid)) ans = mid, l = mid + 1;
else r = mid - 1;
}
printf("%d", ans);
return 0;
}
方法 \(2\):单调队列
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1000010;
inline void read(int &x) {
x = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar());
for (; isdigit(ch); x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar());
}
int n, m, val[N];
int ans = 0x3f3f3f3f;
int Q[N], head, tail;
signed main() {
read(n), read(m);
for (register int i = 1; i <= n; ++i) read(val[i]);
head = 0, tail = -1;
for (register int i = n; i >= 1; --i) {
while (head <= tail && Q[head] > i + m) ++head;
while (head <= tail && val[Q[tail]] <= val[i]) --tail;
Q[++tail] = i;
if (i <= n - m) ans = min(ans, val[Q[head]]);
}
printf("%d", ans);
return 0;
}
后记
遇到贪心操作的题目,考虑答案的操作序列,然后对这个操作序列邻项交换。
二分答案,能够成为答案的和不能成为答案的,看做 \(1\) 和 \(0\),分别考虑。
本文作者:XuYueming,转载请注明原文链接:https://www.cnblogs.com/XuYueming/p/18364514。
若未作特殊说明,本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。