建國中學 王民人
學歷:延平 → 建中
常用Handle:Astrayt
Discord:Astrayt#6587
想學資結所以來講資結
靜態的RMQ
瘦一點的線段樹
一切毒瘤的起源
教的東西應該都不陌生,應該蠻簡單的
稀疏表
給定一個長度為 \(N\) 的正整數序列 \( a_1, ... , a_N\),接著有 \(Q\) 筆詢問,每個詢問可能是下列形式的其中一個:
\(N, Q \leq 5 \times 10^5\)
用倍增法存資料的資料結構,下面是取min的Sparse Table:
建表時間&空間複雜度 \(O(NlogN)\)
查詢的時候找到最大的 \(k\) 滿足 \(2^{k} \leq (r - l + 1)\)
就可以在Sparse Table上查詢\(ST[l][k]\)和\(ST[r-2^k+1][k]\)的答案
時間複雜度\(O(1)\)
這裡就只實作取min的
反正是一樣的東西
#include <bits/stdc++.h>
using namespace std;
#define starburst ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int a[500005] ,ST[500005][25];
signed main(){
starburst
int n; cin >> n;
int q; cin >> q;
for(int i = 1; i <= n; ++i){
cin >> a[i];
ST[i][0] = a[i];
}
for(int j = 1; (1<<j) <= n; ++j){
for(int i = 1; i + (1 << j) - 1 <= n; ++i){
ST[i][j] = min(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);
}
}
for(; q; q--){
int l, r; cin >> l >> r;
int k = 0;
while((1 << (k + 1)) <= (r - l + 1)) k++;
cout << min(ST[l][k], ST[r - (1<<k) + 1][k]) << '\n';
}
}
有些東西不太適合砸Sparse Table
像是區間Sum、區間最大連續和等問題
但也有適合用Sparse Table的時候
畢竟查詢只要\(O(1)\)
請各位自行注意使用時機
如果你有注意到的話,其實Sparse Table和DP一樣可以滾動
但會變離線詢問
只要把詢問依照區間長度排序
就可以從用滾動的方法建立表格並回答所有詢問即可
想試的話請自己實作
因為講師不想做
樹狀數組
a.k.a. Binary Indexed Tree
a.k.a. BIT
在\(O(logN)\)的時間維護前後綴
(操作要有結合律喔)
struct FenwickTree{
int val[maxn];
void upd(int p, int v){
for(; p < N; p += (p & -p)) val[p] += v;
}
int qry(int p){
int ret = 0;
for(; p; p -= (p&-p)) ret += val[p];
return ret;
}
}bit;
BIT的用處相當廣
通常題目困難的地方不會是實作
而是如何抓到方法利用這個工具
由於以下題目的操作大同小異
所以不會附Code
給定一個長度為 \(N\) 的正整數序列 \( a_1, ... , a_N\),接著有 \(Q\) 筆操作,每個操作可能是下列形式的其中一個:
\(N, Q \leq 2 \times 10^5\)
給定一個長度為 \(N\) 的正整數序列 \( a_1, ... , a_N\),請你回答總共有幾對 \((i, j)\) 滿足 \(a_i > a_j\) 且 \(i < j\)。
\(N \leq 2 \times 10^5\)
給定一個長度為 \(N\) 的正整數序列 \( a_1, ... , a_N\),請找出最長遞增子序列的長度。
\(N \leq 2 \times 10^5\)
線段樹
給定一個長度為 \(N\) 的正整數序列 \( a_1, ... , a_N\),接著有 \(Q\) 筆操作,每個操作可能是下列形式的其中一個:
\(N, Q \leq 2 \times 10^5\)
Source: Leetcode
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define starburst ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define maxn 200005
struct SegmentTree{
int node[4*maxn];
void modify(int l, int r, int i, int p, int x){
if(l == r){
node[i] = x;
return;
}
int mid = (l + r) / 2, ls = 2 * i, rs = 2 * i + 1;
if(p <= mid) modify(l, mid, ls, p, x);
else modify(mid + 1, r, rs, p, x);
node[i] = node[ls] + node[rs];
}
int query(int l, int r, int i, int ql, int qr){
if(ql <= l && r <= qr){
return node[i];
}
int ret = 0, mid = (l + r) / 2, ls = 2 * i, rs = 2 * i + 1;
if(ql <= mid) ret += query(l, mid, ls, ql, qr);
if(mid < qr) ret += query(mid + 1, r, rs, ql, qr);
return ret;
}
}seg;
void solve(){
int n, q; cin >> n >> q;
for(int i = 1; i <= n; ++i){
int ai; cin >> ai;
seg.modify(1, n, 1, i, ai);
}
for(; q; q--){
int op; cin >> op;
if(op == 1){
int l, r; cin >> l >> r;
cout << seg.query(1, n, 1, l, r) << '\n';
}else {
int p, x; cin >> p >> x;
seg.modify(1, n, 1, p, x);
}
}
}
signed main(){
starburst
int t = 1; //cin >> t;
while(t--) solve();
}
事實上,線段樹能作到的事情遠不只如此
所有具有結合律的操作都可以用線段樹做!
像是區間Xor、區間極值都可以
接下來試試看做區間修改的線段樹
給定一個長度為 \(N\) 的正整數序列 \( a_1, ... , a_N\),接著有 \(Q\) 筆操作,每個操作可能是下列形式的其中一個:
\(N, Q \leq 2 \times 10^5\)
對於第一次遇到的人來說可能比較難想到
當我們遇到一個節點,節點的區間都在修改區間內的話
就直接改那個節點,打上標記
要碰子節點的時候將標記下推即可
等等畫圖解釋>.<
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define starburst ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define maxn 200005
#define ls (2*i)
#define rs (2*i+1)
#define mid ((l+r)/2)
struct SegmentTree{
int node[4*maxn], tag[4*maxn];
void push(int l, int r, int i){
int t = tag[i]; tag[i] = 0;
node[ls] += (mid - l + 1) * t, tag[ls] += t;
node[rs] += (r - mid) * t, tag[rs] += t;
node[i] = node[ls] + node[rs];
}
void modify(int l, int r, int i, int ql, int qr, int x){
if(ql <= l && r <= qr){
node[i] += (r - l + 1) * x;
tag[i] += x;
return;
}
push(l, r, i);
int mid = (l + r) / 2, ls = 2 * i, rs = 2 * i + 1;
if(ql <= mid) modify(l, mid, ls, p, x);
if(mid < qr) modify(mid + 1, r, rs, p, x);
node[i] = node[ls] + node[rs];
}
int query(int l, int r, int i, int ql, int qr){
if(ql <= l && r <= qr){
return node[i];
}
push(l, r, i);
int ret = 0, mid = (l + r) / 2, ls = 2 * i, rs = 2 * i + 1;
if(ql <= mid) ret += query(l, mid, ls, ql, qr);
if(mid < qr) ret += query(mid + 1, r, rs, ql, qr);
return ret;
}
}seg;
void solve(){
int n, q; cin >> n >> q;
for(int i = 1; i <= n; ++i){
int ai; cin >> ai;
seg.modify(1, n, 1, i, i, ai);
}
for(; q; q--){
int op; cin >> op;
if(op == 1){
int l, r; cin >> l >> r;
cout << seg.query(1, n, 1, l, r) << '\n';
}else {
int l, r, x; cin >> l >> r >> x;
seg.modify(1, n, 1, l, r, x);
}
}
}
signed main(){
starburst
int t = 1; //cin >> t;
while(t--) solve();
}
給定一個長度為 \(N\) 的正整數序列 \( a_1, ... , a_N\),接著有 \(Q\) 筆操作,操作為以下形式:
請在每筆操作後輸出最大子區間和
\(N, Q \leq 2 \times 10^5\)
我們不難發現,這題節點只存一個整數做不了事情
不妨考慮存四個資訊:最大前綴、最大後綴、最大子區間和、區間和
從這裡可以知道,其實線段樹的節點存的可以是任何東西
你甚至可以在線段樹內套一棵線段樹
這樣就可以做到二維的區間修改/查詢
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
#define starburst ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define maxn 200005
#define mid ((l+r)/2)
#define ls (2*i)
#define rs (ls+1)
struct SegmentTree{
struct Node{
int pre, suf, sum, mx;
}node[maxn * 4];
void update(int x, int i){
node[i].pre = node[i].suf = node[i].sum = node[i].mx = x;
}
void pull(Node &cur, Node L, Node R){
cur.pre = max(L.pre, L.sum + R.pre);
cur.suf = max(R.suf, R.sum + L.suf);
cur.sum = L.sum + R.sum;
cur.mx = max(max(L.mx, R.mx), L.suf + R.pre);
}
void modify(int l, int r, int i, int p, int x){
if(l == r){
update(x, i);
return;
}
if(p <= mid) modify(l, mid, ls, p, x);
else modify(mid + 1, r, rs, p, x);
pull(node[i], node[ls], node[rs]);
}
}seg;
void solve(){
int n, q; cin >> n >> q;
for(int i = 1; i <= n; ++i) {
int x; cin >> x;
seg.modify(1, n, 1, i, x);
}
for(; q; q--){
int k, x; cin >> k >> x;
seg.modify(1, n, 1, k, x);
cout << max(0ll, seg.node[1].mx) << '\n';
}
}
signed main(){
starburst
int t = 1; //cin >> t;
while(t--) solve();
}
列出DP式:\( DP[i] = \max_{h[j] < h[i]}\{DP[j] + a_i\}\)
看起來轉移要\(O(N)\)
我們發現可以對\(h\)的值域開線段樹
這樣就可以達到\(O(logN)\)查詢最大值
成功把時間壓下來了
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
#define starburst ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define maxn 200005
#define ls (2 * i)
#define rs (2 * i + 1)
#define mid ((l + r) / 2)
struct SegmentTree{
int val[maxn * 4];
void modify(int l, int r, int i, int p, int x){
if(l == r){
val[i] = x;
return;
}
if(p <= mid) modify(l, mid, ls, p, x);
else modify(mid + 1, r, rs, p, x);
val[i] = max(val[ls], val[rs]);
}
int query(int l, int r, int i, int ql, int qr){
if(ql <= l && r <= qr){
return val[i];
}
int ret = 0;
if(ql <= mid) ret = max(ret, query(l, mid, ls, ql, qr));
if(mid < qr) ret = max(ret, query(mid + 1, r, rs, ql, qr));
return ret;
}
}seg;
void solve(){
int n; cin >> n;
vector<int> dp(n, 0), h(n, 0), a(n, 0);
for(auto &x:h) cin >> x;
for(auto &x:a) cin >> x;
for(int i = 0; i < n; ++i){
dp[i] = (h[i] == 1 ? 0 : seg.query(1, n, 1, 1, h[i] - 1)) + a[i];
seg.modify(1, n, 1, h[i], dp[i]);
}
cout << *max_element(dp.begin(), dp.end());
}
signed main(){
starburst
int t = 1; //cin >> t;
while(t--) solve();
}
其他有關線段樹的技巧:
掃描線
二維線段樹
動態開點
持久化線段樹
戰鬥線段樹
李超線段樹
時間線段樹
線段樹優化建圖
Pattern
吉如一線段樹(Segment Tree Beats)
但題目裡可能會出現,可以等學會了再寫
有興趣可以先上網學
等等會抓幾題講解