Hướng dẫn giải của Buổi hòa nhạc


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, nquang2909

Editorial for concert: Buổi hòa nhạc

Hiểu bài toán

Bài toán yêu cầu đếm số cặp người trong hàng đợi có thể nhìn thấy nhau. Hai người A và B có thể nhìn thấy nhau nếu thỏa mãn một trong hai điều kiện:

  1. Họ đứng ở các vị trí liên tiếp nhau (ngay cạnh).
  2. Không có người nào cao hơn cả A và B đứng giữa họ. Điều này có nghĩa là nếu xét trên đoạn từ A đến B, tất cả các người ở giữa đều có độ cao <= min(height(A), height(B)). Ví dụ: Với dãy [2, 4, 1, 2, 2, 5, 1], các cặp nhìn thấy nhau bao gồm các cặp kề nhau và các cặp 'nhìn xuyên' qua các người thấp hơn. Tuy nhiên, theo các phương án giải, điều kiện 'không có người nào cao hơn A hoặc B' thường được hiểu là tất cả người ở giữa phải thấp hơn hoặc bằng một trong hai người đó (thường là người cao hơn hoặc người bên trái). Trong các giải pháp mẫu, tác giả sử dụng栈 (Stack) để đếm các cặp mà không có người cao hơn ở giữa, bao gồm cả các cặp kề nhau.

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

Cách Duyệt kép (Two Passes)
#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    long long h[n];
    for (int i = 0; i < n; i++) scanf("%lld", &h[i]);

    long long ans = 0;
    // Pass 1: Duyệt từ trái sang phải, đếm các cặp (i, j) sao cho j > i và không có người nào > h[j] ở giữa
    // Thực chất đây là đếm số người bên trái nhỏ hơn hoặc bằng hiện tại mà không bị chặn
    // Pass 2: Duyệt từ phải sang trái
    // Tuy nhiên, giải pháp Stack ở trên làm một lượt duy nhất.

    // Dưới đây là cách tiếp cận Stack tối ưu đã cho:
    long long stackH[n], stackC[n];
    int top = -1;

    for (int i = 0; i < n; i++) {
        long long cnt = 1;
        while (top >= 0 && stackH[top] <= h[i]) {
            ans += stackC[top];
            if (stackH[top] == h[i]) {
                cnt += stackC[top];
            }
            top--;
        }
        if (top >= 0) ans++;
        stackH[++top] = h[i];
        stackC[top] = cnt;
    }

    printf("%lld\n", ans);
    return 0;
}
  • Time Complexity: O(n)
  • Space Complexity: O(n)

Đây là cách tiếp cận tối ưu sử dụng cấu trúc dữ liệu Stack. Ý tưởng là duyệt qua từng người và sử dụng stack để lưu trữ những người có thể nhìn thấy người hiện tại từ bên trái.

  • Stack lưu trữ các cặp (độ cao, số lượng người có độ cao đó).
  • Khi gặp người mới h[i], ta loại bỏ các người trong stack có độ cao <= h[i]. Với mỗi người bị loại bỏ, ta đều có thể nhìn thấy họ (vì h[i] cao hơn hoặc bằng họ, nên không có vật cản).
  • Nếu stack không rỗng sau khi loại bỏ, nghĩa là còn người cao hơn h[i], thì h[i] nhìn thấy người đó.
  • Biến cnt dùng để đếm số người có độ cao bằng h[i] liên tiếp trước đó mà ta có thể 'nhìn xuyên' qua để tính vào các cặp tiếp theo. Cách này xử lý đúng các điều kiện: cặp kề nhau (do stack rỗng hoặc có người cao hơn ở bước đầu), và các cặp xa mà không có người cao hơn ở giữa.
Cách Brute Force (Tối ưu hóa một chút)
#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    int h[100005];
    for(int i=0; i<n; i++) scanf("%d", &h[i]);

    long long ans = 0;
    for(int i=0; i<n; i++) {
        // Đếm các cặp (i, j) với j > i
        for(int j=i+1; j<n; j++) {
            // Kiểm tra xem có người nào cao hơn min(h[i], h[j]) ở giữa không
            // Thực ra điều kiện bài này là không cao hơn HAI NGƯỜI, hiểu đơn giản là mọi người ở giữa <= max(h[i], h[j])
            // Nhưng theo sample, nó là đếm các đoạn tăng/giảm đơn điệu.
            // Tuy nhiên, Brute force đúng chuẩn là:
            int max_mid = 0;
            for(int k=i+1; k<j; k++) {
                if(h[k] > max_mid) max_mid = h[k];
            }
            // Điều kiện nhìn thấy: max_mid <= h[i] hoặc max_mid <= h[j] ?
            // Không, điều kiện là "không có người nào cao hơn A hoặc B". 
            // Tức là tất cả người ở giữa phải thấp hơn hoặc bằng A HOẶC thấp hơn hoặc bằng B.
            // Nếu A < B, thì người ở giữa phải <= A. (Nếu có người > A, nó có thể > B hoặc <= B).
            // Nếu có người > A và <= B, thì người đó vẫn thấp hơn B nên B vẫn thấy A.
            // Nếu có người > B, thì chắn chắn không thấy.
            // Vậy điều kiện là: max(h[i+1]...h[j-1]) <= max(h[i], h[j]).
            // Nhưng sample 1: [2, 4, 1, 2, 2, 5, 1] -> 10 pairs.
            // Pair (B=4, F=5): max_mid (1,2,2) = 2 <= 5. Thấy.
            // Pair (A=2, F=5): max_mid (4,1,2,2) = 4. 4 <= 5? Co. Thấy.
            // Pair (B=4, D=2): max_mid (1) = 1 <= 4. Thấy.
            // Pair (A=2, D=2): max_mid (4,1) = 4. 4 <= 2? Khong. Khong thay.
            // Wait, trong list pairs (A,B), (B,C)... (A là 2, B là 4, C là 1, D là 2...)
            // Pair (A, D): 2 ... 4 ... 1 ... 2. Max mid = 4. 4 > 2 va 4 > 2. Khong thay.
            // Conclusion: Dieu kien la max(mid) <= max(h[i], h[j]).

            // Brute force code:
            int m = 0;
            for(int k=i+1; k<j; k++) if(h[k] > m) m = h[k];
            if(m <= (h[i] > h[j] ? h[i] : h[j])) ans++;
        }
    }
    // Cong them cap ke nhau (vi loop tren khong tinh khe nhau)
    ans += (n-1);
    printf("%lld", ans);
    return 0;
}
  • Time Complexity: O(n^3) hoặc O(n^2) tùy chỉnh
  • Space Complexity: O(n)

Cách tiếp cận này cồng kềnh và chậm. Nó kiểm tra từng cặp (i, j) và duyệt qua các phần tử ở giữa để tìm max. Nếu max này nhỏ hơn hoặc bằng độ cao của một trong hai người (người cao hơn), thì họ nhìn thấy nhau. Cách này không khả thi với N=10^5 do giới hạn thời gian.

Cách Duyệt từ trái sang phải (Single Pass Stack)
#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    long long h[n];
    for (int i = 0; i < n; i++) scanf("%lld", &h[i]);

    long long ans = 0;
    long long stackH[n], stackC[n];
    int top = -1;

    for (int i = 0; i < n; i++) {
        long long cnt = 1;
        // Loại bỏ các người thấp hơn hoặc bằng người hiện tại
        while (top >= 0 && stackH[top] <= h[i]) {
            ans += stackC[top]; // Người hiện tại thấy người này
            if (stackH[top] == h[i]) {
                cnt += stackC[top]; // Cộng dồn số lượng người bằng nhau
            }
            top--;
        }
        // Nếu vẫn còn người trong stack (cao hơn người hiện tại),
        // thì người hiện tại nhìn thấy người đó.
        if (top >= 0) ans++;

        // Đẩy người hiện tại vào stack
        stackH[++top] = h[i];
        stackC[top] = cnt;
    }

    printf("%lld\n", ans);
    return 0;
}
  • Time Complexity: O(n)
  • Space Complexity: O(n)

Đây là biến thể của phương pháp Stack, được trình bày chi tiết trong Solution 1 và 2.

  • Duyệt từ trái sang phải.
  • Stack lưu trữ các người có thể là 'mắt xích' cho các cặp tiếp theo.
  • Vòng lặp while xử lý các người thấp hơn hoặc bằng người hiện tại, tính chúng vào kết quả và gộp nếu bằng nhau.
  • Câu lệnh if (top >= 0) ans++ xử lý việc người hiện tại nhìn thấy người cao hơn ngay trước mặt (người cuối cùng còn lại trong stack).
  • Độ phức tạp O(n) vì mỗi phần tử chỉ được đẩy vào và lấy ra khỏi stack đúng một lần.

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

Cách tiếp cận Time Space Tên
1 O(n) O(n) Duyệt kép (Two Passes)
2 O(n^3) hoặc O(n^2) tùy chỉnh O(n) Brute Force (Tối ưu hóa một chút)
3 O(n) O(n) Duyệt từ trái sang phải (Single Pass Stack)

Bài học kinh nghiệm

  • Bài toán có thể giảm về bài toán đếm số cặp (i, j) sao cho max(h[i+1]...h[j-1]) <= max(h[i], h[j]), hoặc tương đương với việc đếm số đường đi trong đồ thị mà không bị chặn bởi người cao hơn.
  • Sử dụng Monotonic Stack (Stack đơn điệu) là chìa khóa để giải quyết các bài toán liên quan đến 'người nhìn thấy nhau' trong một hàng với độ phức tạp O(n).
  • Với các phần tử có độ cao bằng nhau, cần xử lý đặc biệt bằng cách đếm số lượng (frequency) để tính toán đúng số cặp bị che khuất hoặc nhìn thấy.

Lỗi thường gặp

  • Quên tính các cặp kề nhau (nếu thuật toán không xử lý tự động).
  • Xử lý sai các trường hợp độ cao bằng nhau dẫn đến đếm thiếu hoặc thừa.
  • Lỗi tràn số nguyên (overflow) nếu dùng int cho biến kết quả ans thay vì long long.

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.