Red Sequence
Red Sequence
归并排序,Fenwick优化,cdq分治
1. 题意
给定长度为 \(n\) 的序列,每个位置 \(i\) 有 \(r_i, y_i, b_i \in \{0,1\}\),分别表示红、黄、蓝棋子是否存在,且 \(1 \le r_i + y_i + b_i \le 3\)。同一位置没有同色棋子。
将序列划分成若干连续段。一个连续段是红色段,当且仅当该段内红色棋子数量不少于黄色,也不少于蓝色:
求所有红色段长度之和的最大值。
2. 条件转化
定义每个位置的差值:
则区间 \([l+1, r]\) 是红色段等价于:
令前缀和:
其中 \(A_0 = D_0 = 0\)。
于是区间 \([l+1, r]\) 是红色段当且仅当:
3. DP 设计
设 \(dp[i]\) 表示前 \(i\) 个位置能得到的红色段长度之和的最大值。
转移有两种选择:
- 位置 \(i\) 不放入任何红色段:
- 以 \(i\) 结尾的某个红色段从 \(j+1\) 开始,其中 \(0 \le j < i\):
但必须满足:
合并起来:
把 \(i\) 提出来:
问题转化为:按时间顺序处理 \(i\),每次查询所有 \(j < i\) 且满足 \(A_j \le A_i,\ D_j \le D_i\) 的 \(dp[j] - j\) 的最大值。
这是一个典型的二维偏序 DP。
4. 二维偏序
转移需要同时满足两个不等式:
- \(A_j \le A_i\)
- \(D_j \le D_i\)
并且要查询 \(dp[j] - j\) 的最大值。
直接做是 \(O(n^2)\),需要优化。
5. CDQ 分治 + 树状数组
5.1 分治结构
按时间下标二分区间 \([l, r]\),递归处理:
cdq(l, r):
if l == r:
处理叶子
return
mid = (l + r) / 2
cdq(l, mid) // 1. 先递归左半
用左半更新右半 // 2. 左半 -> 右半
cdq(mid + 1, r) // 3. 再递归右半
归并排序,使 [l, r] 按 A 有序 // 4. 返回给上一层
关键顺序:先处理完左半内部,再用左半更新右半,最后处理右半内部。这样每个 \(j < i\) 的转移都会在它们被分到不同半区的那个节点处,通过“左半更新右半”恰好处理一次。
5.2 每层做什么
- 递归左半:左半内部所有转移完成,左半的 \(dp\) 成为最终值,并且左半已按 \(A\) 有序。
- 用左半更新右半:
- 左半的点 \(j \in [l, mid]\),右半的点 \(i \in [mid+1, r]\)。
- 此时所有 \(j < i\) 自动满足。
- 条件:\(A_j \le A_i\) 且 \(D_j \le D_i\)。
- 查询 \(\max (dp[j] - j)\)。
- 递归右半:右半内部继续分治,处理右半内部的转移。
- 归并排序:把左右两半按 \(A\) 合并,返回给上一层。
5.3 为什么按 A 排序,D 用 BIT
查询需要同时满足两个条件:
处理方式:
- 按 \(A\) 排序 + 双指针:解决第一维 \(A_j \le A_i\)。左半已按 \(A\) 有序,右半复制出来按 \(A\) 排序,双指针扫描,把所有 \(A_j \le A_i\) 的左半点加入 BIT。
- BIT 前缀查询:解决第二维 \(D_j \le D_i\)。BIT 以 \(D_j\) 为下标,维护 \(dp[j] - j\) 的前缀最大值。查询 \(D_i\) 的前缀最大值,得到所有满足 \(D_j \le D_i\) 的 \(dp[j] - j\) 最大值。
于是:
就被 BIT 和双指针共同维护好了。
5.4 归并排序的作用
上层分治需要“左半按 \(A\) 有序”来双指针扫描。递归返回时,把左右两个已经各自按 \(A\) 有序的子区间归并成一个整体有序的区间,返回给上层。这样每层只花 \(O(k)\) 归并,而不是 \(O(k \log k)\) 重新排序。
归并过程:两个指针分别指向左右两半当前最小元素,比较 \(A\) 值,依次写入临时数组,最后复制回原数组。
5.5 时间戳清空 BIT
BIT 在每层 CDQ 中都要复用。如果每次暴力清空 \(O(n)\),总代价太大。
使用时间戳(懒惰清空):
- 每个 BIT 节点
tree[pos]配一个版本号tag[pos]。 - 全局维护
cur_tag,每进入一层新的 CDQ 就++cur_tag,相当于 O(1) 逻辑清空。 update时,若tag[pos] != cur_tag,直接覆盖并打上当前版本号;否则取最大值。query时,只读取tag[pos] == cur_tag的节点,旧版本数据自动忽略。
这样清空代价为 \(O(1)\)。
6. 边界:为什么必须包含 \(j=0\)
转移中 \(j\) 从 \(0\) 开始。\(j=0\) 表示“从位置 \(1\) 到 \(i\) 的整个前缀作为一个红色段”,贡献长度为 \(i\)。
如果 CDQ 区间从 \(1\) 开始,\(j=0\) 不在分治树中,永远不会被加入 BIT,所有从开头开始的红色段都算不进来。
因此必须调用 cdq(0, n),并在叶子特判 l == 0:
if (l == r) {
if (l > 0) dp[l] = max(dp[l - 1], dp[l]);
ass[l] = l;
return;
}
\(dp[0] = 0\),\(A_0 = D_0 = 0\)。
7. 离散化 vs 偏移量
\(D_i\) 的范围是 \([-n, n]\),可能是负数,不能直接作为 BIT 下标。两种处理方式:
- 加偏移量:
pos = D[i] + offset,取offset = n + 1,则pos落在 \([1, 2n+1]\)。 - 离散化:把所有 \(D_i\) 排序去重,映射到 \([1, m]\),\(m\) 为不同值的个数。
加偏移量更简单,但依赖值域已知;离散化更通用,BIT 更小。
8. 复杂度
- 时间:CDQ 分治共 \(O(\log n)\) 层,每层 BIT 操作 \(O(n \log n)\),归并 \(O(n)\),总时间 \(O(n \log^2 n)\)。
- 空间:\(O(n)\)。
对于 \(n \le 10^6\) 可以稳定通过。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
const int INF = 1e8;
int n, dif;//偏移量,修正D为负数的情况
vector<int> A, D;// a = r - y的前缀和,d = r - b的前缀和
vector<int> dp;
vector<int> ass, aright, temp;//排序数组
struct Fenwick {
int n;
vector<int> tree;
vector<int> tag;
int cur;//时间戳,快速懒清零
Fenwick(int n = 1e6) {init(n);}
void init(int n_) {
n = n_;
tree.assign(n + 1, 0); tag.assign(n + 1, 0);
cur = 0;
}
void clear() { cur ++; }
void update(int x, int del) {
for(;x <= n; x += x & -x) {
if(tag[x] != cur) {
tag[x] = cur; tree[x] = del;
}
else if(tree[x] < del) tree[x] = del;
}
}
int query(int x) {
int res = -INF;
for(;x >= 1; x -= x & -x) {
if(tag[x] == cur && tree[x] > res) res = tree[x];
}
return res;
}
};
Fenwick bit;
void cdq(int l, int r) {
if(l == r) {
if(l > 0) dp[l] = max(dp[l - 1], dp[l]);
ass[l] = l;
return;
}
int mid = (l + r) / 2;
cdq(l, mid);
int id = 0;
for(int i = mid + 1; i <= r; i ++) aright[id ++] = i;
sort(aright.begin(), aright.begin() + id, [&](int x, int y) {
return A[x] < A[y];
});
//用左边处理右边
bit.clear();
int left = l;
for(int i = 0; i < id; i ++) {
int rid = aright[i];
while(left <= mid && A[ass[left]] <= A[rid]) {
int lid = ass[left];
bit.update(D[lid] + dif, dp[lid] - lid);
left ++;
}
int best = bit.query(D[rid] + dif);
if(best != -INF) dp[rid] = max(dp[rid], rid + best);
}
cdq(mid + 1, r);
//归并排序
int lt = l, rt = mid + 1, k = l;
while (lt <= mid && rt <= r) {
if(A[ass[lt]] <= A[ass[rt]]) temp[k ++] = ass[lt ++];
else temp[k ++] = ass[rt ++];
}
while(lt <= mid) temp[k ++] = ass[lt ++];
while(rt <= r) temp[k ++] = ass[rt ++];
for(int i = l; i <= r; i ++) ass[i] = temp[i];
}
void solve() {
cin >> n;
A.assign(n + 1, 0); D.assign(n + 1, 0);
for(int i = 1; i <= n; i ++) {
int r, y, b; cin >> r >> y >> b;
int a = r - y, d = r - b;
A[i] = A[i - 1] + a;
D[i] = D[i - 1] + d;
}
dp.assign(n + 1, 0);
ass.assign(n + 1, 0); aright.assign(n + 1, 0); temp.assign(n + 1, 0);
bit.init(2 * n + 5);
dif = n + 1;
cdq(0, n);
cout << dp[n] << "\n";
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int _ = 1; //cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号