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\)。同一位置没有同色棋子。

将序列划分成若干连续段。一个连续段是红色段,当且仅当该段内红色棋子数量不少于黄色,也不少于蓝色:

\[\sum_{i=l+1}^{r} r_i \ge \sum_{i=l+1}^{r} y_i \quad\text{且}\quad \sum_{i=l+1}^{r} r_i \ge \sum_{i=l+1}^{r} b_i \]

求所有红色段长度之和的最大值。


2. 条件转化

定义每个位置的差值:

\[a_i = r_i - y_i,\qquad d_i = r_i - b_i \]

则区间 \([l+1, r]\) 是红色段等价于:

\[\sum_{i=l+1}^{r} a_i \ge 0 \quad\text{且}\quad \sum_{i=l+1}^{r} d_i \ge 0 \]

令前缀和:

\[A_i = \sum_{k=1}^{i} a_k,\qquad D_i = \sum_{k=1}^{i} d_k \]

其中 \(A_0 = D_0 = 0\)。

于是区间 \([l+1, r]\) 是红色段当且仅当:

\[A_r \ge A_l \quad\text{且}\quad D_r \ge D_l \]


3. DP 设计

设 \(dp[i]\) 表示前 \(i\) 个位置能得到的红色段长度之和的最大值。

转移有两种选择:

  1. 位置 \(i\) 不放入任何红色段:

\[dp[i] = dp[i-1] \]

  1. 以 \(i\) 结尾的某个红色段从 \(j+1\) 开始,其中 \(0 \le j < i\):

\[dp[i] = dp[j] + (i - j) \]

但必须满足:

\[A_i \ge A_j \quad\text{且}\quad D_i \ge D_j \]

合并起来:

\[dp[i] = \max\left(dp[i-1],\ \max_{\substack{0 \le j < i \\ A_j \le A_i,\ D_j \le D_i}} \big(dp[j] + i - j\big)\right) \]

把 \(i\) 提出来:

\[dp[i] = \max\left(dp[i-1],\ i + \max_{\substack{0 \le j < i \\ A_j \le A_i,\ D_j \le D_i}} \big(dp[j] - j\big)\right) \]

问题转化为:按时间顺序处理 \(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_j \le A_i,\qquad D_j \le D_i \]

处理方式:

  • 按 \(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\) 最大值。

于是:

\[\max_{\substack{j \in [l, mid] \\ A_j \le A_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;
}
posted @ 2026-10-13 14:38  thenrify  阅读(41)  评论(0)    收藏  举报