P2042 [NOI2005] 維護數列 題解
一道奆資料結構題,需要有較高的碼力和基礎的資料結構。
一看過去就會發現這是道資料結構題,然後這道題實際上就是平衡樹的板子題只是有各種奇怪的操作而已。我用的是 FHQ Treap。
其實吧這道題本來我不打算寫題解的,畢竟還是比較顯然的資料結構題,但是這道題的眾多坑點讓我還是決定寫篇題解,本篇題解採用拆解程式碼的方式貼程式碼,最後會有我對這道題用 FHQ Treap 寫這道題的坑點總結。
分析一下這道題,會發現實際上就是插入,刪除,反轉,推平,區間查詢和,全域性查詢最大子段和。
前置知識:線段樹求最大子段和,例題是 GSS1。
好的現在我認為你應該會了這個 trick。
這道題我們可以仿照線段樹求最大子段和的方式,在每個節點維護以下 11 個值:l,r
Size
子樹大小,val
當前這個節點的權值,Key
就是隨機的值,pre,aft
表示前後綴最大和,sum
表示區間和,Maxn
表示最大子段和。然後還要維護 flag
表示推平懶標記,rev
表示翻轉懶標記。
於是你會發現如果直接開 \(4 \times 10^6\) 是會炸空間的,因此這裡我們需要垃圾回收,就是刪除節點的時候將不用的節點記錄下來以便重複使用,這塊的總複雜度是 \(O(點數)\) 的。
首先貼一下結構體:
const int MAXN = 5e5 + 5; int n, q, Root, a[MAXN], cnt_Node; struct node { int l, r, Size, val, Key; int pre, aft, sum, Maxn; bool flag, rev; #define l(p) tree[p].l #define r(p) tree[p].r #define Size(p) tree[p].Size #define val(p) tree[p].val #define Key(p) tree[p].Key #define pre(p) tree[p].pre #define aft(p) tree[p].aft #define sum(p) tree[p].sum #define Maxn(p) tree[p].Maxn #define flag(p) tree[p].flag #define rev(p) tree[p].rev }tree[MAXN]; stack <int> Rub;
Root
是根,cnt_Node
是當前樹的節點個數(不含刪除,就是拿來開點用的),Rub
是垃圾回收用的。
然後這道題有一個很大的坑點就是 最大子段和 不能為空,也就是說你必須選一個,因此我們需要考慮對 pre,aft,Maxn
做一點手腳:
在 Update(Pushup) 和 新建節點(Make_Node) 的時候,由於所有區間至少要選一個,所以一開始規定 Maxn = val
但是 pre
和 aft
是可以為 0 的(因為你已經選了一個了),然後按照正常的做法更新 Maxn
,注意這裡的 Maxn
是絕對不能和 0 取大的!
還有一點需要注意的是普通線段樹寫法 Update(Pushup) 的時候節點本身是沒有權值的,但是 FHQ Treap 裡面是有的,因此不能忘記把這個節點合併進去。
貼一下 Update
函式:
void Update(int p)
{
if (!p) return ;
Size(p) = Size(l(p)) + Size(r(p)) + 1; sum(p) = sum(l(p)) + sum(r(p)) + val(p);
pre(p) = Max(pre(l(p)), Max(sum(l(p)) + val(p) + pre(r(p)), 0));
aft(p) = Max(aft(r(p)), Max(sum(r(p)) + val(p) + aft(l(p)), 0));
Maxn(p) = Max(val(p), aft(l(p)) + val(p) + pre(r(p)));
if (l(p)) Maxn(p) = Max(Maxn(p), Maxn(l(p)));
if (r(p)) Maxn(p) = Max(Maxn(p), Maxn(r(p)));
} // 就是 Pushup
然後是新建節點 Make_Node
函式:
int Make_Node(int val)
{
int tmp = 0; if (Rub.empty()) tmp = ++cnt_Node; else { tmp = Rub.top(); Rub.pop(); } // 重複利用
l(tmp) = r(tmp) = 0; val(tmp) = val; Size(tmp) = 1;
sum(tmp) = Maxn(tmp) = val; pre(tmp) = aft(tmp) = Max(val, 0);
flag(tmp) = rev(tmp) = 0; Key(tmp) = rand();
return tmp;
} // 新建節點
還有一個下傳懶標記的 Spread(Pushdown) 函式,這塊會順便帶上打翻轉懶標記和推平懶標記的兩個函式 Reverse
和 Cover
。
Reverse
就是正常的翻轉,考慮到 FHQ Treap 的中序遍歷就是原序列,直接交換左右子樹就好了,往下打懶標記。
這裡需要注意兩點:
- 翻轉的時候子樹不能直接打懶標記,是需要看情況的,因為翻轉兩次就是沒有翻轉。
- 注意翻轉的同時前後綴也被翻轉了,因此也是需要交換的。
然後是 Cover
,這個函式往下推平的時候需要注意推平的值就是這個點的 val
,以及兒子的 Maxn
必須要選一個,pre,aft
可選可不選。
寫了這兩個函式就能寫 Spread
了,這三個函式程式碼如下:
void Cover(int p, int val)
{
if (!p) return ;
val(p) = val; sum(p) = Size(p) * val;
pre(p) = aft(p) = ((val > 0) ? sum(p) : 0);
Maxn(p) = (val > 0) ? sum(p) : val; flag(p) = 1;
} // 區間推平
void Reverse(int p)
{
if (!p) return ;
std::swap(l(p), r(p));
std::swap(pre(p), aft(p));
rev(p) ^= 1; // 注意不能直接賦值為 1
} // 區間反轉
void Spread(int p)
{
if (!p) return ;
if (flag(p))
{
if (l(p)) Cover(l(p), val(p));
if (r(p)) Cover(r(p), val(p));
flag(p) = 0;
}
if (rev(p))
{
if (l(p)) Reverse(l(p));
if (r(p)) Reverse(r(p));
rev(p) = 0;
}
} // 就是 Pushdown
然後這道題有一個全域性的坑點,好像有兩組資料是有的,就是操作的時候有時操作的區間長度是為 0 的,這個點也會坑到很多人,因此以上所有函式都需要在最開始加一個判斷節點是否為空。
好的現在有了以上的基礎函式,可以開始寫各類我們需要的函數了。
首先看 Split
函式,這裡的函式按照大小分裂即可。
然後是 Merge
函式,這塊的話就是正常 Merge
,但是當你往下合併的時候哪棵樹要往下合併哪棵樹就需要 Spread
。
void Split(int now, int val, int &x, int &y)
{
if (now == 0) { x = y = 0; return ; }
Spread(now);
if (Size(l(now)) + 1 <= val) { x = now; Split(r(now), val - Size(l(now)) - 1, r(now), y); }
else { y = now; Split(l(now), val, x, l(now)); }
Update(now);
}
int Merge(int x, int y)
{
if (!x || !y) return x + y;
if (Key(x) < Key(y))
{
Spread(x); r(x) = Merge(r(x), y);
Update(x); return x;
}
else
{
Spread(y); l(y) = Merge(x, l(y));
Update(y); return y;
}
}
然後是插入序列 Insert
函式,但是首先我們需要一個 Build
函式來建樹。
這個建樹就是仿照線段樹二分遞迴建樹,Insert
函式應該是基操了,就是將前面 pos
個拿出來然後三棵樹合併。
但顯然這裡也有坑點,先看程式碼:
int Build(int l, int r)
{
if (l == r) return Make_Node(a[l]);
int mid = (l + r) >> 1;
int x = Build(l, mid);
int y = Build(mid + 1, r);
return Merge(x, y); // 注意這三句話
} // 遞迴建樹
void Insert()
{
int pos = Read(), len = Read();
for (int i = 1; i <= len; ++i) a[i] = Read();
int x, y; Split(Root, pos, x, y);
Root = Merge(Merge(x, Build(1, len)), y);
}
看見上面打註釋的三句話了嗎?如果你直接寫成 return Merge(Build(l, mid), Build(mid + 1, r));
,可能程式會先執行後面的 Build(mid + 1, r)
,這樣子你可能就會掛掉了。
加可能的原因是有些人這樣寫是不會掛掉的(比如我這份程式碼),但是有些人會。
然後是 Delete
函式,就是正常的把需要的區間拉出來直接斃了,這裡會加上垃圾回收函式:
void Recycle(int p)
{
Rub.push(p);
if (l(p)) Recycle(l(p));
if (r(p)) Recycle(r(p));
} // 垃圾回收
void Delete()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Root = Merge(x, z); Recycle(y);
}
接下來看區間翻轉和區間推平操作,同樣也是將區間拉出來操作:
void Change_Cover()
{
int pos = Read(), len = Read(), val = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Cover(y, val);
Root = Merge(Merge(x, y), z);
}
void Change_Reverse()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Reverse(y);
Root = Merge(Merge(x, y), z);
}
最後就是兩個查詢操作了,不用我多說了吧:
void Ask_sum()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); printf("%d\n", sum(y));
Root = Merge(Merge(x, y), z);
}
void Ask_Maxn()
{
printf("%d\n", Maxn(Root));
}
接下來總結一下這道題的坑點所在:
- 由於最大子段和不能為空,因此
pre,aft,Maxn
需要細節操作。 - 由於 FHQ Treap 本身的節點也是有權值的,因此也要合併進去。
-
Reverse
操作的時候不能忘記交換前後綴。 - 每次操作之前一定要看一眼節點是不是空的。
- 插入的時候需要先建樹再合併。
- 因為空間限制較小,需要垃圾回收。
以上就是我遇到的所有坑點,如果還有的話就需要自己總結了。
Github:CodeBase-of-Plozia。
Code:
/*
========= Plozia =========
Author:Plozia
Problem:P2042 [NOI2005] 維護數列
Date:2021/12/28
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::stack;
using std::string;
typedef long long LL;
const int MAXN = 5e5 + 5;
int n, q, Root, a[MAXN], cnt_Node;
struct node
{
int l, r, Size, val, Key;
int pre, aft, sum, Maxn;
bool flag, rev;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define Size(p) tree[p].Size
#define val(p) tree[p].val
#define Key(p) tree[p].Key
#define pre(p) tree[p].pre
#define aft(p) tree[p].aft
#define sum(p) tree[p].sum
#define Maxn(p) tree[p].Maxn
#define flag(p) tree[p].flag
#define rev(p) tree[p].rev
}tree[MAXN];
stack <int> Rub;
int Read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = sum * 10 + (ch ^ 48);
return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
int Make_Node(int val)
{
int tmp = 0; if (Rub.empty()) tmp = ++cnt_Node; else { tmp = Rub.top(); Rub.pop(); }
l(tmp) = r(tmp) = 0; val(tmp) = val; Size(tmp) = 1;
sum(tmp) = Maxn(tmp) = val; pre(tmp) = aft(tmp) = Max(val, 0);
flag(tmp) = rev(tmp) = 0; Key(tmp) = rand();
return tmp;
} // 新建節點
void Cover(int p, int val)
{
if (!p) return ;
val(p) = val; sum(p) = Size(p) * val;
pre(p) = aft(p) = ((val > 0) ? sum(p) : 0);
Maxn(p) = (val > 0) ? sum(p) : val; flag(p) = 1;
} // 區間推平
void Reverse(int p)
{
if (!p) return ;
std::swap(l(p), r(p));
std::swap(pre(p), aft(p));
rev(p) ^= 1;
} // 區間反轉
void Update(int p)
{
if (!p) return ;
Size(p) = Size(l(p)) + Size(r(p)) + 1; sum(p) = sum(l(p)) + sum(r(p)) + val(p);
pre(p) = Max(pre(l(p)), Max(sum(l(p)) + val(p) + pre(r(p)), 0));
aft(p) = Max(aft(r(p)), Max(sum(r(p)) + val(p) + aft(l(p)), 0));
Maxn(p) = Max(val(p), aft(l(p)) + val(p) + pre(r(p)));
if (l(p)) Maxn(p) = Max(Maxn(p), Maxn(l(p)));
if (r(p)) Maxn(p) = Max(Maxn(p), Maxn(r(p)));
} // 就是 Pushup
void Spread(int p)
{
if (!p) return ;
if (flag(p))
{
if (l(p)) Cover(l(p), val(p));
if (r(p)) Cover(r(p), val(p));
flag(p) = 0;
}
if (rev(p))
{
if (l(p)) Reverse(l(p));
if (r(p)) Reverse(r(p));
rev(p) = 0;
}
} // 就是 Pushdown
void Split(int now, int val, int &x, int &y)
{
if (now == 0) { x = y = 0; return ; }
Spread(now);
if (Size(l(now)) + 1 <= val) { x = now; Split(r(now), val - Size(l(now)) - 1, r(now), y); }
else { y = now; Split(l(now), val, x, l(now)); }
Update(now);
}
int Merge(int x, int y)
{
if (!x || !y) return x + y;
if (Key(x) < Key(y))
{
Spread(x); r(x) = Merge(r(x), y);
Update(x); return x;
}
else
{
Spread(y); l(y) = Merge(x, l(y));
Update(y); return y;
}
}
int Build(int l, int r)
{
if (l == r) return Make_Node(a[l]);
int mid = (l + r) >> 1;
int x = Build(l, mid);
int y = Build(mid + 1, r);
return Merge(x, y);
} // 遞迴建樹
void Recycle(int p)
{
Rub.push(p);
if (l(p)) Recycle(l(p));
if (r(p)) Recycle(r(p));
} // 垃圾回收
void Insert()
{
int pos = Read(), len = Read();
for (int i = 1; i <= len; ++i) a[i] = Read();
int x, y; Split(Root, pos, x, y);
Root = Merge(Merge(x, Build(1, len)), y);
}
void Delete()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Root = Merge(x, z); Recycle(y);
}
void Change_Cover()
{
int pos = Read(), len = Read(), val = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Cover(y, val);
Root = Merge(Merge(x, y), z);
}
void Change_Reverse()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); Reverse(y);
Root = Merge(Merge(x, y), z);
}
void Ask_sum()
{
int pos = Read(), len = Read();
int x, y, z; Split(Root, pos - 1, x, y);
Split(y, len, y, z); printf("%d\n", sum(y));
Root = Merge(Merge(x, y), z);
}
void Ask_Maxn()
{
printf("%d\n", Maxn(Root));
}
int main()
{
n = Read(), q = Read(); srand(time(0));
for (int i = 1; i <= n; ++i) a[i] = Read();
Root = Build(1, n);
for (int i = 1; i <= q; ++i)
{
string str; std::cin >> str;
if (str == "INSERT") Insert();
if (str == "DELETE") Delete();
if (str == "MAKE-SAME") Change_Cover();
if (str == "REVERSE") Change_Reverse();
if (str == "GET-SUM") Ask_sum();
if (str == "MAX-SUM") Ask_Maxn();
}
return 0;
}