Hướng dẫn giải của Xây dựng


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.

Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.

Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.

Bạn phải đăng nhập để mở lời giải này.

Đăng nhập

Tác giả: Hiếu Nguyễn, wall, shiroboyy, QioCas

Hiểu bài toán

Bài toán yêu cầu mô phỏng quá trình xây dựng n tòa nhà với độ cao mục tiêu a_i. Ban đầu độ cao các tòa nhà bằng 0. Tại mỗi giai đoạn, chọn một đoạn [L, R] để tăng độ cao của các tòa nhà trong đoạn lên 1. Dự án dừng khi tất cả tòa nhà đạt độ cao mục tiêu.

Có hai loại sự kiện:

  1. Cập nhật: Tăng a_i lên k cho mọi i trong đoạn [L, R].
  2. Truy vấn: Với giả định chỉ các tòa nhà từ L đến R cần được xây dựng (các tòa nhà còn lại coi như không cần xây, tức a_i = 0), hỏi số giai đoạn tối thiểu để hoàn thành.

Ta cần xử lý các truy vấn và cập nhật này một cách hiệu quả với n, m lên tới 10^5.

Các cách tiếp cận

Cách Brute Force (Mô phỏng trực tiếp)
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void solve() {
    int n, m;
    cin >> n >> m;
    vector<long long> a(n + 1);
    for (int i = 1; i <= n; ++i) cin >> a[i];

    while (m--) {
        int type;
        cin >> type;
        if (type == 1) {
            int l, r, k;
            cin >> l >> r >> k;
            for (int i = l; i <= r; ++i) a[i] += k;
        } else {
            int l, r;
            cin >> l >> r;
            // Tính số giai đoạn
            // Công thức: sum(a[i]) - sum(min(a[i], a[i-1]))
            // Hoặc đơn giản là greedily build theo thứ tự từ trái sang phải
            // Giai đoạn = a[l] + sum(max(0, a[i] - a[i-1])) for i in l+1..r
            long long ans = a[l];
            for (int i = l + 1; i <= r; ++i) {
                if (a[i] > a[i-1]) ans += (a[i] - a[i-1]);
            }
            cout << ans << '\n';
        }
    }
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t;
    cin >> t;
    while (t--) solve();
    return 0;
}
  • Time Complexity: O(m * n) - Quá chậm, chỉ dành cho dữ liệu nhỏ
  • Space Complexity: O(n)

Cách tiếp cận này cập nhật mảng a trực tiếp và tính kết quả truy vấn bằng cách duyệt toàn bộ đoạn [L, R]. Với mỗi truy vấn, ta cần O(n) thao tác. Tổng thời gian chạy sẽ khoảng O(m * n), vượt quá giới hạn thời gian cho phép (10^10 thao tác).

Cách Segment Tree (Phân tích công thức)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int MAXN = 100005;

int n, m;
ll a[MAXN];

// Cây phân đoạn để lưu:
// 1. Giá trị a[i] (phục vụ tính min(a[i], a[i-1]))
// 2. Giá trị diff[i] = max(0, a[i] - a[i-1])
// 3. Tổng diff
// 4. Phép cập nhật range add

struct Node {
    ll sum_diff;
    ll val_first; // a[l] của node này
    ll val_last;  // a[r] của node này
    ll lazy;
} tree[4 * MAXN];

void push_down(int id, int l, int r) {
    if (tree[id].lazy != 0) {
        int mid = (l + r) / 2;
        // Update left child
        tree[id*2].lazy += tree[id].lazy;
        tree[id*2].val_first += tree[id].lazy;
        tree[id*2].val_last += tree[id].lazy;
        // Update right child
        tree[id*2+1].lazy += tree[id].lazy;
        tree[id*2+1].val_first += tree[id].lazy;
        tree[id*2+1].val_last += tree[id].lazy;

        tree[id].lazy = 0;
    }
}

void update_node(int id, ll val) {
    tree[id].lazy += val;
    tree[id].val_first += val;
    tree[id].val_last += val;
}

void pull_up(int id, int l, int r) {
    int mid = (l + r) / 2;
    tree[id].val_first = tree[id*2].val_first;
    tree[id].val_last = tree[id*2+1].val_last;

    // Tính lại sum_diff
    // sum_diff = sum_diff(left) + sum_diff(right) + correction
    // correction = max(0, val_left_last - val_right_first)
    // Lưu ý: sum_diff của node con chưa bao gồm hiệu giữa các node con.
    // Thực tế, diff[i] phụ thuộc vào a[i] và a[i-1].
    // Nếu ta định nghĩa node涵盖 [L, R], sum_diff là sum_{i=L+1}^{R} max(0, a[i]-a[i-1])
    // Thì khi gộp 2 node [L, M] và [M+1, R]:
    // sum_diff = sum_diff(L, M) + sum_diff(M+1, R) + max(0, a[M+1] - a[M])

    ll correction = max(0LL, tree[id*2+1].val_first - tree[id*2].val_last);
    tree[id].sum_diff = tree[id*2].sum_diff + tree[id*2+1].sum_diff + correction;
}

void build(int id, int l, int r) {
    tree[id].lazy = 0;
    if (l == r) {
        tree[id].sum_diff = 0; // diff[l] không tồn tại (phụ thuộc a[l-1])
        tree[id].val_first = a[l];
        tree[id].val_last = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(id*2, l, mid);
    build(id*2+1, mid+1, r);
    pull_up(id, l, r);
}

void update(int id, int l, int r, int u, int v, ll val) {
    if (v < l || r < u) return;
    if (u <= l && r <= v) {
        update_node(id, val);
        return;
    }
    push_down(id, l, r);
    int mid = (l + r) / 2;
    update(id*2, l, mid, u, v, val);
    update(id*2+1, mid+1, r, u, v, val);
    pull_up(id, l, r);
}

ll query_val(int id, int l, int r, int pos) {
    if (l == r) return tree[id].val_first;
    push_down(id, l, r);
    int mid = (l + r) / 2;
    if (pos <= mid) return query_val(id*2, l, mid, pos);
    else return query_val(id*2+1, mid+1, r, pos);
}

ll query_diff(int id, int l, int r, int u, int v) {
    if (v < l || r < u) return 0;
    if (u <= l && r <= v) return tree[id].sum_diff;
    push_down(id, l, r);
    int mid = (l + r) / 2;
    ll left = query_diff(id*2, l, mid, u, v);
    ll right = query_diff(id*2+1, mid+1, r, u, v);

    // Nếu[left] và [right]被完全分离,需要加上中间的连接项
    // 仅当两个区间相邻且都有效时
    if (max(u, l) <= mid && mid+1 <= min(v, r)) {
        // Need correction between mid and mid+1
        ll val_mid = tree[id*2].val_last; // a[mid] after push down? No, need actual value.
        // Correct way: Query a[mid] and a[mid+1] explicitly
        ll a_mid = query_val(id*2, l, mid, mid);
        ll a_mid_next = query_val(id*2+1, mid+1, r, mid+1);
        left += max(0LL, a_mid_next - a_mid);
    }
    return left + right;
}

void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    build(1, 1, n);
    while (m--) {
        int type;
        cin >> type;
        if (type == 1) {
            int l, r, k;
            cin >> l >> r >> k;
            update(1, 1, n, l, r, k);
        } else {
            int l, r;
            cin >> l >> r;
            ll val_l = query_val(1, 1, n, l);
            ll diff_sum = 0;
            if (l < r) diff_sum = query_diff(1, 1, n, l + 1, r);
            cout << val_l + diff_sum << '\n';
        }
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int t;
    cin >> t;
    while (t--) solve();
    return 0;
}
  • Time Complexity: O(m log n)
  • Space Complexity: O(n)

Đặt câu hỏi: Số giai đoạn cần thiết để xây dựng đoạn [L, R] là gì? Nếu chỉ có 1 tòa nhà, số giai đoạn là a[L]. Nếu có nhiều tòa nhà, ta có thể greedily xây dựng. Giai đoạn 1 xây toàn bộ đoạn, giai đoạn 2 xây đoạn [L, R] nhưng bỏ qua các đỉnh mà tòa nhà bên trái đã xây xong. Định nghĩa diff[i] = max(0, a[i] - a[i-1]). Số giai đoạn = a[L] + sum_{i=L+1}^{R} diff[i]. Nhiệm vụ trở thành duy trì mảng a và mảng diff (tổng tiền tố của diff). Phép cập nhật range [L, R] + k làm a[i] tăng k. Điều này làm thay đổi diff[L] (vì a[L] tăng so với a[L-1]), diff[R+1] (vì a[R+1] không đổi nhưng a[R] tăng), và giữ nguyên diff[i] cho L < i <= R (vì cả a[i] và a[i-1] đều tăng k). Ta cần cấu trúc dữ liệu hỗ trợ:

  1. Range update a[i].
  2. Truy vấn a[L].
  3. Truy vấn tổng diff trên đoạn [L+1, R]. Vì diff[i] phụ thuộc vào a[i] và a[i-1], ta có thể dùng Segment Tree lưu trữ giá trị a và tổng diff. Segment Tree cần hỗ trợ lazy propagation cho phép cộng dồn vào a[i]. Khi a[i] thay đổi, ta phải cập nhật lại diff[i] và diff[i+1] tại node lá và cập nhật lên cha. Node cha cần lưu tổng diff, giá trị a đầu và a cuối của đoạn con. Độ phức tạp: O(m log n).
Cách Fenwick Tree (Optimized)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int MAXN = 100005;

int n, m;
ll a[MAXN];
ll c[MAXN]; // c[i] = a[i] - a[i-1]

struct Fenwick {
    ll bit[MAXN];
    void update(int i, ll val) {
        for (; i <= n; i += i & -i) bit[i] += val;
    }
    ll query(int i) {
        ll sum = 0;
        for (; i > 0; i -= i & -i) sum += bit[i];
        return sum;
    }
    ll query(int l, int r) {
        return query(r) - query(l - 1);
    }
} f_a, f_c;

void update_diff(int i, ll val) {
    // Diff[i] = max(0, c[i])
    // We need to maintain sum of max(0, c[i]).
    // Since c[i] changes, we need to update the sum.
    // However, max(0, c[i]) is not linear.
    // We need a structure that supports range add on c, and range query on max(0, c).
    // But notice: Range add on a implies c[L] += k, c[R+1] -= k.
    // c[i] for i in (L, R] remains unchanged.
    // So c changes at L and R+1.
    // At these points, we can update the contribution to sum of max(0, c[i]).
}

// Solution using two Fenwick trees:
// 1. BIT for a (range add, point query) -> to get a[i]
// 2. BIT for c (range add, point query) -> to get c[i]
// Wait, we need sum of max(0, c[i]).
// Let's use the property: Total stages = a[L] + sum_{i=L+1}^R max(0, a[i]-a[i-1])
// a[L] = sum_{k=1}^L c[k]
// sum_{i=L+1}^R max(0, c[i])
// Total = sum_{k=1}^L c[k] + sum_{i=L+1}^R max(0, c[i])
// This formula is not convenient for range updates.

// Let's try another formula: Total = a[R] + sum_{i=L}^{R-1} max(0, a[i]-a[i+1])
// Or simply: Total = a[L] + sum_{i=L+1}^R max(0, a[i]-a[i-1])
// Range update [L, R] + k:
// a[L...R] += k.
// c[L] += k (since a[L] - a[L-1] increases by k)
// c[R+1] -= k (since a[R+1] - a[R] decreases by k)
// c[i] unchanged for L < i <= R.
// So we need to support updating c at L and R+1.
// We also need to query sum of max(0, c[i]) over a range.
// Since c[i] changes by specific values at specific points, we can just update the BIT storing max(0, c[i]) at those points.

// Wait, what about the a[L] term?
// a[L] = sum_{j=1}^L c[j].
// If we use a Fenwick tree for c, we can get a[L].

// So the plan:
// Maintain Fenwick tree for c.
// Maintain Fenwick tree for D, where D[i] = max(0, c[i]).
// When we update c[L] += k:
//   old_c = c[L], new_c = c[L] + k
//   old_D = max(0, old_c), new_D = max(0, new_c)
//   update D[L] by (new_D - old_D)
//   update c[L] in c-BIT by k
// Similarly for c[R+1] -= k.

// But wait, there is a catch.
// The formula is Total = a[L] + sum_{i=L+1}^R D[i].
// a[L] = sum_{j=1}^L c[j].
// sum_{i=L+1}^R D[i] = sum_{i=L+1}^R max(0, c[i]).

// So we need two BITs:
// BIT_C: stores c[i]. Supports range sum query (to get a[L]).
// BIT_D: stores D[i] = max(0, c[i]). Supports range sum query.

// However, we need to handle updates to c.
// We can't just "add k" to a range in BIT_D because max(0, x) is not linear.
// But notice: Range update on a only changes c at L and R+1.
// So we only need to update BIT_D at L and R+1.

// Algorithm:
// 1. Initialize arrays a and c.
//    c[i] = a[i] - a[i-1] (for i > 1), c[1] = a[1].
//    Build BIT_C from c.
//    Build BIT_D from D[i] = max(0, c[i]).
// 2. Process event type 1 (L, R, k):
//    Update c[L] += k. Update BIT_C. Update BIT_D.
//    If R+1 <= n: Update c[R+1] -= k. Update BIT_C. Update BIT_D.
// 3. Process event type 2 (L, R):
//    ans = query(BIT_C, 1, L) + query(BIT_D, L+1, R)
//    print ans.

void solve() {
    cin >> n >> m;
    // Reset BITs
    memset(f_a.bit, 0, sizeof(f_a.bit));
    memset(f_c.bit, 0, sizeof(f_c.bit));

    vector<ll> c(n + 1);
    vector<ll> a(n + 1);

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

    // Initialize c and BITs
    // We need to build BIT_D which stores max(0, c[i]).
    // Since we update point by point, we can just loop and add.

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

    // We need a separate array for D values to track changes, or just recalc on update.
    // Let's use a global array `current_c` and `current_D`.

    for (int i = 1; i <= n; ++i) {
        f_c.update(i, c[i]);
        f_a.update(i, max(0LL, c[i])); // Using f_a for D values (misnamed in struct but ok)
    }
    // Wait, f_a is used for D in my thought process.
    // Let's rename struct to BIT.

    // Actually, let's just use the code logic.
    // We need to store current c[i] to know the old value of max(0, c[i]).

    // Resetting for the code below:
    // struct BIT { ... } bit_c, bit_d;
    // current_c[i] array.
    // current_d[i] array.
}

struct BIT {
    ll tree[100005];
    void add(int i, ll v) { for (; i <= n; i += i & -i) tree[i] += v; }
    ll sum(int i) { ll s = 0; for (; i > 0; i -= i & -i) s += tree[i]; return s; }
    ll query(int l, int r) { return sum(r) - sum(l-1); }
};

void run() {
    cin >> n >> m;
    vector<ll> a(n + 2), c(n + 2);
    BIT bit_c, bit_d;

    // Clean trees
    for(int i=0; i<=n+1; ++i) { bit_c.tree[i] = 0; bit_d.tree[i] = 0; }

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

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

    for (int i = 1; i <= n; ++i) {
        bit_c.add(i, c[i]);
        bit_d.add(i, max(0LL, c[i]));
    }

    while (m--) {
        int type; cin >> type;
        if (type == 1) {
            int l, r, k; cin >> l >> r >> k;

            // Update c[l]
            ll old_c = c[l];
            c[l] += k;
            bit_c.add(l, k);

            ll old_D = max(0LL, old_c);
            ll new_D = max(0LL, c[l]);
            bit_d.add(l, new_D - old_D);

            // Update c[r+1]
            if (r + 1 <= n) {
                old_c = c[r+1];
                c[r+1] -= k;
                bit_c.add(r+1, -k);

                old_D = max(0LL, old_c);
                new_D = max(0LL, c[r+1]);
                bit_d.add(r+1, new_D - old_D);
            }
        } else {
            int l, r; cin >> l >> r;
            // a[L] = sum(c[1]...c[L])
            // sum_{i=L+1}^R max(0, c[i])
            ll ans = bit_c.sum(l);
            if (l < r) ans += bit_d.query(l + 1, r);
            cout << ans << '\n';
        }
    }
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t; cin >> t;
    while(t--) run();
    return 0;
}
  • Time Complexity: O(m log n)
  • Space Complexity: O(n)

Sử dụng 2 Fenwick Trees. Fenwick 1 (BITC): Lưu mảng chênh lệch c[i] = a[i] - a[i-1] (với a[0] = 0). BITC hỗ trợ cập nhật điểm và truy vấn tổng tiền tố. Tổng tiền tố BITC tại L chính là a[L]. Fenwick 2 (BITD): Lưu mảng D[i] = max(0, c[i]). BIT_D hỗ trợ truy vấn tổng.

Phép cập nhật [L, R] + k:

  • a[L...R] tăng k.
  • Điều này làm c[L] tăng k (vì a[L] tăng, a[L-1] không đổi).
  • c[R+1] giảm k (vì a[R+1] không đổi, a[R] tăng).
  • Các c[i] còn lại không đổi.

Khi c[i] thay đổi, ta cập nhật BITC tại i (thêm giá trị thay đổi). Đồng thời, ta phải cập nhật BITD tại i với giá trị chênh lệch của max(0, c[i]) trước và sau khi thay đổi.

Truy vấn [L, R]:

  • Số giai đoạn = a[L] + sum_{i=L+1}^R max(0, c[i])
  • a[L] = query(BIT_C, 1, L).
  • sum max(0, c[i]) = query(BIT_D, L+1, R).

Độ phức tạp: O(m log n). Cấu trúc này hiệu quả hơn Segment Tree ở chỗ code ngắn gọn hơn và nhanh hơn một chút.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(m * n) - Quá chậm, chỉ dành cho dữ liệu nhỏ O(n) Brute Force (Mô phỏng trực tiếp)
2 O(m log n) O(n) Segment Tree (Phân tích công thức)
3 O(m log n) O(n) Fenwick Tree (Optimized)

Bài học kinh nghiệm

  • Công thức tính số giai đoạn tối thiểu cho đoạn [L, R] là a[L] + sum_{i=L+1}^{R} max(0, a[i] - a[i-1]).
  • Phép cập nhật range [L, R] + k chỉ ảnh hưởng trực tiếp đến 2 vị trí chênh lệch: c[L] tăng k và c[R+1] giảm k. Điều này cho phép sử dụng cấu trúc dữ liệu hỗ trợ cập nhật điểm (Fenwick Tree) thay vì phải cập nhật toàn bộ đoạn.

Lỗi thường gặp

  • Quên xử lý trường hợp R = n khi cập nhật c[R+1] (truy vấn ngoài mảng).
  • Lỗi tính toán a[L] và tổng max(0, c[i]): phải chú ý chỉ số bắt đầu và kết thúc của đoạn con.
  • Sử dụng Segment Tree với lazy propagation cho các giá trị không tuyến tính (như max(0, x)) mà không có cách tính toán lại node cha đúng cách.

Bình luận

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.