# Range Queries(25 題) ## Static Range Sum Queries [problem](https://cses.fi/problemset/task/1646) ```markdown 題目: 給定一個長度為 n 的整數陣列,以及 m 組查詢,每次查詢請輸出區間 [a, b] 內所有數字的總和。 ``` ``` markdown 解法 : 這是一題經典的前綴和問題,先用 prefix[i] 表示前 i 項的總和, 那麼任意區間 [a, b] 的總和就可以用 prefix[b] - prefix[a-1] 得到。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; #define int long long #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) signed main(){ fastio; int n, m; cin >> n >> m; vector<int> prefix(n + 1, 0); for(int i = 1; i <= n; ++i){ int num; cin >> num; prefix[i] = num + prefix[i - 1]; } while(m--){ int a, b; cin >> a >> b; cout << prefix[b] - prefix[a - 1] << '\n'; } return 0; } ``` ## Static Range Minimum Queries [problem](https://cses.fi/problemset/task/1647) ```markdown 題目: - 給定一個長度為 n 的整數陣列。 - 接著有 m 筆查詢,每次查詢給一段區間 [a, b],請輸出該區間內的最小值。 - 資料不會修改,屬於**靜態 RMQ 問題**。 ``` ``` markdown 解法 : ### 1. Segment Tree 預處理 - 對原始陣列建 Segment Tree,記錄每個區間的最小值。 - 查詢時透過 divide-and-conquer 往左右子樹查詢交集區間。 --- ### 2. 操作說明 - `build()`:遞迴建樹。 - `query()`:查詢區間最小值。 ``` ``` cpp #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; const int MAXN = 2e5 + 5; int n, q; int a[MAXN]; int seg[4 * MAXN]; void build(int node, int l, int r) { if (l == r) { seg[node] = a[l]; return; } int mid = (l + r) / 2; build(2 * node, l, mid); build(2 * node + 1, mid + 1, r); seg[node] = min(seg[2 * node], seg[2 * node + 1]); } int query(int node, int l, int r, int ql, int qr) { if (qr < l || r < ql) return INT_MAX; if (ql <= l && r <= qr) return seg[node]; int mid = (l + r) / 2; return min(query(2 * node, l, mid, ql, qr), query(2 * node + 1, mid + 1, r, ql, qr)); } int main() { fastio; cin >> n >> q; for (int i = 0; i < n; ++i) cin >> a[i]; build(1, 0, n - 1); while (q--) { int l, r; cin >> l >> r; cout << query(1, 0, n - 1, l - 1, r - 1) << '\n'; } } ``` ## Dynamic Range Sum Queries [problem](https://cses.fi/problemset/task/1648) ```markdown 題目: - 有一個長度為 n 的整數陣列。 - 操作共分兩種: 1. 將第 a 個數字更新為 b。 2. 查詢某區間 [a, b] 的總和。 - 要處理 m 筆操作,且需在快時間內完成。 ``` ``` markdown 解法 : ### 1. Segment Tree + 單點更新 - 因為每次更新只針對單一點(a 變成 b),不需要區間加法或乘法。 - 可以使用 Segment Tree 並進行 **單點賦值操作(point update)**。 - 查詢則為區間加總(range sum)。 --- ### 2. 實作說明 - 為了復用 Lazy Segment Tree 的模板,這份程式碼套用了「區間賦值 + 區間加總」的通用寫法。 - 在初始化時,將每個數字作為區間長度為 1 的賦值操作。 - 在更新時,也僅更新單點 (a = b)。 - 雖然 lazy 多此一舉。 ``` ``` cpp #include <bits/stdc++.h> #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; struct LazySegTree { int n; vector<long long> tree, lazy; LazySegTree(int size) { n = size; tree.assign(4 * n, 0); lazy.assign(4 * n, 0); } void push(int node, int l, int r) { if (lazy[node] != 0) { tree[node] = (r - l + 1) * lazy[node]; if (l != r) { lazy[2 * node] = lazy[node]; lazy[2 * node + 1] = lazy[node]; } lazy[node] = 0; } } void update(int node, int l, int r, int ul, int ur, long long val) { push(node, l, r); if (r < ul || ur < l) return; if (ul <= l && r <= ur) { lazy[node] = val; push(node, l, r); return; } int mid = (l + r) / 2; update(2 * node, l, mid, ul, ur, val); update(2 * node + 1, mid + 1, r, ul, ur, val); tree[node] = (tree[2 * node] + tree[2 * node + 1]); } long long query(int node, int l, int r, int ql, int qr) { push(node, l, r); if (r < ql || qr < l) return 0; if (ql <= l && r <= qr) return tree[node]; int mid = (l + r) / 2; return (query(2 * node, l, mid, ql, qr) + query(2 * node + 1, mid + 1, r, ql, qr)); } // wrapper functions void add(int l, int r, long long val) { update(1, 0, n - 1, l, r, val); } long long range_sum(int l, int r) { return query(1, 0, n - 1, l, r); } }; int main(){ fastio; int n, m; cin >> n >> m; LazySegTree st(n); for(int i = 0; i < n; ++i){ int num; cin >> num; st.add(i, i, num); } while(m--){ int c, a, b; cin >> c >> a >> b; if(--c) { cout << st.range_sum(a - 1, b - 1) << '\n'; } else { st.add(a - 1, a - 1, b); } } } ``` ## Dynamic Range Minimum Queries [problem](https://cses.fi/problemset/task/1649) ```markdown 題目: - 給你一個長度為 n 的整數陣列。 - 有 m 筆操作,操作包含兩種: 1. 更新第 a 個位置的值為 b。 2. 查詢區間 [a, b] 的最小值。 - 要求高效地完成所有操作。 ``` ``` markdown 解法 : ### 1. Segment Tree + 單點更新 - 使用 Segment Tree 解決區間最小值查詢。 - 此模板用 Lazy Propagation 架構撰寫,但實際操作只有單點更新(區間長度為 1 的賦值)。 --- ### 2. 資料結構說明 - `tree[]`: 儲存每個區間的最小值。 - `lazy[]`: 預設為 -1,僅用於懶標記區間賦值。 - `add(l, r, val)`: 更新區間 [l, r] 的值為 val,這題中實際上只會呼叫 `add(i, i, val)`。 - `range_sum(l, r)`: 查詢區間最小值(其實是 `range_min()`,命名只是保留模板習慣)。 ``` ``` cpp #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; struct LazySegTree { int n; vector<long long> tree, lazy; LazySegTree(int size) { n = size; tree.assign(4 * n, LLONG_MAX); lazy.assign(4 * n, -1); } void push(int node, int l, int r) { if (lazy[node] != -1) { tree[node] = lazy[node]; if (l != r) { lazy[2 * node] = lazy[node]; lazy[2 * node + 1] = lazy[node]; } lazy[node] = -1; } } void update(int node, int l, int r, int ul, int ur, long long val) { push(node, l, r); if (r < ul || ur < l) return; if (ul <= l && r <= ur) { lazy[node] = val; push(node, l, r); return; } int mid = (l + r) / 2; update(2 * node, l, mid, ul, ur, val); update(2 * node + 1, mid + 1, r, ul, ur, val); tree[node] = min(tree[2 * node] , tree[2 * node + 1]); } long long query(int node, int l, int r, int ql, int qr) { push(node, l, r); if (r < ql || qr < l) return LLONG_MAX; if (ql <= l && r <= qr) return tree[node]; int mid = (l + r) / 2; return min(query(2 * node, l, mid, ql, qr) , query(2 * node + 1, mid + 1, r, ql, qr)); } // wrapper functions void add(int l, int r, long long val) { update(1, 0, n - 1, l, r, val); } long long range_sum(int l, int r) { return query(1, 0, n - 1, l, r); } }; int main(){ fastio; int n, m; cin >> n >> m; LazySegTree st(n); for(int i = 0; i < n; ++i){ int num; cin >> num; st.add(i, i, num); } while(m--){ int c, a, b; cin >> c >> a >> b; if(--c) { cout << st.range_sum(a - 1, b - 1) << '\n'; } else { st.add(a - 1, a - 1, b); } } } ``` ## Range Xor Queries [problem](https://cses.fi/problemset/task/1650) ```markdown 題目: - 給定一個長度為 n 的陣列。 - 有 m 筆查詢,每次查詢一段區間 [a, b] 的 XOR 值。 - 所有查詢為靜態查詢,不包含修改。 ``` ``` markdown 解法 : ### 1. 前綴 XOR 陣列(Prefix XOR) - 定義 prefix[i] 為前 i 項的 XOR 結果,即 prefix[i] = a[1] ^ a[2] ^ ... ^ a[i] - 區間 [a, b] 的 XOR 結果為:`prefix[b] ^ prefix[a - 1]` - 原理同前綴和,只是把 `+` 換成了 `^`,且 XOR 符合交換與結合律。 --- ### 2. 為何不用 Segment Tree? - 因為這題資料不會被修改,屬於「純查詢」問題,使用 prefix array 更簡潔且效率更高。 ``` ``` cpp #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; int main(){ fastio; int n, m; cin >> n >> m; vector<long long> prefix(n + 1, 0); for(int i = 1; i <= n; ++i){ int num; cin >> num; prefix[i] = prefix[i - 1] ^ num; } while(m--){ int a, b; cin >> a >> b; cout << (prefix[b] ^ prefix[a - 1]) << '\n'; } } ``` ## Range Update Queries [problem](https://cses.fi/problemset/task/1651) ```markdown 題目: - 給你一個長度為 n 的整數陣列。 - 有兩種操作,共 m 筆: 1. 將區間 [a, b] 中的每個數字加上某個值。 2. 查詢某個位置的當前數值。 - 需要高效處理所有操作。 ``` ``` markdown 解法 : ### 1. Lazy Segment Tree(支援區間加法 + 單點查詢) - 標準 Segment Tree 無法快速處理區間加法 → 改用 Lazy Propagation。 - 區間加法時,用 lazy 陣列標記下推。 - 查詢時從上到下 push 過所有影響當前節點的 lazy。 --- ### 2. 核心函數說明: - `add(l, r, val)`:區間加法操作。 - `range_sum(l, r)`:查詢區間總和,這題中實際只會查詢單點 (l = r)。 ``` ``` cpp #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; struct LazySegTree { int n; vector<long long> tree, lazy; LazySegTree(int size) { n = size; tree.assign(4 * n, 0); lazy.assign(4 * n, 0); } void push(int node, int l, int r) { if (lazy[node] != 0) { tree[node] += (r - l + 1) * lazy[node]; if (l != r) { lazy[2 * node] += lazy[node]; lazy[2 * node + 1] += lazy[node]; } lazy[node] = 0; } } void update(int node, int l, int r, int ul, int ur, long long val) { push(node, l, r); if (r < ul || ur < l) return; if (ul <= l && r <= ur) { lazy[node] = val; push(node, l, r); return; } int mid = (l + r) / 2; update(2 * node, l, mid, ul, ur, val); update(2 * node + 1, mid + 1, r, ul, ur, val); tree[node] = (tree[2 * node] + tree[2 * node + 1]); } long long query(int node, int l, int r, int ql, int qr) { push(node, l, r); if (r < ql || qr < l) return 0; if (ql <= l && r <= qr) return tree[node]; int mid = (l + r) / 2; return (query(2 * node, l, mid, ql, qr) + query(2 * node + 1, mid + 1, r, ql, qr)); } // wrapper functions void add(int l, int r, long long val) { update(1, 0, n - 1, l, r, val); } long long range_sum(int l, int r) { return query(1, 0, n - 1, l, r); } }; int main(){ fastio; int n, m; cin >> n >> m; LazySegTree st(n); for(int i = 0; i < n; ++i){ int num; cin >> num; st.add(i, i, num); } while(m--){ int c; cin >> c; if(--c){ int k; cin >> k; cout << st.range_sum(k-1, k-1) << '\n'; } else { int a, b, num; cin >> a >> b >> num; st.add(a - 1, b - 1, num); } } } ``` ## Forest Queries [problem](https://cses.fi/problemset/task/1652) ```markdown 題目: - 給定一個 n × n 的地圖,每格是 `*`(代表一棵樹)或 `.`(空地)。 - 接著有 m 筆查詢,每次查詢矩形區域內有幾棵樹。 - 要求能在快速時間內處理所有查詢。 ``` ``` markdown 解法 : ### 1. 2D 前綴和(Prefix Sum 2D) - 建立一個 2D 陣列 `forest[y][x]`,代表從 (1,1) 到 (y,x) 區域內的樹木總數。 - 每格的值可以由以下公式計算: forest[i][j] = forest[i-1][j] + forest[i][j-1] - forest[i-1][j-1] + (s[j-1] == '*'); - 每次查詢 [y1, x1] 到 [y2, x2] 的總樹數為: forest[y2][x2] - forest[y1-1][x2] - forest[y2][x1-1] + forest[y1-1][x1-1] ``` ``` cpp #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; int main(){ fastio; int n, m; cin >> n >> m; vector<vector<long long>> forest(n + 1, vector<long long>(n + 1, 0)); for(int i = 1; i <= n; ++i){ string s; cin >> s; for(int j = 1; j <= n; ++j){ forest[i][j] = forest[i - 1][j] + forest[i][j - 1] - forest[i - 1][j - 1] + (s[j - 1] == '*'); } } while(m--){ int x1, x2, y1, y2; cin >> y1 >> x1 >> y2 >> x2; cout << forest[y2][x2] + forest[y1 - 1][x1 - 1] - forest[y2][x1 - 1] - forest[y1 - 1][x2]<< '\n'; } } ``` ## Hotel Queries [problem](https://cses.fi/problemset/task/1143) ```markdown 題目: - 有 n 間旅館,每間可容納的最大人數為 h₁, h₂, ..., hₙ。 - 有 m 組客人,每組人數為 a。 - 對每組客人,找最靠左的旅館且尚有足夠空間接待,並讓他們入住(容量減少)。 - 如果找不到,輸出 0;否則輸出該旅館的編號(從 1 開始)。 ``` ``` markdown 解法 : ### 1. Segment Tree + Max + First-Above 查詢 - 每個節點維護區間內最大的剩餘容量。 - 對每組客人,從 root 開始往左右子樹找出 **最靠左且 ≥ a 的節點**。 - 找到後將該點減少 a,並回傳編號。 --- ### 2. 實作要點 - 使用 Lazy Segment Tree 模板,雖然這題實際上並無需要 lazy 的區間操作,但沿用 template 可 快速實作。 - `query()` 採用「左優先遞迴」方式找最靠左的符合條件者。 - 為保留原始值,用 `hotel[]` 陣列記錄旅館目前剩餘容量。 ``` ``` cpp #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; struct LazySegTree { int n; vector<long long> tree, lazy; LazySegTree(int size) { n = size; tree.assign(4 * n, 0); lazy.assign(4 * n, -1); } void push(int node, int l, int r) { if (lazy[node] != -1) { tree[node] = lazy[node]; if (l != r) { lazy[2 * node] = lazy[node]; lazy[2 * node + 1] = lazy[node]; } lazy[node] = -1; } } void update(int node, int l, int r, int ul, int ur, long long val) { push(node, l, r); if (r < ul || ur < l) return; if (ul <= l && r <= ur) { lazy[node] = val; push(node, l, r); return; } int mid = (l + r) / 2; update(2 * node, l, mid, ul, ur, val); update(2 * node + 1, mid + 1, r, ul, ur, val); tree[node] = max(tree[2 * node] , tree[2 * node + 1]); } long long query(int node, int l, int r, int num) { push(node, l, r); long long mid = (l + r) / 2; long long L = LLONG_MAX, R = LLONG_MAX; if(l == r){ if(tree[node] >= num){ return mid; } else { return LLONG_MAX; } } else if(tree[2 * node] >= num){ L = query(2 * node, l, mid, num); } else if(tree[2 * node + 1] >= num){ R = query(2 * node + 1, mid + 1, r, num); } return min(L, R); } // wrapper functions void add(int l, int r, long long val) { update(1, 0, n - 1, l, r, val); } long long range_query(int num) { return query(1, 0, n - 1, num); } }; int main(){ fastio; int n, m; cin >> n >> m; vector<int> hotel(n); LazySegTree st(n); for(int i = 0; i < n; ++i){ int num; cin >> num; st.add(i, i, num); hotel[i] = num; } while(m--){ int a; cin >> a; long long ans = st.range_query(a); if(ans != LLONG_MAX){ st.add(ans, ans, hotel[ans] - a); hotel[ans] -= a; cout << ans + 1 << ' '; } else { cout << 0 << ' '; } } } ``` ## List Removals [problem](https://cses.fi/problemset/task/1749) ```markdown 題目: - 給定一個長度為 n 個陣列,每個元素編號為 1 到 n。 - 每次 query 刪掉第 k 個元素,並輸出該元素。 ``` ``` markdown 解法 : ### 1. 使用 Segment Tree 解決動態 k-th 人問題 - 我們將所有元素用一個陣列 `element[]` 表示。 - 建立一個 Segment Tree,節點值表示某區間內還活著的元素(即 1 表示該元素還在)。 - 每次要刪除第 k 個元素,可以視為從整體中找第 k 個 1(k-th one),這可以透過 Segment Tree 快速達成。 - 找到後將該元素設為 0(標記為已刪除)。 --- ### 2. 注意事項 - `range_sum(k)`:在 Segment Tree 中找到前綴和為 k 的最左位置。 - `add(pos, pos, 0)`:將該位置設為 0,代表這個元素已經移除。 - 每回合讀入的 `a` 就是我們要找的第 a 個活元素。 ``` ``` cpp #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; struct LazySegTree { int n; vector<long long> tree, lazy; LazySegTree(int size) { n = size; tree.assign(4 * n, 0); lazy.assign(4 * n, -1); } void push(int node, int l, int r) { if (lazy[node] != -1) { tree[node] = (r - l + 1) * lazy[node]; if (l != r) { lazy[2 * node] += lazy[node]; lazy[2 * node + 1] += lazy[node]; } lazy[node] = -1; } } void update(int node, int l, int r, int ul, int ur, long long val) { push(node, l, r); if (r < ul || ur < l) return; if (ul <= l && r <= ur) { lazy[node] = val; push(node, l, r); return; } int mid = (l + r) / 2; update(2 * node, l, mid, ul, ur, val); update(2 * node + 1, mid + 1, r, ul, ur, val); tree[node] = (tree[2 * node] + tree[2 * node + 1]); } long long query(int node, int l, int r, int num) { push(node, l, r); long long mid = (l + r) / 2; long long L = LLONG_MAX, R = LLONG_MAX; if(l == r){ if(tree[node] >= num){ return l; } else { return LLONG_MAX; } } else if(tree[2 * node] >= num){ L = query(2 * node, l, mid, num); } else if(tree[2 * node + 1] >= num - tree[2 * node]){ R = query(2 * node + 1, mid + 1, r, num - tree[2 * node]); } return min(L, R); } // wrapper functions void add(int l, int r, long long val) { update(1, 0, n - 1, l, r, val); } long long range_sum(int num) { return query(1, 0, n - 1, num); } }; int main(){ fastio; int n, m; cin >> n; LazySegTree st(n); vector<long long> element(n); for(int i = 0; i < n; ++i){ int num; cin >> num; st.add(i, i, 1); element[i] = num; } while(n--){ int a; cin >> a; long long pos = st.range_sum(a); cout << element[pos] << ' '; st.add(pos, pos, 0); } } ``` ## Salary Queries [problem](https://cses.fi/problemset/task/1144) ```markdown 題目: - 有 n 位員工,每人有一份薪水。 - 有 q 筆操作,包含: - `! a b`: 將第 a 位員工的薪水更新為 b。 - `? a b`: 詢問薪資範圍在 [a, b] 之間的員工有多少位。 - 每次查詢都需要快速回應。 ``` ``` markdown 解法 : ### 1. 離散化 + Binary Indexed Tree(Fenwick Tree) - 薪資值範圍過大,需先將所有出現過的薪資值進行離散化。 - 使用 BIT 維護薪資值的出現次數。 - 查詢 [a, b] 轉為 prefix sum 差值:`sum(b_idx) - sum(a_idx - 1)` - 更新員工薪資時,先從舊值位置 -1,再在新值位置 +1。 --- ### 2. 離散化細節 - 收集所有出現過的薪資值(初始 + 所有查詢中的 b)。 - 使用 `lower_bound()` 與 `upper_bound()` 找到對應的區間範圍。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 4e5 + 5; int BIT[MAXN]; void add(int idx, int val) { while (idx < MAXN) { BIT[idx] += val; idx += idx & -idx; } } int sum(int idx) { int res = 0; while (idx > 0) { res += BIT[idx]; idx -= idx & -idx; } return res; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, q; cin >> n >> q; vector<int> p(n + 1); vector<pair<char, pair<int, int>>> queries; vector<int> all_vals; for (int i = 1; i <= n; ++i) { cin >> p[i]; all_vals.push_back(p[i]); } for (int i = 0; i < q; ++i) { char type; int a, b; cin >> type >> a >> b; queries.emplace_back(type, make_pair(a, b)); all_vals.push_back(b); // 不管是 ? 還是 ! 都會用到 b } // 離散化 sort(all_vals.begin(), all_vals.end()); all_vals.erase(unique(all_vals.begin(), all_vals.end()), all_vals.end()); auto get_index = [&](int x) -> int { return lower_bound(all_vals.begin(), all_vals.end(), x) - all_vals.begin() + 1; }; vector<int> salary_idx(n + 1); for (int i = 1; i <= n; ++i) { salary_idx[i] = get_index(p[i]); add(salary_idx[i], 1); } for (auto &[type, ab] : queries) { int a = ab.first, b = ab.second; if (type == '!') { add(salary_idx[a], -1); salary_idx[a] = get_index(b); add(salary_idx[a], 1); } else { if (a > b) { cout << "0\n"; continue; } int la = lower_bound(all_vals.begin(), all_vals.end(), a) - all_vals.begin() + 1; int rb = upper_bound(all_vals.begin(), all_vals.end(), b) - all_vals.begin(); if (la > rb) cout << "0\n"; else cout << sum(rb) - sum(la - 1) << '\n'; } } return 0; } ``` ## Prefix Sum Queries [problem](https://cses.fi/problemset/task/2166) ```markdown 題目: - 有一個長度為 n 的整數陣列。 - q 筆操作,包含: 1. `1 i x`:將第 i 個元素設為 x。 2. `2 l r`:查詢區間 [l, r] 的最大子陣列和。 - 要求高效完成所有操作。 ``` ``` markdown 解法 : ### 1. 使用 Prefix Sum + Segment Tree with Lazy Propagation 這題可以只用標準 Segment Tree,但我們沿用更通用的做法: - 計算 prefix sum 陣列 `prefix[i] = a[1] + ... + a[i]` - 利用 segment tree 維護區間的 prefix 最大值 - 更新第 i 項時,相當於將第 i~n 區間都進行加法 → 用 **lazy propagation 做 range add** - 查詢區間最大子段和 `a[l..r]`,即為 `max(prefix[r]) - prefix[l-1]` --- ### 2. 節點資訊 每個節點維護: - 區間最大值 `max_val` - 懶標記 `lazy`:表示尚未傳遞的區間加總 查詢時需加上懶標記進行正確比對。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; #define ll long long struct Node { ll sum, prefix, suffix, res; }; int n, q; vector<Node> tree; vector<ll> a; Node merge(Node l, Node r) { Node res; res.sum = l.sum + r.sum; res.prefix = max(l.prefix, l.sum + r.prefix); res.suffix = max(r.suffix, r.sum + l.suffix); res.res = max({l.res, r.res, l.suffix + r.prefix}); return res; } void build(int node, int l, int r) { if (l == r) { ll val = a[l]; tree[node] = {val, max(0LL, val), max(0LL, val), max(0LL, val)}; return; } int mid = (l + r) / 2; build(2 * node, l, mid); build(2 * node + 1, mid + 1, r); tree[node] = merge(tree[2 * node], tree[2 * node + 1]); } void update(int node, int l, int r, int idx, ll val) { if (l == r) { tree[node] = {val, max(0LL, val), max(0LL, val), max(0LL, val)}; return; } int mid = (l + r) / 2; if (idx <= mid) update(2 * node, l, mid, idx, val); else update(2 * node + 1, mid + 1, r, idx, val); tree[node] = merge(tree[2 * node], tree[2 * node + 1]); } Node query(int node, int l, int r, int ql, int qr) { if (qr < l || r < ql) return {0, 0, 0, 0}; // neutral element if (ql <= l && r <= qr) return tree[node]; int mid = (l + r) / 2; return merge( query(2 * node, l, mid, ql, qr), query(2 * node + 1, mid + 1, r, ql, qr) ); } int main() { ios::sync_with_stdio(0); cin.tie(0); cin >> n >> q; a.resize(n + 1); tree.resize(4 * (n + 1)); for (int i = 1; i <= n; ++i) cin >> a[i]; build(1, 1, n); while (q--) { int type, x, y; cin >> type >> x >> y; if (type == 1) { update(1, 1, n, x, y); } else { cout << query(1, 1, n, x, y).res << '\n'; } } } ``` ## Pizzeria Queries [problem](https://cses.fi/problemset/task/2206) ```markdown 題目: - 有 n 個披薩店,每家店的位置為其在陣列中的編號(0-based)。 - 每家店的披薩價格為 p[i],但實際價格會因顧客與披薩店的距離產生變化。 - 顧客與披薩店的距離為 |i - k|,所以顧客要付的價格為 p[i] + |i - k|。 - 有兩種操作: - `1 k x`:把第 k 家披薩店價格更新為 x - `2 k`:查詢顧客在第 k 個位置要花的最小費用。 ``` ``` markdown 解法 : ### 1. 把絕對值變成兩個區段討論: - 目標是最小化 p[i] + |i - k|。 - 當 i <= k 時:p[i] + (k - i) = (p[i] - i) + k - 當 i >= k 時:p[i] + (i - k) = (p[i] + i) - k - 所以可以將原本的 p[i] 轉換成兩個陣列: - left[i] = p[i] - i,對應於 i ≤ k - right[i] = p[i] + i,對應於 i ≥ k --- ### 2. 建兩棵 Segment Tree: - `segL` 用來維護 left[i],查詢範圍為 [0, k] - `segR` 用來維護 right[i],查詢範圍為 [k, n-1] - 每次查詢答案為: - `min( segL.query(0, k) + k, segR.query(k, n-1) - k )` --- ### 3. 更新操作: - 每次把第 k 家店的價錢改為 x,需要更新: - left[k] = x - k - right[k] = x + k - 分別更新 `segL` 和 `segR`。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; #define int long long const int INF = 1e18; struct SegmentTree { int n; vector<int> tree; SegmentTree(int sz) { n = sz; tree.assign(4*n, INF); } void build(int idx, int l, int r, const vector<int>& arr) { if (l == r) { tree[idx] = arr[l]; return; } int mid = (l+r)/2; build(2*idx, l, mid, arr); build(2*idx+1, mid+1, r, arr); tree[idx] = min(tree[2*idx], tree[2*idx+1]); } void update(int idx, int l, int r, int pos, int val) { if (l == r) { tree[idx] = val; return; } int mid = (l+r)/2; if (pos <= mid) update(2*idx, l, mid, pos, val); else update(2*idx+1, mid+1, r, pos, val); tree[idx] = min(tree[2*idx], tree[2*idx+1]); } int query(int idx, int l, int r, int ql, int qr) { if (qr < l || r < ql) return INF; if (ql <= l && r <= qr) return tree[idx]; int mid = (l+r)/2; return min(query(2*idx, l, mid, ql, qr), query(2*idx+1, mid+1, r, ql, qr)); } }; inline void fastio() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); } int32_t main() { fastio(); int n, q; cin >> n >> q; vector<int> p(n); for (int i = 0; i < n; ++i) cin >> p[i]; vector<int> left(n), right(n); for (int i = 0; i < n; ++i) { left[i] = p[i] - i; right[i] = p[i] + i; } SegmentTree segL(n), segR(n); segL.build(1, 0, n-1, left); segR.build(1, 0, n-1, right); while (q--) { int type; cin >> type; if (type == 1) { int k, x; cin >> k >> x; --k; segL.update(1, 0, n-1, k, x-k); segR.update(1, 0, n-1, k, x+k); } else { int k; cin >> k; --k; int res = INF; res = min(res, segL.query(1, 0, n-1, 0, k) + k); res = min(res, segR.query(1, 0, n-1, k, n-1) - k); cout << res << '\n'; } } return 0; } ``` ## Visible Buildings Queries [problem](https://cses.fi/problemset/task/3304) ```markdown 題目: - 給定一列建築物的高度 h[1..n]。 - 一個建築物可以「看到」右邊第一個比自己高的建築。 - 給 q 組查詢,每次問從第 a 棟出發,往右最多看到幾棟建築(包括自己),且不能超過第 b 棟。 ``` ``` markdown 解法 : ### 1. 預處理每棟樓能看到的下一棟: - 用 stack 從右往左掃,維護「右邊第一個比自己高」的建築編號。 - 記錄為 `nxt[i]`,代表從 i 能跳到哪一棟。 --- ### 2. Binary Lifting 加速跳躍: - 建立 jump table:jump[k][i] 表示從 i 出發,跳 2^k 次會到哪棟。 - 初始 jump[0][i] = nxt[i] - 遞推 jump[k][i] = jump[k-1][jump[k-1][i]] - 預處理時間 O(n log n) --- ### 3. 查詢: - 對於每次查詢 (a, b),從 a 開始跳。 - 若 jump[k][curr] ≤ b 就可以跳,並累加 (1 << k) 到答案。 - 每次查詢最多跳 log n 次,時間 O(log n) ``` ``` cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 2e5 + 5; const int LOG = 20; int n, q; int h[MAXN]; // height of buildings, 1-based int nxt[MAXN]; // nxt[i] = next visible building from i int jump[LOG][MAXN]; // jump[k][i] = from i, jump 2^k steps int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> q; for (int i = 1; i <= n; ++i) cin >> h[i]; // Step 1: 建 next[](右邊第一個比它高的建築) stack<int> s; for (int i = n; i >= 1; --i) { while (!s.empty() && h[s.top()] <= h[i]) s.pop(); if (!s.empty()) nxt[i] = s.top(); else nxt[i] = n + 1; // 超出界限 s.push(i); } // Step 2: 建 jump table (binary lifting) for (int i = 1; i <= n + 1; ++i) jump[0][i] = nxt[i]; jump[0][n + 1] = n + 1; // 防止越界亂跳 for (int k = 1; k < LOG; ++k) for (int i = 1; i <= n + 1; ++i) jump[k][i] = jump[k - 1][jump[k - 1][i]]; // Step 3: 處理每個 query while (q--) { int a, b; cin >> a >> b; int ans = 1; // 至少看到 a 自己 int curr = a; for (int k = LOG - 1; k >= 0; --k) { if (jump[k][curr] <= b) { curr = jump[k][curr]; ans += (1 << k); } } cout << ans << '\n'; } } ``` ## Range Interval Queries [problem](https://cses.fi/problemset/task/3163) ```markdown 題目: - 給定一個長度為 n 的整數陣列 x[1..n]。 - 有 m 個查詢,每次給定範圍 [a, b] 和數值範圍 [c, d]。 - 問在區間 x[a..b] 中,有多少元素的值落在 [c, d] 區間中。 ``` ``` markdown 解法 : ### 1. Wavelet Tree: - 這類屬於「區間值域查詢」,適合使用 Wavelet Tree。 - 時間複雜度為 O(log V),V 為值域大小。 - 這裡使用座標壓縮將值域壓縮到 0 ~ n-1,避免實際建到 1e9 的空間。 --- ### 2. 實作細節: - `raw`:將輸入陣列排序後去重,作為值域基準。 - `encode(val)`:將原始值映射到座標壓縮後的編號。 - 查詢時將 [c, d] 映射到編號區間 [l, r]。 - Wavelet Tree 支援查詢區間 [a, b] 內小於等於 k 的數量。 - 所以答案為 `LTE(a, b, r) - LTE(a, b, l - 1)`。 ``` ``` cpp #pragma GCC optimize("Ofast,unroll-loops,no-stack-protector") #pragma GCC target("avx2,bmi,bmi2,lzcnt,popcnt") #include <bits/stdc++.h> #define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; struct WaveletTree { int lo, hi; WaveletTree *l, *r; vector<int> b; WaveletTree(vector<int>::iterator from, vector<int>::iterator to, int x, int y) { lo = x, hi = y; if (from >= to || lo > hi) return; if (lo == hi) { b.resize(to - from + 1); iota(b.begin(), b.end(), 0); return; } int mid = (lo + hi) / 2; vector<int> left, right; b.reserve(to - from + 1); b.push_back(0); for (auto it = from; it != to; ++it) { if (*it <= mid) { left.push_back(*it); b.push_back(b.back() + 1); } else { right.push_back(*it); b.push_back(b.back()); } } l = new WaveletTree(left.begin(), left.end(), lo, mid); r = new WaveletTree(right.begin(), right.end(), mid + 1, hi); } int LTE(int lq, int rq, int k) { if (lq > rq || k < lo) return 0; if (hi <= k) return rq - lq + 1; return l->LTE(b[lq - 1] + 1, b[rq], k) + r->LTE(lq - b[lq - 1], rq - b[rq], k); } }; int main() { fastio; int n, m; cin >> n >> m; vector<int> x(n), c(m), d(m); for (int i = 0; i < n; ++i) cin >> x[i]; vector<tuple<int, int, int, int>> queries(m); for (int i = 0; i < m; ++i) { int a, b; cin >> a >> b >> c[i] >> d[i]; queries[i] = {a, b, c[i], d[i]}; } vector<int> raw(x.begin(), x.end()); sort(raw.begin(), raw.end()); raw.erase(unique(raw.begin(), raw.end()), raw.end()); auto encode = [&](int val) { return lower_bound(raw.begin(), raw.end(), val) - raw.begin(); }; vector<int> compressed(n); for (int i = 0; i < n; ++i) compressed[i] = encode(x[i]); WaveletTree wt(compressed.begin(), compressed.end(), 0, raw.size() - 1); for (int i = 0; i < m; ++i) { auto [a, b, cc, dd] = queries[i]; int l = lower_bound(raw.begin(), raw.end(), cc) - raw.begin(); int r = upper_bound(raw.begin(), raw.end(), dd) - raw.begin() - 1; if (l >= (int)raw.size() || r < 0 || l > r) { cout << 0 << '\n'; continue; } l = max(l, 0); r = min(r, (int)raw.size() - 1); cout << wt.LTE(a, b, r) - wt.LTE(a, b, l - 1) << '\n'; } } ``` ## Subarray Sum Queries [problem](https://cses.fi/problemset/task/1190) ```markdown 題目: - 給定一個長度為 n 的整數陣列。 - 有 m 筆操作: 1. `1 k x`:將第 k 個數字改為 x。 2. `2 a b`:查詢區間 [a, b] 中的最大子段和(可以是空子段,最小為 0)。 - 陣列可能包含負數,需快速更新與查詢。 ``` ``` markdown 解法 : ### 1. Segment Tree 維護最大子段和(Maximum Subarray Sum) 每個節點維護四個資訊: - `sum`:整個區間的總和 - `prefix`:區間最大前綴和 - `suffix`:區間最大後綴和 - `best`:區間內最大子段和 這些資訊可在合併左右子樹時維護: - res.sum = L.sum + R.sum; - res.prefix = max(L.prefix, L.sum + R.prefix); - res.suffix = max(R.suffix, R.sum + L.suffix); - res.best = max({L.best, R.best, L.suffix + R.prefix}); --- ### 2. 查詢邏輯 - 使用線段樹查詢區間 [l, r] 對應的節點合併結果。 - 若區間內元素皆為負數,返回 0 即為空子段和。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; struct Node { long long sum, prefix, suffix, best; Node(long long val = 0) { sum = prefix = suffix = best = val; } }; const int MAXN = 2e5 + 5; Node tree[4 * MAXN]; int arr[MAXN]; Node merge(const Node &left, const Node &right) { Node res; res.sum = left.sum + right.sum; res.prefix = max(left.prefix, left.sum + right.prefix); res.suffix = max(right.suffix, right.sum + left.suffix); res.best = max({left.best, right.best, left.suffix + right.prefix}); return res; } void build(int l, int r, int idx) { if (l == r) { tree[idx] = Node(arr[l]); return; } int mid = (l + r) / 2; build(l, mid, idx * 2); build(mid + 1, r, idx * 2 + 1); tree[idx] = merge(tree[idx * 2], tree[idx * 2 + 1]); } void update(int pos, int val, int l, int r, int idx) { if (l == r) { tree[idx] = Node(val); return; } int mid = (l + r) / 2; if (pos <= mid) update(pos, val, l, mid, idx * 2); else update(pos, val, mid + 1, r, idx * 2 + 1); tree[idx] = merge(tree[idx * 2], tree[idx * 2 + 1]); } Node query(int ql, int qr, int l, int r, int idx) { if (qr < l || r < ql) return Node(0); // neutral for sum, best=0 if (ql <= l && r <= qr) return tree[idx]; int mid = (l + r) / 2; Node left = query(ql, qr, l, mid, idx * 2); Node right = query(ql, qr, mid + 1, r, idx * 2 + 1); return merge(left, right); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; ++i) cin >> arr[i]; build(1, n, 1); while (m--) { int type, a, b; cin >> type >> a >> b; if (type == 1) { update(a, b, 1, n, 1); } else { cout << max(0LL, query(a, b, 1, n, 1).best) << '\n'; } } return 0; } ``` ## Subarray Sum Queries II [problem](https://cses.fi/problemset/task/3226) ```markdown 題目: - 給定長度為 n 的整數陣列 a[1..n]。 - 有 m 次查詢,每次輸入區間 [a, b],輸出該區間的最大子陣列和(最大連續子區間和)。 - 若區間內所有數都為負數,則輸出 0。 ``` ``` markdown 解法 : ### 1. 使用 Segment Tree 儲存區間資訊: - 每個節點維護四個值: - `sum`: 區間總和 - `prefix`: 從左邊開始的最大前綴和 - `suffix`: 從右邊開始的最大後綴和 - `best`: 該區間的最大子陣列和 --- ### 2. 合併邏輯(Merge): - `sum = L.sum + R.sum` - `prefix = max(L.prefix, L.sum + R.prefix)` - `suffix = max(R.suffix, R.sum + L.suffix)` - `best = max(L.best, R.best, L.suffix + R.prefix)` - 類似於線段樹版本的 Kadane's Algorithm ``` ``` cpp #include <bits/stdc++.h> using namespace std; struct Node { long long sum, prefix, suffix, best; Node(long long val = 0) { sum = prefix = suffix = best = val; } }; const int MAXN = 2e5 + 5; Node tree[4 * MAXN]; int arr[MAXN]; Node merge(const Node &left, const Node &right) { Node res; res.sum = left.sum + right.sum; res.prefix = max(left.prefix, left.sum + right.prefix); res.suffix = max(right.suffix, right.sum + left.suffix); res.best = max({left.best, right.best, left.suffix + right.prefix}); return res; } void build(int l, int r, int idx) { if (l == r) { tree[idx] = Node(arr[l]); return; } int mid = (l + r) / 2; build(l, mid, idx * 2); build(mid + 1, r, idx * 2 + 1); tree[idx] = merge(tree[idx * 2], tree[idx * 2 + 1]); } void update(int pos, int val, int l, int r, int idx) { if (l == r) { tree[idx] = Node(val); return; } int mid = (l + r) / 2; if (pos <= mid) update(pos, val, l, mid, idx * 2); else update(pos, val, mid + 1, r, idx * 2 + 1); tree[idx] = merge(tree[idx * 2], tree[idx * 2 + 1]); } Node query(int node, int l, int r, int ql, int qr) { if (r < ql || qr < l) return 0; if (ql <= l && r <= qr) return tree[node]; int mid = (l + r) / 2; return merge(query(2 * node, l, mid, ql, qr) , query(2 * node + 1, mid + 1, r, ql, qr)); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; ++i) cin >> arr[i]; build(1, n, 1); while (m--) { int a, b; cin >> a >> b; cout << max(0LL, query(1, 1, n, a, b).best) << '\n'; // 最大子陣列和(若全負則輸出 0) } return 0; } ``` ## Distinct Values Queries [problem](https://cses.fi/problemset/task/1734) ```markdown 題目: - 給定一個長度為 n 的整數陣列 a[1..n]。 - 有 q 組查詢,每次給定區間 [l, r],問該區間中有多少個「不同」的數值(distinct elements)。 ``` ``` markdown 解法 : ### 1. Mo's Algorithm(離線區間查詢技巧): - 適合處理所有查詢都已知,並且查詢不需要修改值的情況。 - 將所有查詢依據 block 編號與右端點排序,可保證單次移動成本 amortized 為 O(√n)。 - 每筆查詢以滑動窗口的方式維護當前區間 [curr_l, curr_r] 的答案。 - 本題的答案是當前區間中有多少個不同的元素(可以用 `frequency[x] == 1` 來判斷)。 --- ### 2. 離散化(Coordinate Compression): - 由於 a[i] 數值可能過大(最多 1e9),須先離散化。 - 排序 + 去重後將所有值映射為 1 ~ n 範圍內的編號,方便開 frequency 陣列。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; const int N = 2e5 + 5; int n, q, block_size; int A[N], ans[N]; struct Query { int l, r, idx; bool operator<(const Query& other) const { if (l / block_size != other.l / block_size) return l / block_size < other.l / block_size; return r < other.r; } }; vector<Query> queries; int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> q; block_size = sqrt(n); vector<int> raw(n+1); for (int i = 1; i <= n; ++i) cin >> raw[i]; // 離散化 vector<int> tmp = raw; sort(tmp.begin() + 1, tmp.end()); tmp.erase(unique(tmp.begin() + 1, tmp.end()), tmp.end()); for (int i = 1; i <= n; ++i) { A[i] = lower_bound(tmp.begin() + 1, tmp.end(), raw[i]) - tmp.begin(); } for (int i = 0; i < q; ++i) { int l, r; cin >> l >> r; queries.push_back({l, r, i}); } sort(queries.begin(), queries.end()); vector<int> freq(n+2, 0); // 離散化後最多 n int distinct = 0; auto add = [&](int pos) { if (++freq[A[pos]] == 1) distinct++; }; auto remove = [&](int pos) { if (--freq[A[pos]] == 0) distinct--; }; int curr_l = 1, curr_r = 0; for (auto& query : queries) { int l = query.l, r = query.r; while (curr_l > l) add(--curr_l); while (curr_r < r) add(++curr_r); while (curr_l < l) remove(curr_l++); while (curr_r > r) remove(curr_r--); ans[query.idx] = distinct; } for (int i = 0; i < q; ++i) { cout << ans[i] << "\n"; } return 0; } ``` ## Distinct Values Queries II [problem](https://cses.fi/problemset/task/3356) ```markdown 題目: - 給定長度為 n 的陣列 A[1..n],初始為整數。 - 有兩種操作: - `1 pos val`:把第 pos 個位置更新為 val。 - `2 l r`:詢問區間 A[l..r] 是否所有值皆互不相同(distinct)? - 回答 YES 或 NO。 ``` ``` markdown 解法 : ### 1. 觀察目標: - 如果區間 [l, r] 中有兩個位置值相同,則這區間「不是全 distinct」。 - 問題轉化為:是否存在某個 i ∈ [l, r] 使得「i 的下一個出現位置」在 [l, r] 內? --- ### 2. 使用 S[i] 表示下一次出現的位置: - 若 i 是某值的前一次出現,則 S[i] 記錄下一次出現的位置。 - 若某個 S[i] ∈ [l, r],則區間內存在重複 → 輸出 NO。 - 否則輸出 YES。 --- ### 3. Segment Tree 維護最小 S[i]: - 建一棵 Segment Tree,維護每個位置的 S[i]。 - 對於每次查詢 (l, r),查詢區間最小值 minS = min{S[l..r]}。 - 若 minS ≤ r,代表區間內有重複元素。 --- ### 4. 動態更新: - 使用 `map<int, set<int>>` 記錄每個值的位置集合。 - 每次值更新,調整相鄰出現位置的 S[] 並用 Segment Tree 更新。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; const int INF = 1e9+10; struct SegmentTree { int n; vector<int> tree; SegmentTree(int sz) { n = sz; tree.assign(4*n, INF); } void build(int idx, int l, int r, const vector<int>& S) { if (l == r) { tree[idx] = S[l]; return; } int mid = (l+r)/2; build(2*idx, l, mid, S); build(2*idx+1, mid+1, r, S); tree[idx] = min(tree[2*idx], tree[2*idx+1]); } void update(int idx, int l, int r, int pos, int val) { if (l == r) { tree[idx] = val; return; } int mid = (l+r)/2; if (pos <= mid) update(2*idx, l, mid, pos, val); else update(2*idx+1, mid+1, r, pos, val); tree[idx] = min(tree[2*idx], tree[2*idx+1]); } int query(int idx, int l, int r, int ql, int qr) { if (qr < l || r < ql) return INF; if (ql <= l && r <= qr) return tree[idx]; int mid = (l+r)/2; return min(query(2*idx, l, mid, ql, qr), query(2*idx+1, mid+1, r, ql, qr)); } }; int main() { ios::sync_with_stdio(false); cin.tie(0); int n, q; cin >> n >> q; vector<int> A(n+1); for (int i = 1; i <= n; ++i) cin >> A[i]; vector<int> S(n+1, INF); map<int, set<int>> pos_map; for (int i = 1; i <= n; ++i) { auto& s = pos_map[A[i]]; if (!s.empty()) { int prv = *prev(s.end()); S[prv] = i; } s.insert(i); } SegmentTree seg(n+1); seg.build(1,1,n,S); while (q--) { int type; cin >> type; if (type == 2) { int l, r; cin >> l >> r; int minS = seg.query(1,1,n,l,r); cout << (minS <= r ? "NO\n" : "YES\n"); } else { int pos, val; cin >> pos >> val; if (A[pos] == val) continue; // 刪除舊值鏈上的 pos { auto& s = pos_map[A[pos]]; auto it = s.find(pos); int prv = -1, nxt = -1; if (it != s.begin()) prv = *prev(it); if (next(it) != s.end()) nxt = *next(it); if (prv != -1) { S[prv] = nxt == -1 ? INF : nxt; seg.update(1,1,n,prv,S[prv]); } if (nxt != -1) { // 不需要處理 S[nxt] 因為它已經指向它之後的 } S[pos] = INF; seg.update(1,1,n,pos,INF); s.erase(it); } A[pos] = val; // 插入新值鏈上的 pos { auto& s = pos_map[val]; s.insert(pos); auto it = s.find(pos); int prv = -1, nxt = -1; if (it != s.begin()) prv = *prev(it); if (next(it) != s.end()) nxt = *next(it); if (prv != -1) { S[prv] = pos; seg.update(1,1,n,prv,S[prv]); } if (nxt != -1) { S[pos] = nxt; } else { S[pos] = INF; } seg.update(1,1,n,pos,S[pos]); } } } return 0; } ``` ## Increasing Array Queries [problem](https://cses.fi/problemset/task/2416) ```markdown 題目: - 給定一個長度為 n 的陣列 a[1..n]。 - 定義你可以執行以下操作任意次: - 選定一個區間 [l..r],把 a[l..r] 區間內的所有值設為同一個值 v(只要這樣做不會讓陣列不遞增)。 - 現在有 q 個查詢,每個查詢給你一個區間 [l, r],問若從原始 a[] 依序進行操作直到整個陣列遞增後, a[l..r] 的總和會是多少。 ``` ``` markdown 解法 : ### 1. 模擬「變成遞增陣列」的過程: - 要讓陣列遞增,當 a[i] < a[i-1] 時,就需要把 a[i] 提高到 a[i-1]。 - 所以我們可以倒著從右往左處理,維持一個「單調不遞減」的序列。 - 對每個位置 l,我們會把區間 [l, r] 設為 a[l],其中 r 是第一個滿足 a[r] ≥ a[l] 的位置。 --- ### 2. Segment Tree 維護目前陣列: - 使用一棵 Lazy Segment Tree: - 支援區間 assign:將一段區間全部設為某個值。 - 支援區間最大值查詢 mx[] - 支援區間總和查詢 sm[] - 還需要一個 `orderOf()` 函數,找出從某個位置 l 開始,第一個比 a[l] 小的位置,作為 assign 的右邊界。 --- ### 3. 處理查詢: - 使用前綴和 a[i] → 原始總和為 a[r] - a[l-1] - 更新後總和使用 Segment Tree 查詢 sum(l, r) - 所以答案為: - `sum(l, r) - (a[r] - a[l-1])`,表示總共補上去的值(花費) ``` ``` cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; typedef pair<int,int> pii; const int maxN = 2e5+1; const int SIZE = 4*maxN; int N, Q, lo[SIZE], hi[SIZE]; ll a[maxN], ass[SIZE], mx[SIZE], sm[SIZE], ans[maxN]; vector<pii> queries[maxN]; int len(int i){ return hi[i]-lo[i]+1; } void assign(int i, ll val){ ass[i] = mx[i] = val; sm[i] = val * len(i); } void push(int i){ if(ass[i]){ assign(2*i, ass[i]); assign(2*i+1, ass[i]); ass[i] = 0; } } void pull(int i){ sm[i] = sm[2*i] + sm[2*i+1]; mx[i] = max(mx[2*i], mx[2*i+1]); } void init(int i, int l, int r){ lo[i] = l; hi[i] = r; if(l == r){ sm[i] = a[l]; return; } int m = (l+r)/2; init(2*i, l, m); init(2*i+1, m+1, r); pull(i); } void update(int i, int l, int r, ll val){ if(l > hi[i] || r < lo[i]) return; if(l <= lo[i] && hi[i] <= r){ assign(i, val); return; } push(i); update(2*i, l, r, val); update(2*i+1, l, r, val); pull(i); } int orderOf(int i, int l, int val){ if(lo[i] == hi[i]) return lo[i]; push(i); int idx = -1; if(hi[2*i] <= l || mx[2*i] < val) idx = orderOf(2*i+1, l, val); else idx = orderOf(2*i, l, val); pull(i); return idx; } ll sum(int i, int l, int r){ if(l > hi[i] || r < lo[i]) return 0; if(l <= lo[i] && hi[i] <= r) return sm[i]; push(i); ll left = sum(2*i, l, r); ll right = sum(2*i+1, l, r); pull(i); return left+right; } int main(){ scanf("%d %d", &N, &Q); for(int i = 1; i <= N; i++) scanf("%lld", &a[i]); init(1, 1, N); for(int q = 0, l, r; q < Q; q++){ scanf("%d %d", &l, &r); queries[l].push_back({r, q}); } for(int i = 2; i <= N; i++) a[i] += a[i-1]; for(int l = N; l >= 1; l--){ int val = a[l]-a[l-1]; int modifyR = (mx[1] < val ? N+1 : orderOf(1, l, val)); update(1, l, modifyR-1, val); for(pii q : queries[l]){ int r = q.first; int id = q.second; ans[id] = sum(1, l, r) - (a[r]-a[l-1]); } } for(int i = 0; i < Q; i++) printf("%lld\n", ans[i]); } ``` ## Movie Festival Queries ## Forest Queries II [problem](https://cses.fi/problemset/task/1739) ```markdown 題目: - 給定一個 n × n 的地圖(1 ≤ n ≤ 1000),由 '.' 和 '*' 組成,代表空地和樹。 - 有 q 次操作,每次為下列兩種之一: - `1 y x`:將格子 (y,x) 的內容在 '.' 與 '*' 間切換。 - `2 y1 x1 y2 x2`:詢問矩形區域內總共有幾棵樹(即 '*' 的個數)。 ``` ``` markdown 解法 : ### 1. 使用 2D BIT(Binary Indexed Tree): - 因為有大量查詢與修改,使用 2D Fenwick Tree 來維護每個位置是否有樹。 - 每次修改對應位置的值 +1 或 -1。 - 查詢某個矩形區域樹木總數,可以用 2D 前綴和計算: ``` query(y1,x1,y2,x2) = sum(y2,x2) - sum(y1-1,x2) - sum(y2,x1-1) + sum(y1-1,x1-1) ``` --- ### 2. 初始化: - 將初始地圖上所有 '*' 的位置,在 BIT 中加 1。 --- ### 3. 更新操作: - 若原本為 '*',則變為 '.' 並從 BIT 扣除 1。 - 反之則變為 '*' 並在 BIT 加上 1。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; const int N = 1005; int n, q; int BIT[N][N]; char grid[N][N]; // 單點修改 BIT void add(int x, int y, int delta) { for (int i = x; i <= n; i += i & -i) { for (int j = y; j <= n; j += j & -j) { BIT[i][j] += delta; } } } // 查詢 (1,1) ~ (x,y) 的總和 int sum(int x, int y) { int res = 0; for (int i = x; i > 0; i -= i & -i) { for (int j = y; j > 0; j -= j & -j) { res += BIT[i][j]; } } return res; } // 查詢任意矩形 (y1,x1) ~ (y2,x2) int query(int y1, int x1, int y2, int x2) { return sum(y2, x2) - sum(y1 - 1, x2) - sum(y2, x1 - 1) + sum(y1 - 1, x1 - 1); } int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> q; for (int i = 1; i <= n; ++i) { string s; cin >> s; for (int j = 1; j <= n; ++j) { grid[i][j] = s[j-1]; if (grid[i][j] == '*') add(i, j, 1); } } while (q--) { int type; cin >> type; if (type == 1) { int y, x; cin >> y >> x; if (grid[y][x] == '*') { grid[y][x] = '.'; add(y, x, -1); } else { grid[y][x] = '*'; add(y, x, 1); } } else { int y1, x1, y2, x2; cin >> y1 >> x1 >> y2 >> x2; cout << query(y1, x1, y2, x2) << "\n"; } } } ``` ## Range Updates and Sums [problem](https://cses.fi/problemset/task/1735) ```markdown 題目: - 給定長度為 n 的整數陣列 a[1..n],有 q 次操作,每次為以下三種之一: - `1 a b x`:將區間 [a, b] 中的每個元素都加上 x。 - `2 a b x`:將區間 [a, b] 中的每個元素都設為 x。 - `3 a b`:詢問區間 [a, b] 的總和。 ``` ``` markdown 解法 : ### 1. 使用 Lazy Segment Tree 同時支援加法與指派: - 每個節點紀錄: - `sum`:該區間總和 - `add`:延遲加法操作(區間加法用) - `set` + `to_set`:延遲指派操作(區間設值用) --- ### 2. 推進與合併邏輯: - `push()`: - 若該區間被標記為 set,則直接覆蓋子節點並清除加法。 - 若有 add,則將加法往子節點下推。 - `range_add()`: - 若完全覆蓋,直接加上值並打上 add 標記。 - `range_set()`: - 若完全覆蓋,直接設為該值並打上 set 標記,清除 add。 - `query()`: - 查詢區間和時,需先 push 確保所有延遲標記被處理。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; #define ll long long const int N = 2e5+5; struct Node { ll sum, add, set; bool to_set; } seg[N<<2]; ll a[N]; int n, q; void build(int id, int l, int r) { if (l == r) { seg[id].sum = a[l]; return; } int mid = (l+r)/2; build(id*2,l,mid); build(id*2+1,mid+1,r); seg[id].sum = seg[id*2].sum + seg[id*2+1].sum; } void push(int id, int l, int r) { int mid = (l+r)/2; if (seg[id].to_set) { ll v = seg[id].set; seg[id*2].sum = (mid-l+1)*v; seg[id*2+1].sum = (r-mid)*v; seg[id*2].set = seg[id*2+1].set = v; seg[id*2].to_set = seg[id*2+1].to_set = 1; seg[id*2].add = seg[id*2+1].add = 0; seg[id].to_set = 0; } if (seg[id].add) { ll v = seg[id].add; seg[id*2].sum += (mid-l+1)*v; seg[id*2+1].sum += (r-mid)*v; if (seg[id*2].to_set) seg[id*2].set += v; else seg[id*2].add += v; if (seg[id*2+1].to_set) seg[id*2+1].set += v; else seg[id*2+1].add += v; seg[id].add = 0; } } void range_add(int id, int l, int r, int ql, int qr, ll v) { if (ql<=l && r<=qr) { seg[id].sum += (r-l+1)*v; if (seg[id].to_set) seg[id].set += v; else seg[id].add += v; return; } push(id,l,r); int mid = (l+r)/2; if (ql<=mid) range_add(id*2,l,mid,ql,qr,v); if (qr>mid) range_add(id*2+1,mid+1,r,ql,qr,v); seg[id].sum = seg[id*2].sum + seg[id*2+1].sum; } void range_set(int id, int l, int r, int ql, int qr, ll v) { if (ql<=l && r<=qr) { seg[id].sum = (r-l+1)*v; seg[id].set = v; seg[id].to_set = 1; seg[id].add = 0; return; } push(id,l,r); int mid = (l+r)/2; if (ql<=mid) range_set(id*2,l,mid,ql,qr,v); if (qr>mid) range_set(id*2+1,mid+1,r,ql,qr,v); seg[id].sum = seg[id*2].sum + seg[id*2+1].sum; } ll query(int id, int l, int r, int ql, int qr) { if (ql<=l && r<=qr) return seg[id].sum; push(id,l,r); int mid = (l+r)/2; ll res = 0; if (ql<=mid) res += query(id*2,l,mid,ql,qr); if (qr>mid) res += query(id*2+1,mid+1,r,ql,qr); return res; } int main() { ios::sync_with_stdio(0); cin.tie(0); cin >> n >> q; for (int i=1;i<=n;++i) cin >> a[i]; build(1,1,n); while (q--) { int type,a,b; ll x; cin >> type >> a >> b; if (type==1) { cin >> x; range_add(1,1,n,a,b,x); } else if (type==2) { cin >> x; range_set(1,1,n,a,b,x); } else { cout << query(1,1,n,a,b) << "\n"; } } return 0; } ``` ## Polynomial Queries [problem](https://cses.fi/problemset/task/1736) ```markdown 題目: - 給定長度為 n 的整數陣列 a[1..n]。 - 有 q 個操作,每次為以下兩種之一: - `1 l r`:對區間 [l, r] 做如下操作: - a[l] += 1, a[l+1] += 2, ..., a[r] += (r - l + 1) - `2 l r`:詢問區間 [l, r] 的總和。 ``` ``` markdown 解法 : ### 1. 把操作視為等差數列加法: - 每次更新相當於往區間 [l, r] 加上一個首項為 1、公差為 1 的等差數列。 - 可以推廣成加任意首項為 s、公差為 d 的等差級數。 --- ### 2. 延遲標記處理兩個變數(s, d): - 使用 Lazy Segment Tree,每個節點標記: - `lazy_start`: 延遲加的首項 - `lazy_d`: 延遲加的公差 - 使用 `calc(len, s, d)` 計算區間長度為 len 的等差數列總和: - `s + (s+d) + ... + (s+(len-1)d)` = `s * len + d * len*(len-1)/2` --- ### 3. 推進 lazy 時要調整首項: - 對於左子區間,首項不變。 - 對於右子區間,首項 = 父首項 + 公差 × 左子長度。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 2e5 + 5; struct Node { ll sum = 0; ll lazy_start = 0, lazy_d = 0; } seg[N*4]; int n, q; ll a[N]; void build(int i, int l, int r) { if (l == r) { seg[i].sum = a[l]; return; } int m = (l + r) / 2; build(i*2, l, m); build(i*2+1, m+1, r); seg[i].sum = seg[i*2].sum + seg[i*2+1].sum; } ll calc(int len, ll s, ll d) { return s * len + d * 1LL * len * (len-1) / 2; } void apply(int i, int l, int r, ll s, ll d) { seg[i].sum += calc(r-l+1, s, d); seg[i].lazy_start += s; seg[i].lazy_d += d; } void push(int i, int l, int r) { if (seg[i].lazy_start == 0 && seg[i].lazy_d == 0) return; int m = (l + r) / 2; int left_len = m - l + 1; // 左兒子:首項 = 父首項,公差不變 apply(i*2, l, m, seg[i].lazy_start, seg[i].lazy_d); // 右兒子:首項 = 父首項 + 左兒子長度*公差 apply(i*2+1, m+1, r, seg[i].lazy_start + seg[i].lazy_d * left_len, seg[i].lazy_d); seg[i].lazy_start = seg[i].lazy_d = 0; } void update(int i, int l, int r, int ql, int qr, ll s, ll d) { if (qr < l || ql > r) return; if (ql <= l && r <= qr) { ll start = s + (l - ql) * d; apply(i, l, r, start, d); return; } push(i, l, r); int m = (l + r) / 2; update(i*2, l, m, ql, qr, s, d); update(i*2+1, m+1, r, ql, qr, s, d); seg[i].sum = seg[i*2].sum + seg[i*2+1].sum; } ll query(int i, int l, int r, int ql, int qr) { if (qr < l || ql > r) return 0; if (ql <= l && r <= qr) return seg[i].sum; push(i, l, r); int m = (l + r) / 2; return query(i*2, l, m, ql, qr) + query(i*2+1, m+1, r, ql, qr); } int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> q; for (int i = 1; i <= n; i++) cin >> a[i]; build(1, 1, n); while (q--) { int type, l, r; cin >> type >> l >> r; if (type == 1) { update(1, 1, n, l, r, 1, 1); } else { cout << query(1, 1, n, l, r) << "\n"; } } } ``` ## Range Queries and Copies [problem](https://cses.fi/problemset/task/1737) ```markdown 題目: - 給定長度為 n 的陣列 a[1..n]。 - 有 q 次操作,共三種類型: 1. `1 k a x`:對第 k 版本的陣列進行修改,將第 a 個位置設為 x。 2. `2 k a b`:查詢第 k 版本陣列中區間 [a, b] 的總和。 3. `3 k`:複製第 k 版本陣列,產生一個新的版本(為新的根)。 - 最初的版本為第 0 個版本。 ``` ``` markdown 解法 : ### 1. 使用 Persistent Segment Tree(可持久化線段樹): - 每次修改或複製,都產生一棵新樹(實際上僅複製 O(log n) 個節點) - 修改操作的時間與空間複雜度皆為 O(log n) - 版本透過指針記錄在 `roots[]` 中,每次產生新版本就推入新 root。 --- ### 2. 操作說明: - `build()`:建構初始線段樹,儲存第 0 版本。 - `update()`:從某個舊版本 root 出發,僅建立必要的新節點產生新版本。 - `query()`:在指定版本的 root 上查詢區間和。 - `copy()`:直接複製 root 指針,空間 O(1),無需實際複製整棵樹。 ``` ``` cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; const int MAXN = 2e5 + 5; const int MAXQ = 2e5 + 5; const int MAXLOG = 20; // log2(MAXN) struct Node { ll sum; Node *left, *right; Node(ll s = 0) : sum(s), left(nullptr), right(nullptr) {} }; int n, q; ll arr[MAXN]; vector<Node*> roots; // 建樹 Node* build(int l, int r) { Node* node = new Node(); if (l == r) { node->sum = arr[l]; return node; } int m = (l + r) / 2; node->left = build(l, m); node->right = build(m+1, r); node->sum = node->left->sum + node->right->sum; return node; } // 更新 Node* update(Node* prev, int l, int r, int pos, ll val) { Node* node = new Node(); if (l == r) { node->sum = val; return node; } int m = (l + r) / 2; if (pos <= m) { node->left = update(prev->left, l, m, pos, val); node->right = prev->right; } else { node->left = prev->left; node->right = update(prev->right, m+1, r, pos, val); } node->sum = node->left->sum + node->right->sum; return node; } // 查詢 ll query(Node* node, int l, int r, int ql, int qr) { if (qr < l || ql > r) return 0; if (ql <= l && r <= qr) return node->sum; int m = (l + r) / 2; return query(node->left, l, m, ql, qr) + query(node->right, m+1, r, ql, qr); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> q; for (int i = 1; i <= n; i++) cin >> arr[i]; roots.push_back(build(1, n)); while (q--) { int type; cin >> type; if (type == 1) { int k, a, x; cin >> k >> a >> x; k--; // 0-based roots[k] = update(roots[k], 1, n, a, x); } else if (type == 2) { int k, a, b; cin >> k >> a >> b; k--; cout << query(roots[k], 1, n, a, b) << "\n"; } else if (type == 3) { int k; cin >> k; k--; roots.push_back(roots[k]); // 複製指針即可 } } } ``` ## Missing Coin Sum Queries