Array Preprocessing & Range Update

➖ Difference Array – Mảng hiệu

Difference Array biến mỗi phép cộng trên đoạn từ O(n) thành O(1). Sau khi xử lý tất cả cập nhật, ta dùng Prefix Sum để khôi phục mảng kết quả. Đây là kỹ thuật nền tảng cho sweep line, cập nhật đoạn offline và mảng hiệu 2D.

Range AddO(1) mỗi cập nhật
RebuildO(n) bằng Prefix Sum
OfflineXử lý xong rồi mới truy vấn
2DCập nhật hình chữ nhật O(1)

🎯 Mục tiêu bài học

Cần hiểu

  • Ý nghĩa của diff[i].
  • Cách chuyển mảng gốc thành mảng hiệu.
  • Cách cộng x vào đoạn [L,R] bằng hai thay đổi.
  • Cách khôi phục mảng bằng Prefix Sum.
  • Cách mở rộng sang hình chữ nhật 2D.

Khi nào dùng?

  • Nhiều cập nhật cộng trên đoạn.
  • Không cần biết kết quả ngay sau từng cập nhật.
  • Bài phủ đoạn, số lớp phủ, lịch sự kiện.
  • Tô/cộng hình chữ nhật trên ma trận.
  • Bước trung gian trước Prefix Sum.

1. Đặt vấn đề

Bài toán mở đầu.
Cho mảng a[0..n-1] và q thao tác. Mỗi thao tác có dạng:
Cộng x vào mọi phần tử trong đoạn [L,R]
Sau khi thực hiện toàn bộ q thao tác, hãy in mảng cuối cùng.

1.1. Cách làm trực tiếp

Với mỗi thao tác, duyệt từ L đến R và cộng x vào từng phần tử.

Cách làmMỗi cập nhậtKhôi phục/in kết quảTổng thời gian
Duyệt trực tiếpO(R-L+1)O(n)Có thể O(nq)
Difference ArrayO(1)O(n)O(n+q)

1.2. Quan sát quan trọng

Khi cộng x vào đoạn [L,R], mức tăng bắt đầu từ L và kết thúc ngay sau R:

  • Tại L: bắt đầu cộng thêm x.
  • Tại R+1: hủy tác động bằng cách trừ x.
diff[L] += x;    diff[R+1] -= x
Bản chất: Difference Array không lưu trực tiếp giá trị từng phần tử, mà lưu sự thay đổi giữa hai vị trí liên tiếp.

2. Định nghĩa và công thức

Từ mảng a sang diff

diff[0] = a[0]
diff[i] = a[i] - a[i-1]

Từ diff khôi phục a

a[0] = diff[0]
a[i] = a[i-1] + diff[i]
Liên hệ với Prefix Sum: Mảng gốc chính là Prefix Sum của mảng hiệu.

3. Mô phỏng tạo mảng hiệu

Mảng a

Mảng diff

4. Mô phỏng cập nhật đoạn

Mảng hiệu hiện tại

Mảng kết quả

Cập nhật [L,R] += x chỉ thay đổi tối đa hai vị trí:
diff[L] += x; nếu R+1 < n thì diff[R+1] -= x

5. Difference Array 2D

Với ma trận, ta có thể cộng x vào toàn bộ hình chữ nhật từ (x1,y1) đến (x2,y2) bằng bốn mốc:

diff[x1][y1] += v
diff[x1][y2+1] -= v
diff[x2+1][y1] -= v
diff[x2+1][y2+1] += v

Ma trận kết quả 5×6

Khôi phục 2D: cần lấy Prefix Sum theo cả hàng và cột, hoặc dùng công thức bao hàm–loại trừ hai chiều.

6. Phân tích thuật toán

Thao tácCách trực tiếpDifference ArrayGhi chú
Tạo diff từ aO(n)Mỗi vị trí tính một hiệu.
Cộng x vào [L,R]O(R-L+1)O(1)Đổi tại L và R+1.
Khôi phục mảngO(n)Prefix Sum của diff.
Cập nhật hình chữ nhậtO(diện tích)O(1)Đổi bốn góc.
Khôi phục ma trậnO(nm)Prefix Sum 2D.
Giới hạn: Difference Array phù hợp cho cập nhật offline. Nếu cần query và update xen kẽ trực tuyến, nên dùng Fenwick Tree hoặc Segment Tree.

7. Code mẫu

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, q;
    cin >> n >> q;

    vector<long long> a(n);
    for (long long &x : a) cin >> x;

    vector<long long> diff(n);

    diff[0] = a[0];
    for (int i = 1; i < n; i++) {
        diff[i] = a[i] - a[i - 1];
    }

    while (q--) {
        int left, right;
        long long value;
        cin >> left >> right >> value;

        --left;
        --right;

        diff[left] += value;

        if (right + 1 < n) {
            diff[right + 1] -= value;
        }
    }

    a[0] = diff[0];

    for (int i = 1; i < n; i++) {
        a[i] = a[i - 1] + diff[i];
    }

    for (long long x : a) {
        cout << x << ' ';
    }
}
int n, q;
cin >> n >> q;

vector<long long> diff(n + 1, 0);

while (q--) {
    int left, right;
    long long value;

    cin >> left >> right >> value;
    --left;
    --right;

    diff[left] += value;
    diff[right + 1] -= value;
}

vector<long long> a(n, 0);
long long current = 0;

for (int i = 0; i < n; i++) {
    current += diff[i];
    a[i] = current;
}
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m, q;
    cin >> n >> m >> q;

    vector<vector<long long>> diff(
        n + 2,
        vector<long long>(m + 2, 0)
    );

    while (q--) {
        int x1, y1, x2, y2;
        long long value;

        cin >> x1 >> y1 >> x2 >> y2 >> value;

        diff[x1][y1] += value;
        diff[x1][y2 + 1] -= value;
        diff[x2 + 1][y1] -= value;
        diff[x2 + 1][y2 + 1] += value;
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            diff[i][j]
                += diff[i - 1][j]
                + diff[i][j - 1]
                - diff[i - 1][j - 1];
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cout << diff[i][j] << ' ';
        }
        cout << '\n';
    }
}
def apply_range_updates(a, updates):
    n = len(a)
    diff = [0] * n

    diff[0] = a[0]

    for i in range(1, n):
        diff[i] = a[i] - a[i - 1]

    for left, right, value in updates:
        diff[left] += value

        if right + 1 < n:
            diff[right + 1] -= value

    result = [0] * n
    result[0] = diff[0]

    for i in range(1, n):
        result[i] = result[i - 1] + diff[i]

    return result

8. Giải thích code

8.1. Vì sao diff[i] = a[i] - a[i-1]?

diff[i] biểu diễn mức thay đổi khi đi từ vị trí i−1 sang i. Khi cộng dồn các mức thay đổi, ta nhận lại giá trị ban đầu.

8.2. Vì sao chỉ sửa L và R+1?

diff[L] += x làm mọi prefix từ L trở đi tăng x. diff[R+1] -= x hủy mức tăng từ vị trí R+1, nên chỉ đoạn L đến R bị ảnh hưởng.

8.3. Vì sao phải kiểm tra R+1 < n?

Nếu R là phần tử cuối, không có vị trí R+1 trong mảng diff kích thước n. Có thể tránh nhánh này bằng cách cấp phát diff kích thước n+1.

8.4. Vì sao dùng long long?

Một phần tử có thể nhận nhiều lần cộng. Tổng cuối cùng có thể vượt giới hạn int dù mỗi x riêng lẻ không lớn.

8.5. Tại sao cập nhật 2D dùng bốn góc?

Hai dấu cộng bắt đầu vùng tác động; hai dấu trừ hủy tác động theo hàng và cột. Góc dưới phải bị trừ hai lần nên phải cộng lại một lần.

8.6. Có thể query ngay sau mỗi update không?

Không hiệu quả nếu phải khôi phục toàn bộ mảng sau từng cập nhật. Difference Array phù hợp khi gom tất cả update trước rồi dựng kết quả một lần.

9. Lỗi thường gặp

  • Quên đổi chỉ số 1-based của đề sang 0-based trong code.
  • Quên kiểm tra R+1 < n.
  • Dùng dấu cộng tại R+1 thay vì dấu trừ.
  • Khôi phục mảng bằng công thức sai.
  • Nhầm Difference Array với Prefix Sum.
  • Dùng Difference Array khi cần query xen kẽ update.
  • Dùng int cho dữ liệu tổng lớn.
  • Trong 2D, quên cộng lại góc (x2+1,y2+1).
  • Với mảng ban đầu khác 0, quên tạo diff từ a trước khi update.

10. Quiz và bài tập

Câu 1. Để cộng x vào đoạn [L,R], cần làm gì?
Câu 2. Khôi phục mảng từ diff bằng kỹ thuật nào?
Câu 3. Khi nào Difference Array phù hợp nhất?

📘 Cơ bản

  1. Cộng q đoạn rồi in mảng.
  2. Đếm số lớp phủ tại mỗi vị trí.
  3. Tìm vị trí được phủ nhiều nhất.

📗 Trung bình

  1. Lịch tăng giảm số người theo thời gian.
  2. Cập nhật đoạn và tìm max cuối cùng.
  3. Phủ đoạn trên trục tọa độ nén.

📙 Nâng cao

  1. Difference Array 2D.
  2. Cập nhật cấp số cộng trên đoạn.
  3. Difference bậc hai.

🐉 HSG

  1. Sweep line bằng event difference.
  2. Imos Method.
  3. Difference + coordinate compression.