2025.03.06 CW 模拟赛 C. 列表
C. 列表
原题链接.
思路
我们考虑取数的过程: 第一次一定会取 \(a_{n + 1}\), 然后我们会在 \([1, n]\) 或 \([n + 2, 2n + 1]\) 的区间中选择一个数删除. 如果我们在 \([1, n]\) 中选一个数删除, 那么下一个数会取到 \(a_{n + 2}\), 否则我们会取到 \(a_{n}\).
不难发现 \(a_{n + 1}\) 其实是一个分界点, 如果我们在其左边删数, 那么下一次我们选的数就是右边的第一个数; 否则我们在其右边删数, 那么下一次我们选的数就是左边最后一个数.
抽象化一下这个过程, 我们将左右两边分别放在栈里进行维护. 具体的, 我们将左半部分从 1 到 \(n\) 依次压入栈中, 将右半部分从 \(2n + 1\) 到 \(n + 2\) 依次压入栈中. 描述一下上面的操作: 我们每次选择其中一个栈的栈顶加入集合, 再在另一个栈中删除一个数.
我们可以枚举集合最终连续的区间 \([l, r]\), 思考如何判断区间是否合法. 发现我们只关心 \(a_i\) 是否在 \([l, r]\) 内, 所以我们钦定如果 \(a_i \in [l, r]\) 那么 \(a_i = 1\), 否则 \(a_i = 0\).
设两栈分别为 \(A, B\), 我们可以得出一个贪心策略:
- 两个栈顶都是 0. 任意选择一个加入集合, 另一个删除即可.
- 其中一个是 1. 将 1 加入集合, 另一个 0 删除即可.
- 两个都是 1. 将 \(A\) 栈顶的 1 加入集合, 删除 \(B\) 中最靠近栈顶的 0; 再将 \(B\) 栈顶的 1 加入集合, 删除 \(A\) 中最靠近栈顶的 0.
前两个比较显然, 我们考虑第 3 种情况. 对于操作 1, 发现我们会浪费一个 0 来和 1 进行「配对」, 所以我们要将 1 尽量靠近栈顶. 换句话说, 我们要删除最靠近栈顶的 0. 贪心得证.
直接枚举区间是 \(\mathcal{O}(n^2)\) 的. 但是我们发现如果区间 \([l, r]\) 合法, 那么其子区间 \([l - 1, r], [l, r - 1]\) 同样合法, 所以我们可以双指针将枚举做到 \(\mathcal{O}(n)\).
继续发掘性质, 发现我们判断的实质就是判断对于 \(A, B\) 中每一个 1 是否都可以与一个下标大于它的 0 进行配对. 所以区间 \([l, r]\) 有解当且仅当: 对于每个 1 向另一个栈中的下标大于它的连边, 会得到两个二分图, 并且两个二分图均有完美匹配. 从而, \([l, r]\) 合法的充要条件是: 每个后缀中, \(A\) 中 1 的个数不超过 \(B\) 中 0 的个数且 \(B\) 中 1 的个数不超过 \(A\) 中 0 的个数. 发现其中一个条件满足另一个就必定满足, 所以接下来我们只讨论第一个.
令 \(b_i\) 表示 \([i, n]\) 中 1 的数量减去 \([i + n + 1, 2n + 1]\) 中 0 的数量. 我们在双指针移动的过程中顺便维护并修改前缀即可, 用线段树做到 \(\mathcal{O}(n \log n)\). 不合法当且仅当 \(\exists b_i < 0\).
代码
#include "iostream"
using namespace std;
char buf[1 << 20], *p1, *p2;
#define getchar() (p1 == p2 and (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), p1 == p2) ? 0 : *p1++)
void read() {}
template <class T, class ...T1>
void read(T &x, T1 &...y) {
x = 0;
char ch = getchar(); bool f = 0;
for (; ch < '0' or ch > '9'; ch = getchar()) f |= ch == '-';
for (; ch >= '0' and ch <= '9'; x = x * 10 + (ch & 15), ch = getchar());
x = (f ? -x : x);
read(y...);
}
constexpr int N = 2e5 + 1;
int n, m, pos[N << 1];
int P = 1, mn[N * 3], sum[N * 3];
void build() {
while (P <= n + 1) P <<= 1;
for (int i = 1; i <= n; ++i) mn[P + i] = n - i + 1;
for (int i = P - 1; i; --i) mn[i] = min(mn[i << 1] ? mn[i << 1] : 1e9, mn[i << 1 | 1] ? mn[i << 1 | 1] : 1e9);
}
void update(int l, int r, int k) {
l += P - 1, r += P + 1;
while (l ^ 1 ^ r) {
if (~l & 1) mn[l ^ 1] += k, sum[l ^ 1] += k;
if (r & 1) mn[r ^ 1] += k, sum[r ^ 1] += k;
l >>= 1, r >>= 1;
mn[l] = min(mn[l << 1], mn[l << 1 | 1]) + sum[l];
mn[r] = min(mn[r << 1], mn[r << 1 | 1]) + sum[r];
}
for (l >>= 1; l; l >>= 1) mn[l] = min(mn[l << 1], mn[l << 1 | 1]) + sum[l];
}
void update(int u, int k) {
int r = pos[u] > n ? pos[u] - n - 1 : n - pos[u] + 1;
if (r >= 1 and r <= n) update(1, r, k);
}
void init() {
read(n), m = n << 1 | 1;
build();
for (int i = 1, x; i <= m; ++i)
read(x), pos[x] = i;
}
void calculate() {
int ans = 0;
for (int l = 1, r = 0; l <= m; ++l) {
while (r < m) {
update(++r, -1);
if (mn[1] < 0) {
update(r--, 1);
break;
}
}
ans = max(ans, r - l + 1);
update(l, 1);
}
printf("%d", ans);
}
void solve() {
init();
calculate();
}
int main() {
solve();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现