Hướng dẫn giải của Đoạn con có tổng không âm


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, tdq, mimiquang1234, binhcode8

Hiểu bài toán

Cho một dãy số nguyên $A$ gồm $N$ phần tử. Tìm độ dài lớn nhất của một đoạn con liên tiếp có tổng các phần tử không âm (tức là tổng >= 0). Nếu không tồn tại đoạn con nào thỏa mãn, in ra -1.

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

Cách Brute Force (Tử dãy)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n; cin >> n;
    vector<int> a(n);
    for (int &x : a) cin >> x;
    int res = 0;
    for (int i = 0; i < n; i++) {
        long long sum = 0;
        for (int j = i; j < n; j++) {
            sum += a[j];
            if (sum >= 0) {
                res = max(res, j - i + 1);
            }
        }
    }
    if (res == 0) cout << -1;
    else cout << res;
    return 0;
}
  • Time Complexity: O(N^2)
  • Space Complexity: O(1)

Đây là cách tiếp cận trực quan nhất. Duyệt qua tất cả các vị trí bắt đầu $i$ của đoạn con. Với mỗi $i$, duyệt qua tất cả các vị trí kết thúc $j \ge i$, tính tổng và cập nhật độ dài lớn nhất nếu tổng không âm. Với $N \le 10^5$, phương pháp này thực hiện khoảng $10^{10}$ phép tính, vượt quá giới hạn thời gian (thường là 1 giây).

Cách Prefix Sum và Sắp xếp
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    long long n;
    cin >> n;
    vector<long long> a(n);
    for (auto &x : a) cin >> x;

    vector<pair<long long, long long>> pref(n + 1);
    long long sum = 0;
    pref[0] = {0, 0};

    // Tính tổng tiền tố và lưu chỉ số
    for (int i = 1; i <= n; i++) {
        sum += a[i - 1];
        pref[i] = {sum, i};
    }

    // Sắp xếp các tổng tiền tố theo giá trị tăng dần
    sort(pref.begin(), pref.end());

    long long ans = -1, mn = 1e18;

    // Duyệt qua các tổng đã sắp xếp, giữ lại chỉ số nhỏ nhất đã gặp
    for (auto &p : pref) {
        long long idx = p.second;
        if (idx < mn) mn = idx;
        ans = max(ans, idx - mn);
    }

    if (ans <= 0) cout << -1;
    else cout << ans;
    return 0;
}
  • Time Complexity: O(N log N)
  • Space Complexity: O(N)

Sử dụng tổng tiền tố $P[i] = \sum{k=1}^i ak$. Tổng đoạn con từ $i$ đến $j$ là $P[j] - P[i-1]$. Điều kiện $P[j] - P[i-1] \ge 0$ tương đương với $P[i-1] \le P[j]$. Ta cần tìm $i-1$ và $j$ sao cho $P[i-1] \le P[j]$ và $j - (i-1)$ là lớn nhất. Bằng cách sắp xếp các cặp (giá trị tổng tiền tố, chỉ số), ta có thể duyệt qua các tổng đã sắp xếp. Khi gặp một tổng tại vị trí $j$, nếu trước đó đã gặp một tổng nhỏ hơn hoặc bằng tại vị trí $i-1$ nhỏ nhất, ta cập nhật kết quả. Phương pháp này hiệu quả hơn Brute Force nhưng vẫn chậm hơn phương pháp tối ưu.

Cách Monotonic Stack (Ngăn xếp đơn điệu)
#include <bits/stdc++.h>
using namespace std;

#define int long long

signed main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    // Tính tổng tiền tố
    vector<int> p(n + 1, 0);
    for (int i = 1; i <= n; ++i) {
        p[i] = p[i - 1] + a[i];
    }
    // Xây dựng ngăn xếp chứa các chỉ số i sao cho p[i] giảm dần
    vector<int> st;
    for (int i = 0; i <= n; ++i) {
        if (st.empty() || p[i] < p[st.back()]) st.push_back(i);
    }
    int ans = 0;
    // Duyệt ngược từ cuối về đầu
    for (int j = n; j >= 0; --j) {
        while (!st.empty() && p[j] >= p[st.back()]) {
            ans = max(ans, j - st.back());
            st.pop_back();
        }
        if (st.empty()) break;
    }
    cout << (ans == 0 ? -1 : ans);
    return 0;
}
  • Time Complexity: O(N)
  • Space Complexity: O(N)

Đây là phương pháp tối ưu nhất.

  1. Tính tổng tiền tố $P[k]$ với $P[0]=0$. Đặt $P[k]$ vào mảng.
  2. Xây dựng một ngăn xếp chứa các chỉ số $i$ sao cho các giá trị $P[i]$ trong ngăn xếp tăng dần (từ đáy lên). Cụ thể, ta chỉ đẩy chỉ số $i$ vào ngăn xếp nếu $P[i] < P[st.back()]$. Điều này đảm bảo rằng nếu ta tìm được $j$ sao cho $P[j] \ge P[st.back()]$, thì $j - st.back()$ là độ dài lớn nhất có thể tính được với giá trị $P[st.back()]$ này (vì $st.back()$ là chỉ số nhỏ nhất có giá trị tổng như vậy).
  3. Duyệt $j$ từ $n$ về $0$. Với mỗi $j$, ta kiểm tra ngăn xếp: nếu $P[j] \ge P[st.top()]$, điều này có nghĩa là đoạn con từ $st.top()$ đến $j$ có tổng không âm. Vì $j$ đang giảm dần, ta cập nhật độ dài $j - st.top()$ và loại bỏ $st.top()$ khỏi ngăn xếp (vì các $j$ nhỏ hơn sau này sẽ cho độ dài ngắn hơn). Phương pháp này đảm bảo mỗi phần tử được đẩy vào và lấy ra khỏi ngăn xếp tối đa 1 lần, đạt độ phức tạp tuyến tính.

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

Cách tiếp cận Time Space Tên
1 O(N^2) O(1) Brute Force (Tử dãy)
2 O(N log N) O(N) Prefix Sum và Sắp xếp
3 O(N) O(N) Monotonic Stack (Ngăn xếp đơn điệu)

Bài học kinh nghiệm

  • Bài toán có thể chuyển đổi về bài toán tìm hai chỉ số $i, j$ sao cho tổng tiền tố $P[j] \ge P[i]$ và $j-i$ lớn nhất.
  • Phương pháp Monotonic Stack dựa trên ý tưởng: với một giá trị tổng tiền tố $P[i]$ cố định, ta muốn tìm $j$ lớn nhất sao cho $P[j] \ge P[i]$ (với $j > i$).
  • Ngăn xếp đơn điệu giúp loại bỏ các chỉ số không thể tối ưu hóa kết quả, giảm độ phức tạp từ $O(N^2)$ hoặc $O(N \log N)$ xuống $O(N)$.

Lỗi thường gặp

  • Quên xử lý trường hợp không có đoạn con nào thỏa mãn (cần in -1).
  • Lỗi tràn số khi tính tổng, cần sử dụng kiểu dữ liệu long long cho tổng tiền tố.
  • Lầm tưởng giữa chỉ số của tổng tiền tố và chỉ số của phần tử trong mảng gốc (cần chú ý khoảng cách $j - i$).

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.