CF1326E 题解
为什么有那么多人喜欢把套路题叫成思维题,真是神奇。
在不知道套路的情况下,套路题便是思维题;如果能熟练运用套路,思维题便成了套路题罢。
这道题的套路是两种经典套路相乘。
- 对于一类二分题,我们可以在二分后把满足条件的设为 $1$ ,不满足的设为 $0$ 或 $-1$ (视情况而定),这时我们能很方便与统计/做前后缀和。
- 对于一类动态的前后缀和问题(或者上述问题),很多时候可以用线段树维护动态的。
我太菜了,第一种套路都没想到。
所以到底该怎么做呢?我们先从简单入手罢。假设我们已经确定了哪些位置有炸弹,那么可以套用第一种套路。就是去二分答案 $x$ ,这时候我们发现我们把大于 $x$ 的全部标为 $1$ ,表示可以删掉,不难发现,由于 $x$ 只要剩 $1$ 个,而且相同的权值把越前面置 $1$ 必定更优(那么后面就越可能有东西能删),那么我们把除最后一个外权值等于 $x$ 的也置 $1$ ,然后判断是否有前缀和小于等于 $0$ 即可。
由于这种做法非常的逊,所以我们考虑优化他(对每个数做一遍的时间复杂度为 $O(n^2 \log n)$ )。所以我们考虑优化掉一个 $n$ 的级别。这时也许还是被二分限制着,但是只要阅读了套路二,你将会豁然开朗。
由于有单调性所以能二分,而我们发现答案是单调递减的,所以呢我们发现在这样的性质下一个答案是合法的仅当有一个点右边的大于等于 $ans$ 的数大于炸弹数,这样必定会剩下至少一个没有被删,而因为答案是单调递减的所以答案一定是当前的。我们记录每种数值的位置(注意数值是 $1$ 到 $n$ 的全排列),每次,把 $1$ 到 $q[i-1]$ 的位置置位减 $1$ ,然后把答案减小时对 $1$ 到答案在 $p$ 中的位置加 $1$ ,这样可以算出后缀和。
#include <bits/stdc++.h>
#define L(i, a, b) for(int i = a; i <= b; i++)
#define R(i, a, b) for(int i = a; i >= b; i--)
using namespace std;
const int N = 300010;
struct Tree{
int l, r, mid, mx, lazy;
void Tag(int v){mx += v, lazy += v;}
}t[N << 2];
int n, id[N], p[N], q[N];
void Pushup(int p){
t[p].mx = max(t[p << 1].mx, t[p << 1 | 1].mx);
}
void Pushdown(int p){
t[p << 1].Tag(t[p].lazy);
t[p << 1 | 1].Tag(t[p].lazy);
t[p].lazy = 0;
}
void Build(int p, int l, int r){
t[p] = {l, r, l + r >> 1, 0, 0};
if(l == r) return;
Build(p << 1, t[p].l, t[p].mid);
Build(p << 1 | 1, t[p].mid + 1, t[p].r);
Pushup(p);
}
void Update(int p, int l, int r, int v){
if(l <= t[p].l && t[p].r <= r){
t[p].Tag(v);
return;
}
Pushdown(p);
if(l <= t[p].mid) Update(p << 1, l, r, v);
if(t[p].mid < r) Update(p << 1 | 1, l, r, v);
Pushup(p);
}
int main(){
scanf("%d", &n);
L(i, 1, n) scanf("%d", &p[i]), id[p[i]] = i;
L(i, 1, n) scanf("%d", &q[i]);
Build(1, 1, n);
int x = n;
L(i, 1, n){
if(i > 1) Update(1, 1, q[i - 1], -1);
while(t[1].mx <= 0) Update(1, 1, id[x--], 1);
printf("%d ", x + 1);
}
return 0;
}