LibreOJ 6277. 數列分塊入門 1 題解

題目連接: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]\) 至少有兩個分塊。
咱們把問題拆分紅三步走:

  1. 更新最左邊的那個分塊;
  2. 更新最右邊的那個分塊;
  3. 更新中間的那些分塊(若是有的話)。

step.1 更新最左邊的那個分塊

首先咱們來分析最左邊的分塊,即 \(a[l]\) 所屬的分塊:

  • 若是 \(l\) mod \(m \ne 1\),說明 \(a[l]\) 不是他所在的分塊的第一個元素,那麼我仍是須要從 \(a[l]\) 開始從前日後更新全部和 \(a[l]\) 屬於同一個分塊的元素(即:將全部知足條件 \(i \ge l\)\(p[i] = p[l]\)\(a[i]\) 加上 \(c\));
  • 不然(即 \(l\) mod \(m = 1\)),說明 \(a[l]\) 是他所在的分塊的第一個元素,那麼咱們只要整塊更新便可:\(v[p[l]] += c\)
if (l % m != 1) {    // 說明l不是分塊p[l]的第一個元素
    for (int i = l; p[i]==p[l]; i ++)
        a[i] += c;
}
else v[p[l]] += c;

step.2 更新最右邊的那個分塊

接下來咱們來分析最右邊的分塊,即 \(a[r]\) 所屬的分塊:

  • 若是 \(r\) mod \(m = 0\),說明 \(a[r]\) 不是他所在的分塊的最後一個元素,那麼咱們須要從 \(a[r]\) 開始從後往前更新全部和 \(a[r]\) 屬於同一個分塊的元素(即:將全部知足條件 \(i \le r\)\(p[i] = p[r]\)\(a[i]\) 加上 \(c\));
  • 不然(即 \(r\) mod \(m = 0\)),說明 \(a[r]\) 是他所在的分塊的最後一個元素,那麼咱們只須要整塊更新便可:\(v[p[r]] += 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;

3. 更新中間的那些分塊(若是有的話)

在前兩步當中,咱們已經更新完了最左邊的分塊(\(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]\) 對應的值,那麼他應該對應兩部分:

  1. \(a[i]\) 自己的值;
  2. \(a[i]\) 所屬的分塊 \(p[i]\) 的總體更新量 \(v[p[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})\)

參考連接:http://www.javashuo.com/article/p-giqzkvug-he.html

相關文章
相關標籤/搜索