前言
題目連結:洛谷。
感覺 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]\),則有轉移:
具體地,我們讓 \(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 等決策。
若干區間,每次給出一個點,對包含這個點的所有區間操作,可以嘗試去掉包含的區間,這樣就能二分出連續的一段,就能區間操作了。