时之魔法(bitset优化背包)

NOIP2021 前倒数第二场模拟赛 T3。

题目内容

雪菜有一个 \(1\sim n\) 的排列 \(p\)保证 \(n\) 是偶数。现在她将排列前 \(\frac{n}{2}\) 个数和后 \(\frac{n}{2}\) 个数分成两个序列,然后执行如下操作:

  • 若两个序列均是空序列,则停止操作;
  • 若有且仅有一个是空序列,则选择另一个序列开头的数,将他删去,放到序列 \(P\) 的末尾;
  • 若两个序列均非空,则比较两个序列开头的数的大小,选择最小的数,将它删去,放到序列 \(P\) 的末尾。

可以发现,序列 \(P\) 也是一个排列。

现在雪菜有 \(T\) 组数据,每次给出 \(n\) 和排列 \(P\),她想知道对于 \(n!\) 个排列,是否有排列能通过上述操作得到排列 \(P\)。若可以,输出 Yes,否则输出 No

\(1\le T\le 100,1\le n\le 3\times 10^4,1\le \sum n\le 6\times 10^4\)

解题思路

暴力分其实还是很多的,\(n\le 8\) 爆搜、\(P_i=i\) 输出 Yes,怎么也能糊到 30 分。

关注 \(n\le 2000\) 的部分分。我们考虑把给定的排列 \(P\) 推回原来的两个序列(设为 \(a,b\)),以样例的第 3 组数据为例:

1 2 3 10 5 6 7 8 4 9 11 12

前面的 1 2 3 10 似乎可以随便放,而 5 6 7 8 4 9 这一段就不行。如果 10 放在序列 \(a\) 中而它们在序列 \(b\) 里,那它们势必会赶在 10 前面出去,只好把它们放在 10 的后面。

等等!如果这样做,就会形成一个长度为 7 且不可分割的大段,无论我们怎样调整其它元素的位置,也满足不了构成两个长度为 \(\frac{n}{2}\) 序列的要求。所以,这组数据的答案是 No

这提示了我们:“大段”的实质是两个前缀 \(\max\) 之间的距离。我们要做的就是:把每两个前缀 \(\max\) 之间的距离处理出来(得到一堆数),然后用这堆数凑成两个 \(\frac{n}{2}\)!这是一个经典的 01 背包问题,时间复杂度 \(\mathcal{O(n^2)}\)

这个 01 背包可以用 bitset 加以优化,达到 \(\mathcal{O(\frac{n}{w})}\)

bitset 优化对付的问题主要是只需要判断方案是否存在(毕竟它只有 0 或 1)的情况。可以直接设 \(dp_i\) 表示和为 \(i\) 的情况是否存在,边界为 \(dp_0=1\)。然后……前方高能

for i := 1 to n do
	dp |= dp << v[i];

这就是 dp 方程!考虑它为什么是对的:对于 \(dp_i\) 来说,我们加入了 \(v_i\),使得它可以转移到 \(dp_{i+v_i}\),这在 bitset 上表现为左移运算,然后通过或运算更新即可。(而且它还自带一个“滚动数组”的 buff

感觉确实是【模板:bitset 优化背包】啊……下面是 AC 代码:

//为了简洁,砍掉了多测部分
int n, top = 0;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
	scanf("%d", &p[i]);
	if (p[i] > p[st[top]]) st[++top] = i;//存前缀 max 的位置
}
st[++top] = n + 1;
dp.reset(), dp[0] = 1;
for (int i = 1; i < top; ++i) dp |= (dp << st[i + 1] - st[i]);
puts(dp[n / 2] ? "Yes" : "No");//能否凑出 n/2

THE END

posted @ 2021-11-16 16:08  q0000000  阅读(357)  评论(0编辑  收藏  举报