題目連接:https://loj.ac/problem/6277html
給出一個長爲 \(n\) 的數列,以及 \(n\) 個操做,操做涉及區間加法,單點查值。c++
第一行輸入一個數字 \(n\)。
第二行輸入 \(n\) 個數字,第 \(i\) 個數字爲 \(a_i\),以空格隔開。
接下來輸入 \(n\) 行詢問,每行輸入四個數字 \(opt\)、\(l\)、\(r\)、\(c\),以空格隔開。
若 \(opt=0\),表示將位於 \([l,r]\) 之間的數字都加 \(c\)。
若 \(opt=1\),表示詢問 \(a_r\) 的值( \(l\)和 \(c\) 忽略)。算法
對於每次詢問,輸出一行一個數字表示答案。數組
4 1 2 2 3 0 1 3 1 1 0 1 0 0 1 2 2 1 0 2 0
2 5
對於 \(100%\) 的數據,\(1 \le n \le 50000, -2^{31} \le others,ans \le 2^{31}-1\)。spa
本題涉及的算法:數列分塊 。
數列分塊,就是把一個長度爲 \(n\) 的數組,拆分紅一個個連續的長度爲 \(\lfloor \sqrt{n} \rfloor\) 的小塊(若是 \(n\) 不能被 \(\lfloor \sqrt{n} \rfloor\) 整除,則最後一個分塊的長度爲 \(n\) mod \(\lfloor \sqrt{n} \rfloor\))。
而後咱們這裏設 \(m = \sqrt{n}\),那麼咱們能夠定義數組中的第 \(i\) 個元素 \(a_i\) 所屬的分塊爲 \(\lfloor \frac{i-1}{m} \rfloor + 1\)(即:\(a_1,a_2, \cdots ,a_m\) 屬於第 \(1\) 個分塊,\(a_{m+1},a_{m+2}, \cdots ,a_{2m}\) 屬於第 \(2\) 個分塊,……)。
爲了入門方便起見,咱們定義一個數組 \(p[i]\) 表示 \(a_i\) 所屬的分組編號。code
scanf("%d", &n); m = sqrt(n); for (int i = 1; i <= n; i ++) p[i] = (i-1)/m + 1; for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
實際上,全部的分塊都是這樣:把一個數列分紅幾塊,而後對它們進行批量處理。
通常來講,咱們直接把塊大小設爲 \(\sqrt{n}\),但實際上,有時候咱們要根據數據範圍、具體複雜度來肯定塊大小。htm
咱們來分析一下這裏的更新操做。
由於咱們本題只涉及一種類型的更新操做——給區間 \([l,r]\) 範圍內的每個數增長一個值 \(c\)。
這些數一定是屬於連續的塊 \(p[l], p[l]+1, \cdots , p[r]\) 內的。
而且咱們能夠發現:當塊的數量 \(\gt 2\) 時,除了 \(p[l]\) 和 \(p[r]\) 這兩塊可能存在「部分元素須要更新」的狀況,其他全部的分塊(\(p[l]+1, p[l]+2, \cdots , p[r]-1\))都是將整塊元素都增長了 \(c\) 的。blog
對於編號爲 \(k\) 的分塊,咱們能夠知道屬於這個分塊的元素的編號從 \(m \times (k-1)+1\) 到 \(m \times k\)。
若是咱們的更新操做面臨着將一整塊的元素都更新 \(c\)(即每一個元素都增長\(c\)),那麼咱們能夠採起以下樸素方法:get
for (int i = m*(k-1)+1; i <= m*k; i ++) a[i] += c;
這種方法的時間複雜度是 \(O(m) = O( \sqrt{n} )\)。it
但其實咱們不須要對一整塊當中的每個元素都加 \(c\) ,由於他們都加上 \(c\) 了,因此我乾脆標記這個分塊有個總體的增量 \(c\) 便可。
咱們能夠開一個大小爲 \(\sqrt{n}\) 的數組 \(v\),其中 \(v[i]\) 用於表示第 \(i\) 個分塊的總體更新量。
那麼,當我須要對編號爲 \(k\) 的那個塊進行總體的更新操做,我能夠執行以下代碼:
v[k] += c;
因此,咱們能夠將區間 \([l,r]\) 總體增長 \(c\) 的操做拆分以下:
首先,若是 \(a[l]\) 和 \(a[r]\) 屬於同一個分塊(那麼只有一個不完整的分塊),我仍是樸素地從 \(a[l]\) 到 \(a[r]\) 遍歷並將每一個元素加上 \(c\):
if (p[l] == p[r]) { // 說明在同一個分塊,直接更新 for (int i = l; i <= r; i ++) a[i] += c; return; }
不然,說明從 \(a[l]\) 到 \(a[r]\) 至少有兩個分塊。
咱們把問題拆分紅三步走:
首先咱們來分析最左邊的分塊,即 \(a[l]\) 所屬的分塊:
if (l % m != 1) { // 說明l不是分塊p[l]的第一個元素 for (int i = l; p[i]==p[l]; i ++) a[i] += c; } else v[p[l]] += c;
接下來咱們來分析最右邊的分塊,即 \(a[r]\) 所屬的分塊:
if (r % m != 0) { // 說明r不是分塊p[r]的最後一個元素 for (int i = r; p[i]==p[r]; i --) a[i] += c; } else v[p[r]] += c;
在前兩步當中,咱們已經更新完了最左邊的分塊(\(a[l]\)所屬的分塊)及最右邊的分塊(\(a[r]\)所屬的分塊),那麼剩下來的就是中間的那些分塊(即編號爲\(p[l]+1, p[l]+2, \cdots , p[r]-1\)的那些分塊),這些分塊都是整塊更新的,全部對於這些分塊,咱們直接將更新量 \(c\) 加到其總體更新量當中便可。
for (int i = p[l]+1; i < p[r]; i ++) v[i] += c;
若是咱們如今要查詢 \(a[i]\) 對應的值,那麼他應該對應兩部分:
因此 \(a[i]\) 的實際值爲 \(a[i] + v[p[i]]\)。
這樣,咱們就分析玩了數列分塊對應的更新和查詢這兩種操做。
完整實現代碼以下:
#include <bits/stdc++.h> using namespace std; const int maxn = 50050; int n, m, a[maxn], p[maxn], v[300], op, l, r, c; void add(int l, int r, int c) { if (p[l] == p[r]) { // 說明在同一個分塊,直接更新 for (int i = l; i <= r; i ++) a[i] += c; return; } if (l % m != 1) { // 說明l不是分塊p[l]的第一個元素 for (int i = l; p[i]==p[l]; i ++) a[i] += c; } else v[p[l]] += c; if (r % m != 0) { // 說明r不是分塊p[r]的最後一個元素 for (int i = r; p[i]==p[r]; i --) a[i] += c; } else v[p[r]] += c; for (int i = p[l]+1; i < p[r]; i ++) v[i] += c; } int main() { scanf("%d", &n); m = sqrt(n); for (int i = 1; i <= n; i ++) p[i] = (i-1)/m + 1; for (int i = 1; i <= n; i ++) scanf("%d", a+i); for (int i = 0; i < n; i ++) { scanf("%d%d%d%d", &op, &l, &r, &c); if (op == 0) add(l, r, c); else printf("%d\n", a[r] + v[p[r]]); } return 0; }
更新最左邊的那個分塊:
由於每一個分塊的元素不超過 \(\sqrt{n}\) 因此操做次數不會超過 \(\sqrt{n}\);
更新最右邊的那個分塊:
由於每一個分塊的元素不超過 \(\sqrt{n}\) 因此操做次數不會超過 \(\sqrt{n}\);
更新中間的那些分塊:
由於分塊個數不會超過 \(\sqrt{n}+1\) 因此中間那些分塊的數量不會超過 \(\sqrt{n}\)。
因此更新一次的時間複雜度爲 \(O( \sqrt{n} ) + O( \sqrt{n} ) + O( \sqrt{n} ) = O( \sqrt{n} )\)。
查詢直接返回 \(a[i] + v[p[i]]\) ,因此查詢的時間複雜度爲 \(O(1)\)。
綜上所述,由於一共有 \(n\) 次操做,因此該算法的時間複雜度爲 \(O(n \sqrt{n})\)。