不需分析结论的麻烦做法,模拟赛时想到这个做法没写出来,跟同学说场上没写出这个题的时候同学大为震惊……
首先,可以想到设 为最后一个元素为 ,选了 个是否可行。转移方程即为
这个转移显然可以用前缀或优化,我们设 为 ,类似的设 为 。
首先可以滚掉第一维 。即变为 这样的形式;然后可以舍弃原数组。具体来说, 就等于 或者 ,所以可以对于 的所有 以及 的所有 更新过去,不需再用原数组。转移方程变为
现在转移还是需要 ,而且现在需要维护的状态是一个 数组,这很不好。继续发现,因为 前缀或的性质,所以对于每个 ,一定可以找到一个断点 () 满足对于所有 的 ,都有 ;对于所有 的 ,都有 。对于 也可以类似的找到 。
我们只需要维护这些断点,就可以 找出对应的 值和 值。而且,我们还可以发现,对于一个有效转移(即转移过去的值为 ),就相当于给对应的断点取了一个 。于是可以写出转移方程:
现在时间复杂度被优化到了 ,而需要维护的状态数已经变为了 级别,距离目标已经很近了。
我们猜测, 这两个断点序列都是单调的,但是很遗憾,这个结论是错误的。但是我们可以发现一个进阶结论:对于所有的 相同的位置, 在这些位置上是单调的,也就是说,它们都是由两个单调序列拼起来的,因为在 相同的情况下,相邻的两个位置的决策必然有一个包含另一个。
所以,能转移的 一定是在一个前缀内且满足 为某个字符的,而这个前缀也是可以快速求出的。
具体的,我们需要维护 并支持以下操作:
- 对于一个前缀内 的 令 ;
- 求出位置 使得 为最后一个满足 且 的位置;
- 求出位置 使得 为最后一个满足 且 的位置。
我们开一棵线段树,每一个节点中对于每一种二元组 维护出区间内的 以及标记,每次修改直接对于所有满足要求的二元组都对应的打上标记,而询问位置就在线段树上二分即可。
求答案比较简单,我们只需要倒着搜整棵线段树,找到第一个 或者 的 作为答案。总时间复杂度为 ,常数较大。
接下来是完整代码:
#include <algorithm>
#include <cstring>
#include <cstdio>
#define inf 0x3f3f3f3f
using namespace std;
const int N = 300001;
int n, a[N], ci[N];char b[N];
struct node{int pf[4], pg[4], cz[4], tg[4];}tr[N << 2];
inline void tmin(int& x, int y){if (x == -1) x = y;else x = min(x, y);}
inline void tmax(int& x, int y){x = max(x, y);}
inline void addtag(int u, int z, int x){if (x == -1) return;
if (z == 0) tmin(tr[u].pf[0], x), tmin(tr[u].pf[2], x), tmin(tr[u].tg[0], x);
if (z == 1) tmax(tr[u].pg[0], x), tmax(tr[u].pg[2], x), tmax(tr[u].tg[1], x);
if (z == 2) tmin(tr[u].pf[1], x), tmin(tr[u].pf[3], x), tmin(tr[u].tg[2], x);
if (z == 3) tmax(tr[u].pg[1], x), tmax(tr[u].pg[3], x), tmax(tr[u].tg[3], x);
}
inline void downtag(int u){for (int i = 0;i < 4;i ++)
addtag(u << 1, i, tr[u].tg[i]), addtag(u << 1 | 1, i, tr[u].tg[i]), tr[u].tg[i] = -1;
}
inline void pushup(int u){
for (int i = 0;i < 4;i ++){tr[u].pf[i] = n, tr[u].pg[i] = 0;
if (tr[u << 1].cz[i]) tmin(tr[u].pf[i], tr[u << 1].pf[i]), tmax(tr[u].pg[i], tr[u << 1].pg[i]);
if (tr[u << 1 | 1].cz[i]) tmin(tr[u].pf[i], tr[u << 1 | 1].pf[i]), tmax(tr[u].pg[i], tr[u << 1 | 1].pg[i]);
}
}
void build(int u, int l, int r){memset(tr[u].tg, -1, 16);
if (l == r){ci[l] = (b[l + 1] == 'D') << 1 | b[l] == 'D';
memset(tr[u].pf, 0x3f, 16), memset(tr[u].pg, 0, 16);
tr[u].pf[ci[l]] = n, tr[u].pg[ci[l]] = tr[u].cz[ci[l]] = 1;return;
}int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r), pushup(u);
for (int i = 0;i < 4;i ++) tr[u].cz[i] = tr[u << 1].cz[i] | tr[u << 1 | 1].cz[i];
}
void update(int u, int l, int r, int p, int z, int x){
if (r <= p) return addtag(u, z << 1, x), addtag(u, z << 1 | 1, x);
if (l > p) return;int mid = l + r >> 1;downtag(u);
update(u << 1, l, mid, p, z, x), update(u << 1 | 1, mid + 1, r, p, z, x), pushup(u);
}
inline int gtf(int u){int res = inf;if (tr[u].cz[0]) tmin(res, tr[u].pf[0]);if (tr[u].cz[1]) tmin(res, tr[u].pf[1]);return res;}
inline int gtg(int u){int res = 0;if (tr[u].cz[2]) tmax(res, tr[u].pg[2]);if (tr[u].cz[3]) tmax(res, tr[u].pg[3]);return res;}
int fndpos(int u, int l, int r, int x){
if (l == r) return x > gtf(u) ? l : -1;int mid = l + r >> 1;downtag(u);
if (gtf(u << 1 | 1) < x) return fndpos(u << 1 | 1, mid + 1, r, x);
return fndpos(u << 1, l, mid, x);
}
int gndpos(int u, int l, int r, int x){
if (l == r) return x < gtg(u) ? l : -1;int mid = l + r >> 1;downtag(u);
if (gtg(u << 1 | 1) > x) return gndpos(u << 1 | 1, mid + 1, r, x);
return gndpos(u << 1, l, mid, x);
}
int solve(int u, int l, int r){
if (l == r) return tr[u].pf[ci[l]] < n || tr[u].pg[ci[l]] > 1 ? l : 0;
int mid = l + r >> 1;downtag(u);
return max(solve(u << 1, l, mid), solve(u << 1 | 1, mid + 1, r));
}
int main(){scanf("%d", &n);
for (int i = 1;i <= n;i ++) scanf("%d", &a[i]);
scanf("%s", b + 1), build(1, 0, n - 1);
for (int i = 1, p0, p1;i <= n;i ++){
p0 = fndpos(1, 0, n - 1, a[i]), p1 = gndpos(1, 0, n - 1, a[i]);
update(1, 0, n - 1, p0 + 1, 0, a[i]);
update(1, 0, n - 1, p1 + 1, 1, a[i]);
}printf("%d", solve(1, 0, n - 1));
}
虽然这个做法被标算完全爆踩,但是一步步推导并实现出自己独特的做法的过程令我乐在其中,很有成就感。而且,这个做法也大大的加深了我对数据结构优化 DP 的理解。