莫隊的 1.5 近似構造 題解

XuYueming發表於2024-08-21

前言

題目連結:洛谷

感覺 T4 比 T3 水,雖然我都沒做出來

題意簡述

給定 \(1 \sim n\) 的排列 \(a\)\(m\) 個區間 \([l_i, r_i]\)。定義值域區間 \([L, R]\) 的價值為 \(\operatorname{val}([L, R]) \operatorname{:=} \max \limits_{i = 1}^m \sum\limits_{k = l_i}^{r_i}[L \le a_k \le R]\)。求將 \([1, n]\) 劃分為若干不交的區間的價值之積的最大值,對 \(998244353\) 取模。

題目分析

劃分值域,其實一個序列上的問題,不妨考慮 DP。設 \(f_i\) 表示 \([1, i]\) 的值域區間已經被劃分成若干值域區間的價值的最大積。特別地,不妨令 \(f_0 = 1\)

接下來考慮轉移。考慮求 \(f_i\),可以不進行任何操作,直接繼承 \(i - 1\)。也可以進行一次劃分,那麼令新劃分的值域區間為 \([j, i]\),則有轉移:

\[f_i = \max _ {j=1} ^ {i} \Big \lbrace f_{j - 1} \cdot \operatorname{val}([j, i]) \Big \rbrace \]

具體地,我們讓 \(j\)\(i\) 開始向左掃,同時維護 \(m\) 個區間的桶,對於當前 \([j, i]\) 擴充套件出的新的一個值 \(j\),我們找到 \(a_p = j\) 的位置 \(p\),讓 \(l_i \leq p \leq r_i\) 的區間的桶計數加一,轉移就很簡單了。

另外,由於同時要最大值和取模後的值,套路化地,記真實值為 \(x\),我們只用維護 \((\log x, x \bmod M)\) 的二元組,由對數性質,比較最值是取前者,答案則是後者。易知 double 精度足夠。

這樣做的時間複雜度是 \(\Theta(n^2m)\),怎麼最佳化呢?不妨輸出一下最優決策時的相關資訊,發現 \(\operatorname{val}([j, i]) \in \lbrace 2, 3 \rbrace\),這是為什麼呢?理解起來很簡單,因為我們在價值 \(>3\) 的時候,總是能夠把 \([j, i]\) 劃分成兩個區間,使得它們價值之積在之前價值取到 \(\max\) 的區間中,取到一個不劣於之前的價值的價值。讀者自證不難。

於是,DP 只能決策一個價值為 \(2\)\(3\) 的區間。現在的問題就是對於每一個 \(i\),找到最後一個 \(j\),使 \(\operatorname{val}([j, i]) = 2\),再用相同演算法求得 \(\operatorname{val}([j, i]) = 3\)\(j\),最後就能 \(\Theta(n)\) DP 了。

發現,區間越大,其價值越大,要維護區間價值為 \(2 / 3\),類似於滑動視窗,可以用雙指標預處理。時間複雜度降為了 \(\Theta(nm)\),瓶頸在於預處理。還有沒有更好的處理方式呢?我們發現,雙指標已經無法最佳化了,現在迫切需要一個能維護這 \(m\) 個桶的資料結構,支援查詢全域性最值、給能夠包含某一個點的區間對應的桶做加法。

這並不套路。全域性最值通常很好維護,難點在做一些毫無規律的單點加。注意到區間對我們來說是沒有先後順序之分的,不妨排個序。至於關鍵字,為了服務我們的目的,不妨按照左端點先排一次序。現在,我們已經能夠透過二分快速縮小我們想要做單點加的範圍了。我們只需要在這些左端點 \(l_i \leq p\) 的區間中,找到右端點 \(r_i \geq p\) 的區間,對這些區間做單點加。

可是,右端點不是單調的,我們還是無法便捷地操作。有沒有什麼方法使得在左端點單增的同時,右端點也單增呢?即,不存在一個區間包含另一個區間的情況。這似乎意味著我們必須要刪除一些區間。發現,對於 \(a\) 包含 \(b\) 的情況,我們完全可以刪除被包含的 \(b\) 而不影響答案,應為如果某一個值域區間 \([L, R]\)\(\operatorname{val}\)\(b\) 取到了 \(\max\),在 \(a\) 一定不劣。預處理刪除是 naive 的。於是,我們就能做到左右端點分別單增。

這就意味著,我們能夠迅速地定位一段區間,並對這段區間的每一個桶做單點加。等等!這不就退化到區間加了嗎,結合所求的最值,直接上一棵線段樹就行了。

至此,我們透過了這道題,時間複雜度:\(\Theta((n + m) \log m)\)

當然,有些常數最佳化,例如排序值域很小,直接用桶;對於每個點,我們可以雙指標處理出要區間加的範圍。於是乎,卡脖子的瓶頸就在於線段樹了。具體請看程式碼。

程式碼

\(\Theta(n^2m)\)

\(\Theta(nm)\)

以及正解,妥妥地跑到了(除了出題人之前交的Rank 1

#include <cstdio>
using namespace std;

const int MAX = 1 << 26;
char buf[MAX], *p = buf;
#define getchar() *p++
#define isdigit(ch) (ch >= '0' && ch <= '9')
inline void read(int &x) {
    x = 0; char ch = getchar();
    for (; !isdigit(ch); ch = getchar());
    for (;  isdigit(ch); x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar());
}

constexpr const double lg2 = 0.693147180559945286226763982995180413126945495605468750000000000736520;
constexpr const double lg3 = 1.098612288668109782108217586937826126813888549804687500000000000736520;
constexpr inline int max(int a, int b) { return a > b ? a : b; }

const int N = 300010;
const int mod = 998244353;

int n, m, mxR[N];
int val[N], whr[N];

struct Segment {
    int L, R;
} line[N];

int two[N], san[N];

struct node {
    double sum;
    int val;
    constexpr node(double s = 0, int v = 0) : sum(s), val(v) {}
    inline friend bool operator < (const node & a, const node & b) {
        return a.sum < b.sum;
    }
    inline friend node operator + (const node & a, const node & b) {
        return node(a.sum + b.sum, 1ll * a.val * b.val % mod);
    }
    inline friend node max(const node & a, const node & b) {
        return a < b ? b : a;
    }
} dp[N];

constexpr node TWO(lg2, 2), SAN(lg3, 3);

struct Segment_Tree {
    #define lson (idx << 1    )
    #define rson (idx << 1 | 1)
    
    struct node {
        int l, r, lazy, mx;
    } tree[N << 2];
    
    void build(int idx, int l, int r) {
        tree[idx] = {l, r, 0, 0};
        if (l == r) return;
        int mid = (l + r) >> 1;
        build(lson, l, mid), build(rson, mid + 1, r);
    }
    
    inline void pushtag(int idx, int v) {
        tree[idx].mx += v;
        tree[idx].lazy += v;
    }
    
    inline void pushdown(int idx) {
        if (!tree[idx].lazy) return;
        pushtag(lson, tree[idx].lazy);
        pushtag(rson, tree[idx].lazy);
        tree[idx].lazy = 0;
    }
    
    void modify(int idx, int l, int r, int v) {
        if (l <= tree[idx].l && tree[idx].r <= r) return pushtag(idx, v);
        pushdown(idx);
        if (l <= tree[lson].r) modify(lson, l, r, v);
        if (r >= tree[rson].l) modify(rson, l, r, v);
        tree[idx].mx = max(tree[lson].mx, tree[rson].mx);
    }
    
    #undef lson
    #undef rson
} yzh;  // yzh i love you!

inline int getmx() {
    return yzh.tree[1].mx;
}

int LEFT[N], RIGHT[N];
// 第一個右端點不小於 whr[i] 的位置
// 最後一個左端點不大於 whr[i] 的位置

inline void add(int i, int v) {
    i = whr[i];
    if (LEFT[i] <= RIGHT[i]) yzh.modify(1, LEFT[i], RIGHT[i], v);
}

signed main() {
    fread(buf, 1, MAX, stdin);
    read(n), read(m);
    for (int i = 1; i <= n; ++i) read(val[i]), whr[val[i]] = i;
    for (int i = 1, L, R; i <= m; ++i) read(L), read(R), mxR[L] = max(mxR[L], R);
    m = 0;
    for (int i = 1, curR = 0; i <= n; ++i)
        if (mxR[i] > curR) {
            line[++m] = {i, mxR[i]};
            curR = mxR[i];
        }
    
    for (int i = 1; i <= n; ++i) {
        LEFT[i] = LEFT[i - 1];
        while (line[LEFT[i]].R < i) ++LEFT[i];
    }
    RIGHT[n + 1] = m;
    for (int i = n; i >= 1; --i) {
        RIGHT[i] = RIGHT[i + 1];
        while (line[RIGHT[i]].L > i) --RIGHT[i];
    }
    
    yzh.build(1, 1, m);
    for (int i = 1; i <= n; ++i) {
        add(i, 1);
        if (getmx() < 2) continue;
        two[i] = two[i - 1];
        if (!two[i]) two[i] = 1;
        while (true) {
            add(two[i]++, -1);
            if (getmx() < 2) {
                add(--two[i], 1);
                break;
            }
        }
    }
    
    yzh.build(1, 1, m);
    for (int i = 1; i <= n; ++i) {
        add(i, 1);
        if (getmx() < 3) continue;
        san[i] = san[i - 1];
        if (!san[i]) san[i] = 1;
        while (true) {
            add(san[i]++, -1);
            if (getmx() < 3) {
                add(--san[i], 1);
                break;
            }
        }
    }
    
    dp[0].val = 1;
    for (int i = 1; i <= n; ++i) {
        dp[i] = dp[i - 1];
        if (two[i]) dp[i] = max(dp[i], dp[two[i] - 1] + TWO);
        if (san[i]) dp[i] = max(dp[i], dp[san[i] - 1] + SAN);
    }
    printf("%d", dp[n].val);
    return 0;
}

後記

遇到最值取模,不一定是存在一種構造的方法,能夠保證算出最值,然後在演算法過程中取模,也可能是取對數 DP 等決策。

若干區間,每次給出一個點,對包含這個點的所有區間操作,可以嘗試去掉包含的區間,這樣就能二分出連續的一段,就能區間操作了。

相關文章