###### tags: `OI` # 112-1 延平中學校內賽簡易題解 以下題解如果有問題,歡迎找出題者討論: * E-mail: dave910602@gmail.com * Facebook: 郭軒語(Dave Kuo) * Discord: HNO2#4672 [題本連結](https://drive.google.com/file/d/15dfCJ-ZgQXZ34Dx_wUc5RR-MWDCVXE2R/view?usp=drive_link) 特別感謝 SorahISA、Misuki 幫忙驗題。 如果在題解過程中看到什麼演算法或資料結構不會的話,[這裡](https://github.com/HHSH-CYSH-WGSH-HSNU-Summer-Camp)跟[這裡](https://www.csie.ntu.edu.tw/~sprout/algo2023/)都是很好的學習資源。 ## pA. 分糖果 (candy) ### Subtask 2 > $k = 0$ 根據題意,$k = 0$ 的意思就是不會做任何分配工作,所以每個小朋友手上拿的糖果數量還是一樣,直接輸出輸入的 $a$ 就好。 ### Subtask 3 > $n=2$,且在 $k$ 次平分中,兩人手上糖果數量和都是偶數 如果 $k = 0$ 就直接照原樣輸出。否則在第一次平分以後,兩人各會拿到 `(a[0] + a[1]) / 2` (0-based)顆糖果,不論平分多少次都是一樣。 ### Subtask 4 > $n=2$ 考慮兩人手上糖果數量和是奇數的情況。如果 $k$ 是奇數,那第一個人會拿到 `(a[0] + a[1]) / 2`(0-based) 顆,第二個人會多拿一顆,也就是 `(a[0] + a[1]) / 2 + 1`(0-based)。如果 $k$ 是偶數就兩人糖果數量對調。 ### Subtask 5 > 在 $k$ 次平分中,兩人手上糖果數量和都是偶數 跟 Subtask 3 一樣,只是在第 $i$ 次(0-based)分配的時候是分 `a[i % n]` 和 `a[(i + 1) % n]`。 ### Subtask 6 > 無額外限制 跟 Subtask 4 一樣,只是在第 $i$ 次(0-based)分配的時候是分 `a[i % n]` 和 `a[(i + 1) % n]`。 :::spoiler Sample Solution ```cpp= #include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, k; cin >> n >> k; vector<int> a(n); for (int &i : a) cin >> i; for (int i = 0; i < k; i++) { int x = i % n, y = (x + 1) % n; int sum = a[x] + a[y]; a[x] = sum / 2; a[y] = sum - sum / 2; } for (int i = 0; i < n; i++) cout << a[i] << " \n"[i == n - 1]; } ``` ::: ## pB. 食物組合 (combination) ### Subtask 2 > $k=1$ $k = 1$ 就是只能選擇一種食物,所以就是所有攤位的食物好吃程度。要記得由小到大排序,在這裡可以使用任何 $\mathcal{O}(n^2)$ 或 $\mathcal{O}(n \log n)$ 的排序演算法。 ### Subtask 3 > $k \leq 4$ 可以寫好幾層迴圈去枚舉所有的食物組合,$k$ 是幾就寫幾層迴圈,然後把所有找到的滿足程度推到一個 `std::vector` 裡面。只是在這裡排序不能使用 $\mathcal{O}(n^2)$ 的做法了,需要使用 $\mathcal{O}(n \log n)$ 的排序演算。最簡單的方法就是引用 `std::sort`。 ### Subtask 4 > $n \leq 20$ 使用位元枚舉枚舉所有 $2^n$ 食物的選法,之後看枚舉到的數字是否包含恰好 $k$ 個位元是 $1$,是的話就推到一個 `std::vector` 裡面,之後再把那個 `std::vector` 排序。 ### Subtask 5 > 無額外限制 跟 Subtask 4 一樣,只是要改成使用遞迴枚舉。遞迴枚舉的話,可以寫一個函式 `recursion(int idx, int selected, int sum)`,代表最後一個被選的編號是 `idx`、已經選了幾個以及目前選到的數字總和。 遞迴枚舉的時候要注意不要遞迴到實際上不存在的狀態(也就是從目前的參數遞迴下去不會找到一個合法的 $k$ 個數字的選法,例如 `idx = n`、`selected = 0`)只要這一點做到了,runtime 就是可以被保證的。 :::spoiler Sample solution ```cpp= #include <bits/stdc++.h> using namespace std; int n, k; vector<int> a, v; void recursion(int idx, int selected, int sum) { if (selected > 0) sum += a[idx]; if (selected == k) { v.emplace_back(sum); return; } for (int i = idx + 1; i < n; i++) if (n - i >= k - selected) recursion(i, selected + 1, sum); } int main() { ios::sync_with_stdio(0); cin.tie(0); cin >> n >> k; a.resize(n); for (int &i : a) cin >> i; recursion(-1, 0, 0); sort(v.begin(), v.end()); int m = v.size(); for (int i = 0; i < m; i++) cout << v[i] << " \n"[i == m - 1]; } ``` ::: ## pC. 搭公車 (bus) ### Subtask 2 > $n=1, q \leq 1000, m_1 \leq 1000$ 應該容易發現答案一定是 $s_{1,1}, s_{1,2},\ldots,s_{1, m_1}$ 其中一個(這點對於所有 Subtask 都成立)。在這個 Subtask 中,對於每筆詢問,可以直接枚舉所有 $s_{1,1}, s_{1,2},\ldots,s_{1, m_1}$,看他加上 $d_1$ 以後會不會超過詢問值,如果不會遲到就表示可以搭那班公車。 ### Subtask 3 > $n \leq 1000, q \leq 1000, \sum m_i \leq 1000$ 同樣對於每筆詢問,可以直接枚舉所有 $s_{1,1}, s_{1,2},\ldots,s_{1, m_1}$,看到達公車站 $n+1$ 的時候會不會遲到。方法就是每次到一個公車站,就直接選下一班可以搭的公車搭上去,最後就可以算出到達公車站 $n+1$ 的時間。 ### Subtask 4 > $n=1$ 和 Subtask 2 一樣,只是這次不能夠一個一個枚舉 $s_{1,1}, s_{1,2},\ldots,s_{1, m_1}$ 了。注意到當 $x$ 越大的時候,從 $s_{1,x}$ 出發,到達公車站 $n+1$ 的時間就會越晚(這點對於所有 Subtask 都成立)。以上這個性質讓我們可以二分搜。 二分搜的方法有兩個,一個是對 $x$ 二分搜,看在 $s_{1,x}$ 出發,到達公車站 $n+1$ 的時間 $s_{1,x} + d_1$ 會不會超時。另一個是直接找 $s_{1,1}, s_{1,2},\ldots,s_{1, m_1}$ 中最後一個不超過詢問值減去 $d_1$ 的數字。 ### Subtask 5 > $m_1 = m_2 = \cdots = m_n$ 從每個 $s_{n,1}, s_{n,2},\ldots,s_{n, m_n}$ 往回找,找到要在 $s_{n, x} + d_n$ 抵達公車站,最早需要什麼時候從公車站 $1$ 出發才行,往回找的時候一樣是二分搜。之後在詢問的時候就對 $s_{n,1} + d_n, s_{n,2} + d_n,\ldots,s_{n, m_n} + d_n$ 二分搜,找到最後一個不超過詢問值的數字,就可以找到相對應的答案。 ### Subtask 6 > 無額外限制 我們對於每條路線的每個發車時間紀錄一個值 arrival,代表如果從這條路線的這個時間點出發,什麼時候會抵達公車站 $n + 1$。從後面往回推: * 一開始時,$s_{n,j}$ 的 arrival time 就是 $s_{n,j} + d_n$。 * 當要找第 $i$ 條路線第 $j$ 個發車時間的 arrival time 時,就去找到 $s_{i+1,1},\ldots,s_{i+1,m_{i+1}}$ 中第一個大於等於 $s_{i,j} + d_i$ 的數字,找到他的 arrival time 就是答案。 最後處理詢問的時候,就對 $s_{1,1},\ldots,s_{1,m_1}$ 的 arrival 二分搜。 :::spoiler Sample Solution ```cpp= #include <bits/stdc++.h> using namespace std; constexpr int INF = 2'100'000'000; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, q; cin >> n >> q; vector<int> m(n), l(n); vector<vector<int>> bus(n), arrival(n); for (int i = 0; i < n; i++) { cin >> m[i] >> l[i]; bus[i].resize(m[i]); for (int &j : bus[i]) cin >> j; } for (int i : bus[n - 1]) arrival[n - 1].emplace_back(l[n - 1] + i); for (int i = n - 2; i >= 0; i--) { for (int j : bus[i]) { auto idx = lower_bound(bus[i + 1].begin(), bus[i + 1].end(), j + l[i]) - bus[i + 1].begin(); if (idx >= m[i + 1]) arrival[i].emplace_back(INF); else arrival[i].emplace_back(arrival[i + 1][idx]); } } for (int i = 1; i <= q; i++) { int x; cin >> x; auto idx = upper_bound(arrival[0].begin(), arrival[0].end(), x) - arrival[0].begin(); if (idx == 0) cout << -1 << " \n"[i == q]; else cout << bus[0][idx - 1] << " \n"[i == q]; } } ``` ::: ## pD. 電力供給 (power-supply) ### Subtask 2 > $m = n-1$ 以下題解都使用圖論的術語講解:房間 = 節點、電線 = 邊、公司 = 顏色、邊的價錢 = 權重。 這題的本質是要求出一個圖上的生成樹。因為給出的圖是一棵樹,所以很自然的就是全部的邊都選。然後根據題意,如果至少有一個 $c_i =j$,就要把 $p_j$ 加到答案裡。 ### Subtask 3 > $k = 1$ 裸的[最小生成樹(MST)](https://oi-wiki.org/graph/mst/)問題。最後記得要把 $p_1$ 加到答案裡面。任何你會的 MST 演算法都可以用。 ### Subtask 4 > $k \leq 8$ 枚舉所有的顏色集合。對於每個顏色集合,找到所有是那些顏色的邊,對他做 MST,這個顏色集合對應的花費就是 (MST $w_i$ 權重和 + 顏色 $p_i$ 總和)。取所有顏色集合中答案最小的就好。如果用 Kruskal's Algorithm 做 MST,總時間複雜度是 $\mathcal{O}(m \log m + 2^k m \alpha(n))$。 ### Subtask 5 > $k \leq 12$ 以下兩個 Subtask 都需要用到以下關鍵性質: :::success 性質:假設 $E_1, E_2$ 是兩個邊的集合,且令 $S(E_1), S(E_2)$ 分別是兩個邊的集合的最小生成森林(每個 connected component 的最小生成樹的聯集),則兩個邊集合的聯集的最小生成森林 $S(E_1 \cup E_2)$ 中的任意一條邊都是 $S(E_1)$ 或 $S(E_2)$ 中的邊。換句話說,假如 $e$ 不在 $E_1$ 或 $E_2$ 的最小生成森林中,他就不會出現在 $E_1 \cup E_2$ 的最小生成森林中。 證明:可以直接由 [Cut Property](https://www.csie.ntu.edu.tw/~sprout/algo2023/ppt_pdf/week12/Minimum_spanning_tree.pdf) 得證。 ::: 根據以上性質,我們可以先對每個顏色做一次 MST,留下最多 $n-1$ 條邊。之後枚舉所有顏色集合的時候,就選取那個顏色留下來的邊就好,每次就考慮最多 $k(n-1)$ 條邊。總時間複雜度是 $\mathcal{O}(m (\log m + \alpha(n)) + 2^k nk \alpha(n))$ 或 $\mathcal{O}(m (\log m + \alpha(n)) + 2^k nk \log k \alpha(n))$,視實作而定。 ### Subtask 6 > 無額外限制 其實以上的作法可以再進一步延伸,我們對於每個顏色集合都去維護他的 MST 的邊集合。當要找出一個顏色集合的邊集合 $E$ 的 MST 時,如果他只包含一種顏色,就直接做;否則把第一種顏色的邊 $E_1$ 和其他邊 $E_2$ 拆開,把他們各自 MST 上的邊抓過來做 MST,這樣子最多只需要考慮 $2(n-1)$ 條邊。總時間複雜度變成 $\mathcal{O}(m (\log m + \alpha(n)) + 2^k n \alpha(n))$。 實作的時候,需要注意不要使用多餘的排序操作。例如說當要合併兩個邊集合的 MST 時,可以直接使用 `std::merge` 把兩個已經照邊權大小排的 sequence 合併起來。這個操作是線性的,不需要再依照邊權大小再排序一次。 :::spoiler Sample Solution ```cpp= #include <bits/stdc++.h> using namespace std; using ll = long long; constexpr ll INF = 1'000'000'000'000'000'000LL; struct edge { int u, v, w; edge(int _u, int _v, int _w) : u(_u), v(_v), w(_w) {} bool operator<(const edge &rhs) const { return w < rhs.w; } }; struct DSU { vector<int> p, sz; void reset(int n) { p.resize(n); sz.resize(n); for (int i = 0; i < n; i++) p[i] = i, sz[i] = 1; } int Find(int x) { return p[x] == x ? x : p[x] = Find(p[x]); } bool Same(int x, int y) { return Find(x) == Find(y); } void Union(int x, int y) { x = Find(x), y = Find(y); if (x == y) return; if (sz[x] > sz[y]) swap(x, y); sz[y] += sz[x], p[x] = y; } } dsu; ll run_kruskal(int n, vector<edge> &edges) { dsu.reset(n); ll ret = 0; vector<edge> new_edges; for (auto [u, v, w] : edges) { if (!dsu.Same(u, v)) { ret += w; new_edges.emplace_back(u, v, w); dsu.Union(u, v); } } edges.swap(new_edges); return ((int)edges.size() == n - 1 ? ret : INF); } int main() { ios::sync_with_stdio(0); cin.tie(0); int n, m, k; cin >> n >> m >> k; vector<int> p(k); vector<int> u(m), v(m), w(m), c(m); vector<vector<edge>> edges(1 << k); vector<ll> sum1(1 << k), sum2(1 << k); for (int &i : p) cin >> i; for (int i = 0; i < m; i++) { cin >> u[i] >> v[i] >> w[i] >> c[i]; u[i]--, v[i]--, c[i]--; edges[1 << c[i]].push_back(edge(u[i], v[i], w[i])); } for (int i = 0; i < k; i++) { sort(edges[1 << i].begin(), edges[1 << i].end()); sum1[1 << i] = run_kruskal(n, edges[1 << i]); } for (int i = 1; i < (1 << k); i++) { if (!edges[i].empty()) continue; int b = (i & (-i)); merge(edges[b].begin(), edges[b].end(), edges[i ^ b].begin(), edges[i ^ b].end(), back_inserter(edges[i])); sum1[i] = run_kruskal(n, edges[i]); } for (int i = 1; i < (1 << k); i++) { for (int j = 0; j < k; j++) if ((i >> j) & 1) sum2[i] += p[j]; } ll ans = INF; for (int i = 1; i < (1 << k); i++) ans = min(ans, sum1[i] + sum2[i]); cout << ans << '\n'; } ``` ::: ## pE. 區間中位數 (median) ### Subtask 2 > $n \leq 100$ 枚舉所有的區間,對於每個區間排序之後找出中位數。之後再對所有區間排序找到第 $k$ 小的。 時間複雜度是 $\mathcal{O}(n^3 \log n)$ ### Subtask 3 > $n \leq 3000$ 一樣是找到所有區間的中位數以後對所有區間排序。只是在這裡我們在固定左界的時候,需要在短時間內求出固定左界下的所有區間中位數。方法有兩種: 1. 參考動態中位數的作法。維護一個 max-heap(現在目前所有 $\leq$ 中位數的數字)和一個 min-heap(現在目前所有 $>$ 中位數的數字),在每次插入數字時保持兩邊都是恰好一半的數字,使得 max-heap 的頂端就是動態中位數。每次要插入數字的時候,看他是大於還是 $\leq$ 目前的中位數,先插入相對應的 heap,之後再去調整兩個 heap 的大小,使得 max-heap 的頂端就是動態中位數。 2. 直接使用 BIT 或線段樹之類的維護每個數字出現了幾次。因為線段樹/BIT 的特性,可以在 $\mathcal{O}(\log n)$ 時間內找出第一個 $\leq k$ 的前綴在哪裡,也就可以在 $\mathcal{O}(\log n)$ 時間內找出區間中位數。 ### Subtask 4 > $a_i = i$($1 \leq i \leq n$) 因為這個 Subtask 給出的 $a_1,a_2,\ldots,a_n$ 的特殊性,答案是可以直接 $\mathcal{O}(1)$ 算出來的。 ### Subtask 5 > 無額外限制 #### 第一個數字的求法 我們嘗試二分搜找出第一個答案。給定一個 $x$,判斷答案是不是 $\leq x$ 的方法,就是算出有幾個區間的區間中位數 $\leq x$。如果把那些滿足 $a_i > x$ 的 $i$,讓他的 $b_i$ 都設成 $1$,否則就設成 $-1$,那就相當於求出有多少個區間的總和 $\leq 0$,可以使用前綴和 + 逆序數對求出。時間複雜度可能是 $\mathcal{O}(n \log n)$ 或 $\mathcal{O}(n \log^2 n)$。 #### 第二個數字的求法 假設第一個答案是 $ans_1$,那可以求出 $ans_1$ 時,每個左界有多少個區間 $\leq 0$,在算逆序數對的時候順便算出來。但是這樣子只能求出每個左界中位數 $\leq ans_1$ 的區間有幾個,我們還需要算出 $ans_1 - 1$時,每個左界中位數 $\leq ans_1 - 1$ 的區間有幾個,兩個相減以後就會得到每個左界區間中位數恰好是 $ans_1$ 的有幾個。之後就看到 $k$ 落在哪個左界就好。 #### 第三個數字的求法 因為求出 $ans_1$ 和 $ans_2$ 了,我們就可以使用 Subtask 2 中的方法,算出固定左界之下的區間中位數,看 $k$ 是在哪個右界了。 :::spoiler Sample Solution ```cpp= #include <bits/stdc++.h> using namespace std; using ll = long long; vector<int> start_count(vector<int> &a, int x) { int n = a.size(); vector<int> b(n), ret(n); for (int i = 0; i < n; i++) { if (a[i] <= x) b[i] = 1; else b[i] = -1; } // count range sum >= 0 map<int, int> prefix_count; prefix_count[0] = 1; int threshold = 0, smaller_count = 1; int prefix_sum = 0; for (int i = n - 1; i >= 0; i--) { prefix_sum += b[i]; while (threshold < prefix_sum) { threshold++; smaller_count += prefix_count[threshold]; } while (threshold > prefix_sum) { smaller_count -= prefix_count[threshold]; threshold--; } ret[i] = smaller_count; prefix_count[prefix_sum]++; if (prefix_sum <= threshold) smaller_count++; } return ret; } int main() { ios::sync_with_stdio(0); cin.tie(0); int n; long long k; cin >> n >> k; vector<int> a(n); for (int &i : a) cin >> i; int ans1, ans2, ans3; // binary search for 1st answer int L = 0, R = n; while (R - L > 1) { int M = (L + R) >> 1; auto ret = start_count(a, M); ll total_segments = accumulate(ret.begin(), ret.end(), 0LL); if (total_segments >= k) R = M; else L = M; } ans1 = R; // ans2 calculation auto retR = start_count(a, R), retR1 = start_count(a, R - 1); k -= accumulate(retR1.begin(), retR1.end(), 0LL); for (int i = 0; i < n; i++) { if (k <= (retR[i] - retR1[i])) { ans2 = i + 1; break; } else k -= (retR[i] - retR1[i]); } // ans3 calculation priority_queue<int> small_numbers; priority_queue<int, vector<int>, greater<>> large_numbers; for (int i = ans2 - 1; i < n; i++) { if (i == ans2 - 1 || a[i] <= small_numbers.top()) { small_numbers.push(a[i]); } else { large_numbers.push(a[i]); } int size = i - (ans2 - 1) + 1; while (small_numbers.size() > (size + 1) / 2) { large_numbers.push(small_numbers.top()); small_numbers.pop(); } while (small_numbers.size() < (size + 1) / 2) { small_numbers.push(large_numbers.top()); large_numbers.pop(); } int median = small_numbers.top(); if (median == ans1) { k--; if (k == 0) { ans3 = i + 1; break; } } } cout << ans1 << ' ' << ans2 << ' ' << ans3 << '\n'; } ``` ::: ## pF. 典型區間詢問題 (range-query) ### Subtask 2 > $n,q \leq 3000$ 直接對於每筆詢問用一個 for 迴圈算出答案就好。時間複雜度 $\mathcal{O}(nq)$ ### Subtask 3 > $x_i$($1 \leq i \leq q$) 在 $q$ 筆詢問中最多只有 $50$ 種不同的值。 對於每種 $x_i$ 算出 $(x_i \bmod a_j)$ 的前綴和,之後每筆詢問就找出對應的 $x_i$ 區間答案。 ### Subtask 4 > $l_i = 1, r_i = n$($1 \leq i \leq q$)。 我們的目標是先預處理 $x_i =1$ 到 $2 \times 10^5$ 的所有答案。 我們讓 $ans_1,ans_2,\ldots$ 代表 $x_i = 1,2,\ldots$ 的答案,並且把每個 $ans_i$ 都初始化為 $q \times i$。首先先算出每種數字在 $a$ 中出現了幾次。假設一個數字 $k$ 出現了 $b$ 次,那因為 $(x_i \bmod a_j) = x_i - a_j \lfloor \frac{x_i}{a_j} \rfloor$,可以在 $ans_{k}, ans_{2k}, ans_{3k},\ldots$ 的位置都扣掉 $k \times b$,之後再對 $ans$ 做前綴和就可以求出 $ans_1,ans_2,\ldots$ 正確的值了。 ### Subtask 5 > $a_i \leq 50$($1 \leq i \leq n$)。 對於每種數字做出現了幾次的前綴和,之後每筆詢問就掃過每個數字求他的貢獻。假如一個數字 $k$ 在區間內出現了 $b$ 次,對答案的貢獻就是 $(x_i \bmod k) \times b$。 ### Subtask 6 > $a_i \geq 4000$($1 \leq i \leq n$)。 引導性的子任務。以下提供兩種思路: 1. 對序列分塊,之後對於每一塊都去做 Subtask 4 的事情,因為這個 Subtask $a_i$ 值域都很大,所以 runtime 可以被保證。查詢的時候就分成兩邊不完整的塊和中間完整的塊分別去做查詢。完整的塊就直接戳算好答案,不完整的塊就直接一個迴圈跑過去就好。 2. 把所有詢問照 $x_i$ 由小到大排序,參考 $(x_i \bmod a_j) = x_i - a_j \lfloor \frac{x_i}{a_j} \rfloor$,用線段樹或 BIT 之類的維護 $- a_j \lfloor \frac{x_i}{a_j} \rfloor$ 的部分。由於 $\frac{C}{a_i}$ 都很小,所以 runtime 可以被保證。 ### Subtask 7 > 無額外限制 把 Subtask 5 和 6 的作法結合起來,$a_i \leq \sqrt{C}$ 的部分用前者,$a_i > \sqrt{C}$ 用後者。小心實作的話,令 $m = \max(n,C,q)$,則可以做到 $\mathcal{O}(m \sqrt{m})$。如果常數小一點的話 $\mathcal{O}(m \sqrt{m \log m})$ 也有機會過。 :::spoiler Sample Solution ```cpp= #include <bits/stdc++.h> using namespace std; using ll = long long; constexpr int B = 450; constexpr int C = 200'000; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, q; cin >> n >> q; vector<int> a(n); for (int &i : a) cin >> i; const int nB = (n + B - 1) / B; vector<vector<int>> small_count(B + 1, vector<int>(n)); vector<vector<ll>> large_sum(nB, vector<ll>(C + 1)); // partition the whole array into blocks of size B // precalculation for each block, based on small (<=B) and large (>B) values for (int i = 0; i < nB; i++) { int l = i * B, r = min((i + 1) * B, n); // [l,r) vector<int> large_values; for (int j = l; j < r; j++) { if (a[j] <= B) small_count[a[j]][j]++; else large_values.emplace_back(a[j]); } int large_value_count = large_values.size(); for (int j : large_values) for (int k = j; k <= C; k += j) large_sum[i][k] -= j; for (int j = 1; j <= C; j++) large_sum[i][j] += large_sum[i][j - 1] + large_value_count; } // prefix sum on count of small values for (int i = 1; i <= B; i++) for (int j = 1; j <= n; j++) small_count[i][j] += small_count[i][j - 1]; auto get_small_prefix = [&](int x, int y) { if (y < 0) return 0; else return small_count[x][y]; }; // process queries while (q--) { int l, r, x; cin >> l >> r >> x; l--, r--; int lb = l / B, rb = r / B; ll ans = 0; // small block if (lb == rb) { for (int i = l; i <= r; i++) ans += (x % a[i]); cout << ans << '\n'; continue; } // in block if (lb + 1 <= rb - 1) { // small int blockl = (lb + 1) * B, blockr = rb * B - 1; for (int i = 1; i <= B; i++) { ans += (x % i) * 1LL * (get_small_prefix(i, blockr) - get_small_prefix(i, blockl - 1)); } // large for (int i = lb + 1; i <= rb - 1; i++) { ans += large_sum[i][x]; } } for (int i = l; i < (lb + 1) * B; i++) ans += (x % a[i]); for (int i = rb * B; i <= r; i++) ans += (x % a[i]); cout << ans << '\n'; } } ``` :::