Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Lượng nước mưa giữ lại

Lượng nước mưa giữ lại
Nguồn: CLUE OJ – Chương 15: Ngăn xếp, Hàng đợi Độ khó: Khá Ngăn xếp đơn điệu Max tiền tố / hậu tố

Đề bài

Lượng Nước Mưa Giữ Lại

Cho $n$ $(n \le 10^6)$ các số nguyên $h[i]$ $(0 \le h[i] \le 10^9)$ biểu diễn bản đồ độ cao, trong đó chiều rộng của mỗi thanh là 1, hãy tính lượng nước mà các thanh có thể giữ lại sau khi mưa.

Dữ liệu vào: dòng đầu là $n$, dòng sau là $n$ số $h[1], h[2], \ldots, h[n]$.

Kết quả: một số nguyên là lượng nước giữ lại.

Hình minh hoạ ví dụ 1: cột đen là các thanh, phần xanh là nước mưa giữ lại

Hình vẽ minh hoạ ví dụ 1: màu đen là các cột, màu xanh là nước mưa được giữ lại (6 ô).

Ràng buộc:

  • 30% số điểm đầu tiên có $h[i] \le 10^3$ và $n \le 10^2$.
  • 30% số điểm tiếp theo có $h[i] \le 10^3$ và $n \le 10^4$.
  • 40% điểm còn lại không có giới hạn gì thêm.

Giới hạn thời gian: 1 giây, bộ nhớ: 256 MB.

Ví dụ 1 – input
12
0 1 0 2 1 0 1 3 2 1 2 1
Ví dụ 1 – output
6
Ví dụ 2 – input
6
4 2 0 3 2 5
Ví dụ 2 – output
9

Lời mở đầu

Đây là bài "Trapping Rain Water" rất nổi tiếng, xuất hiện trong hầu hết các bộ đề phỏng vấn lẫn các khoá học cấu trúc dữ liệu. Có nhiều cách nhìn bài này: nhìn theo từng cột (mỗi cột chứa bao nhiêu nước) dẫn tới lời giải max tiền tố – hậu tố, còn nhìn theo từng lớp nằm ngang dẫn tới lời giải bằng ngăn xếp đơn điệu – đúng chủ đề của chương. Mình sẽ trình bày cả hai, kèm kết quả chạy thật trên bộ test.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Bộ testBộ test chính thức có gì?

Bộ test có 100 test:

TestnĐộ cao $h_i$Đáp án
1–205 … 120 … 200 … 54
21–301000 … 1.00051.406 … 61.015
31–6010.0000 … 1.0005.882.465 … 6.023.758
61–80100.0000 … 1.000.000.00069.542.461.836.922 … 70.119.508.878.082
81–1001.000.0000 … 1.000.000.000698.293.695.275.454 … 700.052.719.415.156
  • Subtask khớp đúng với bộ test: test 1–30 có $n \le 100$, $h \le 1000$; test 31–60 có $n = 10^4$, $h \le 1000$; test 61–100 có $n = 10^5$ (test 61–80) và $n = 10^6$ (test 81–100) với $h$ tới $10^9$.
  • Đáp án lớn nhất cỡ $7 \cdot 10^{14}$ – vượt xa int. Ngay cả min(...) - h[i] cũng nên tính bằng long long cho chắc, nên mình khai báo luôn mảng $h$ kiểu long long.

Số test đúng của từng cách theo subtask (chạy thật, giới hạn 1 giây):

SubtaskĐề ghiTestCách 1
$O(n^2)$
Cách 2
(max 2 phía)
Cách 3
(ngăn xếp)
1 (30%)$h_i \le 10^3$, $n \le 10^2$1–3030/3030/3030/30
2 (30%)$h_i \le 10^3$, $n \le 10^4$31–6030/3030/3030/30
3 (40%)$n \le 10^6$, $h_i \le 10^9$61–1000/4040/4040/40

Ý tưởngNhìn theo cột và nhìn theo lớp

Nhìn theo cột
Nước đọng trên cột $i$ dâng lên tới mức $\min(\text{cột cao nhất bên trái}, \text{cột cao nhất bên phải})$ (tính cả chính cột $i$), vì nước tràn ra phía tường thấp hơn. Vậy cột $i$ giữ được $$w_i = \min\left(\max_{j \le i} h_j,\ \max_{j \ge i} h_j\right) - h_i.$$ Đáp án là $\sum w_i$.

Ở ví dụ 2 ($4, 2, 0, 3, 2, 5$): cột thứ 3 (cao 0) có max trái 4, max phải 5 → nước lên tới 4, giữ $4 - 0 = 4$. Cộng lại $0 + 2 + 4 + 1 + 2 + 0 = 9$.

Nhìn theo lớp – ngăn xếp
Đi từ trái sang phải, giữ ngăn xếp các cột có độ cao giảm dần (các "bậc thang đi xuống" chưa có tường phải). Khi gặp cột $i$ cao hơn đỉnh ngăn xếp, cột đỉnh trở thành đáy của một vũng: lấy nó ra, cột kế dưới trong ngăn xếp là tường trái $t$, cột $i$ là tường phải. Vũng này đổ thêm được một lớp nước hình chữ nhật: $$\text{cao} = \min(h_t, h_i) - h_{\text{đáy}}, \quad \text{rộng} = i - t - 1.$$ Lặp lại tới khi đỉnh không còn thấp hơn $h_i$, rồi đẩy $i$ vào ngăn xếp.

Em nhập dãy độ cao rồi bấm Tiếp để xem ngăn xếp "đổ nước" từng lớp. Cột vàng là cột $i$ đang xét, cột đỏ là đáy vừa lấy ra, cột xanh dương đậm là tường trái; lớp nước cam là lớp vừa đổ thêm:

Ví dụ nhanh:

Cách 1Mỗi cột tìm max hai phía – Subtask 1, 2

Làm thẳng theo công thức "nhìn theo cột": với mỗi cột $i$, chạy một vòng sang trái tìm trai, một vòng sang phải tìm phai.

C++▶ Chạy tay từng dòng codeCách 1: với mỗi cột chạy 2 vòng tìm cột cao nhất bên trái, bên phải – O(n²).
Dữ liệu mẫu:
Ví dụ 1 có n = 12 nên khung này giới hạn n ≤ 8 (cách này rất nhiều bước); em xem ví dụ 1 ở khung cách 2 và cách 3 nhé.
  1. Cả hai vòng đều tính luôn chính cột $i$, nên min(trai, phai) >= h[i] – không bao giờ cộng số âm.
  2. tong là long long; với subtask 1, 2 đáp án nhỏ nhưng subtask 3 cần tới $7 \cdot 10^{14}$.
Không gian$O(n)$
Thời gian$O(n^2)$

Mỗi cột tốn $O(n)$ nên tổng $O(n^2)$: $n = 10^4$ là $10^8$ phép so sánh đơn giản – C++ vẫn kịp, nhưng $n = 10^5$ đã là $10^{10}$.

Kết quả chạy thật: Bản C++ đúng 60/100 test, quá thời gian ở test 61–100; test đúng chạy lâu nhất 0.08s. Bản Python đúng 30/60 test (mình chỉ chạy test 1–60), quá thời gian ở test 31–60; test đúng chạy lâu nhất 0.02s.

Gợi ý của đề "$h \le 10^3$" ở subtask 1, 2 còn cho phép một cách khác: xét từng mức nước $k = 1, 2, \ldots, \max h$ và đếm số ô ở độ cao $k$ nằm giữa hai cột cao $\ge k$ – độ phức tạp $O(n \cdot \max h)$, cũng chỉ ăn được 2 subtask đầu.

Cách 2Max tiền tố và max hậu tố – Full điểm

Hai vòng tìm max ở cách 1 lặp đi lặp lại rất nhiều. Ta tính trước một lần:

  • $L[i] = \max(h_1, \ldots, h_i)$ $= \max(L[i-1], h_i)$ – max tiền tố, đi từ trái sang.
  • $R[i] = \max(h_i, \ldots, h_n)$ $= \max(R[i+1], h_i)$ – max hậu tố, đi từ phải sang.

Khi đó $w_i = \min(L[i], R[i]) - h_i$, cộng lại trong $O(n)$.

C++▶ Chạy tay từng dòng codeCách 2: tính trước max tiền tố L và max hậu tố R – O(n).
Dữ liệu mẫu:
Không gian$O(n)$
Thời gian$O(n)$

Kết quả chạy thật: Bản C++ đúng 100/100 test; test đúng chạy lâu nhất 0.08s. Bản Python đúng 100/100 test; test đúng chạy lâu nhất 0.94s.

Mở rộng: có thể bỏ luôn 2 mảng phụ bằng hai con trỏ $l = 1, r = n$ chạy vào giữa: phía nào có max hiện tại nhỏ hơn thì phía đó đã biết chắc mực nước (vì phía kia chắc chắn có tường cao hơn), cộng nước rồi dịch con trỏ phía đó vào. Bộ nhớ $O(1)$, thời gian vẫn $O(n)$.

Cách 3Ngăn xếp đơn điệu – Full điểm

Đây là cài đặt của cách "nhìn theo lớp". Ngăn xếp lưu chỉ số cột (cần chỉ số để tính chiều rộng).

C++▶ Chạy tay từng dòng codeCách 3: ngăn xếp chỉ số cột giảm dần, đổ nước từng lớp – O(n).
Dữ liệu mẫu:
  1. Điều kiện h[st.top()] < h[i] dùng dấu nhỏ hơn nghiêm ngặt: cột bằng nhau vẫn nằm lại trong ngăn xếp; khi sau này nó làm đáy với tường trái cao bằng nó thì cao = 0, cộng thêm 0 – không sai.
  2. Nếu lấy đáy ra mà ngăn xếp rỗng thì không có tường trái: nước chảy ra ngoài bên trái, break. Lúc này ngăn xếp đã rỗng nên vòng while cũng sẽ dừng.
  3. Chiều rộng i - t - 1 chứ không phải 1: các cột giữa $t$ và đáy đã bị lấy ra trước đó, phía trên chúng đã có nước tới độ cao $h_{\text{đáy}}$, giờ ta đổ thêm một lớp phủ toàn bộ khoảng giữa hai tường.
  4. Mỗi chỉ số vào ngăn xếp 1 lần, ra tối đa 1 lần → $O(n)$.
Không gian$O(n)$
Thời gian$O(n)$

Kết quả chạy thật: Bản C++ đúng 100/100 test; test đúng chạy lâu nhất 0.11s. Bản Python đúng 100/100 test; test đúng chạy lâu nhất 0.92s.

Tổng kếtKết quả chạy thật trên bộ test

CáchNgôn ngữĐúngLâu nhấtGhi chú
1. Mỗi cột tìm max 2 phía $O(n^2)$C++60/100> 1squá thời gian test 61–100
Python30/60> 1sMình chỉ chạy test 1–60; quá thời gian test 31–60
2. Max tiền tố / hậu tốC++100/1000.08sfull điểm
Python100/1000.94sfull điểm
3. Ngăn xếp đơn điệuC++100/1000.11sfull điểm
Python100/1000.92sfull điểm
Ghi chú của mình: thời gian là thời gian CPU đo trên máy của mình, giới hạn 1 giây như đề. Hai cách $O(n)$ đều qua trọn bộ test: cách 2 chạy test lớn nhất khoảng 0.08 giây, cách 3 khoảng 0.11 giây (phần lớn thời gian là đọc $10^6$ số). Với $n = 10^6$, bản Python (đọc bằng sys.stdin.buffer) vẫn qua cả 100 test nhưng sát nút – khoảng 0.9 giây – nên nộp lên máy chấm có thể lúc qua lúc không; C++ thì dư sức.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Nhận xét