Dãy số 7
Đề bài
Dãy số
Cho dãy số nguyên $a_1, a_2, \ldots, a_n$ $(1 \le n \le 2 \cdot 10^6)$, phạm vi mỗi phần tử có giá trị tuyệt đối không quá $10^4$. Dãy số này được viết trên một vòng tròn, nghĩa là khi cắt dãy số đã cho tại vị trí $j$ bất kì ta thu được dãy số như sau: $a_j, a_{j+1},$ $\ldots,$ $a_n,$ $a_1,$ $a_2,$ $\ldots,$ $a_{j-1}$.
Vị trí $j$ được gọi là vị trí tốt nếu các điều kiện dưới đây đều được thỏa mãn:
- $a_j > 0$
- $a_j + a_{j+1} > 0$
- $\ldots$
- $a_j + a_{j+1} + \cdots + a_n > 0$
- $a_j + a_{j+1} + \cdots + a_n + a_1 > 0$
- $\ldots$
- $a_j + a_{j+1} + \cdots$ $+ a_n + a_1 + a_2$ $+ \cdots + a_{j-2} > 0$
- $a_j + a_{j+1} + \cdots$ $+ a_n + a_1 + a_2$ $+ \cdots + a_{j-2}$ $+ a_{j-1} > 0$
Yêu cầu: Hãy đếm số lượng vị trí tốt có trong dãy số nguyên $a_1, a_2, \ldots, a_n$.
Dữ liệu vào:
- Dòng đầu tiên chứa số nguyên $n$;
- Dòng thứ hai chứa dãy số nguyên $a_1, a_2, \ldots, a_n$.
Dữ liệu ra: một số nguyên duy nhất là số lượng vị trí tốt tìm được.
Subtask:
- 50% điểm có $n \le 10^4$.
- 30% điểm tiếp theo có $n \le 10^5$.
- 20% đ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.
5
0 1 -2 10 327
1 -2 0 5 -4 1 -20Lời mở đầu
Nói gọn lại: cắt vòng tròn tại $j$, đi một vòng và cộng dần – mọi tổng cộng dồn (gồm $n$ tổng) đều phải dương. Ở ví dụ 1 với dãy $0, 1, -2, 10, 3$: cắt tại $j = 4$ ta được $10, 3, 0, 1, -2$ với các tổng cộng dồn $10, 13, 13, 14, 12$ đều dương → tốt. Cắt tại $j = 5$: $3, 0, 1, -2, 10$ → $3, 3, 4, 2, 12$ → tốt. Các vị trí còn lại đều có một tổng $\le 0$ (ví dụ $j = 1$ có ngay $a_1 = 0$). Vậy đáp án là 2.
Để ý điều kiện cuối cùng chính là tổng cả dãy phải dương. Ở ví dụ 2, tổng cả dãy là $-1$ nên không vị trí nào tốt, đáp án 0.
Bài có 3 subtask rất "chuẩn" cho 3 mức thuật toán: $O(n^2)$, $O(n \log n)$ và $O(n)$. Mình sẽ đi lần lượt cả 3 và chạy thật trên bộ test.
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJLộ 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:
| Test | n | Giá trị $a_i$ | |Tổng| lớn nhất | Số test đáp án 0 | Tỉ lệ vị trí tốt |
|---|---|---|---|---|---|
| 1–30 | 5 … 20 | -30 … 30 | 120 | 12/30 | 17% |
| 31–50 | 1.461 … 8.846 | -9.999 … 9.999 | 377.414 | 14/20 | 0% |
| 51–80 | 100.000 | -9.998 … 9.999 | 153.179.473 | 0/30 | 99% |
| 81–90 | 500.000 | -9.976 … 9.999 | 763.992.284 | 0/10 | 100% |
| 91–95 | 1.000.000 | -998.338.343 … 999.999.485 | 67.787.053.902.242 | 0/5 | 100% |
| 96–100 | 2.000.000 | -988.956.816 … 999.999.459 | 135.824.833.271.121 | 0/5 | 100% |
int cho tổng (với $|a_i| \le 10^4$, $n \le 2 \cdot 10^6$ thì tổng chỉ tới $2 \cdot 10^{10}$ – vốn đã vượt int rồi!) thì chắc chắn sai. Mọi tổng trong bài này mình đều dùng long long.Ngoài ra: ở các test nhỏ (1–50), khoảng một nửa số test có tổng cả dãy $\le 0$ nên đáp án bằng 0; còn từ test 51 trở đi gần như mọi vị trí đều tốt – đây là ác mộng của cách thử từng vị trí vì nó không thể dừng sớm.
Subtask (theo $n$) khớp với bộ test. Số test đúng của từng cách:
| Subtask | Đề ghi | Test | Cách 1 (thử từng j) | Cách 2 (multiset) | Cách 3 (min tiền tố/hậu tố) |
|---|---|---|---|---|---|
| 1 (50%) | $n \le 10^4$ | 1–50 | 50/50 | 50/50 | 50/50 |
| 2 (30%) | $n \le 10^5$ | 51–80 | 0/30 | 30/30 | 30/30 |
| 3 (20%) | $n \le 2 \cdot 10^6$ | 81–100 | 0/20 | 15/20 | 20/20 |
Ý tưởngDuỗi vòng tròn thành dãy gấp đôi
Mẹo quen thuộc với vòng tròn: viết dãy hai lần liên tiếp $a_1, \ldots, a_n, a_1, \ldots, a_n$. Khi đó cắt tại $j$ chính là lấy đoạn $j, j+1, \ldots, j+n-1$ của dãy gấp đôi. Gọi $P_k$ là tổng tiền tố của dãy gấp đôi ($P_0 = 0$). Tổng cộng dồn từ $j$ tới $k$ là $P_k - P_{j-1}$, nên:
Nhìn bằng hình: vẽ các điểm $P_0, P_1, \ldots, P_{2n-1}$. Vị trí $j$ tốt khi cả "cửa sổ" $n$ điểm $P_j..P_{j+n-1}$ đều nằm hẳn phía trên đường ngang vẽ ở độ cao $P_{j-1}$. Bài toán trở thành: với mỗi cửa sổ độ dài $n$ trượt dần sang phải, tìm giá trị nhỏ nhất của cửa sổ.
Em nhập dãy rồi bấm Tiếp để xem từng vị trí cắt $j$ (điểm xanh: trên đường ngang, điểm đỏ: không trên đường ngang):
Có hai cách tìm min của cửa sổ trượt:
- Dùng
multisetchứa $n$ giá trị trong cửa sổ: trượt sang phải thì xoá $P_j$, thêm $P_{j+n}$, min là phần tử đầu – $O(n \log n)$ (cách 2). - Tách cửa sổ thành 2 phần: $P_j..P_n$ (min hậu tố của mảng tiền tố $S$ gốc) và $P_{n+1}..P_{n+j-1}$ $= S_n + S_1,$ $\ldots,$ $S_n + S_{j-1}$ (bằng $S_n$ cộng min tiền tố) – $O(n)$ (cách 3).
Cách 1Thử từng vị trí cắt – Subtask 1
Làm đúng như đề: với mỗi $j$, đi một vòng, cộng dồn và dừng ngay khi gặp tổng $\le 0$.
- Vị trí thứ $k$ (đếm từ 0) tính từ $j$ trên vòng tròn là
(j - 1 + k) % n + 1: chuyển về chỉ số từ 0, cộng $k$, lấy dư cho $n$ rồi đổi lại về chỉ số từ 1. - Gặp tổng $\le 0$ thì
breakluôn – nếu dữ liệu có nhiều vị trí xấu thì chạy khá nhanh. Nhưng ở test 51–100 gần như vị trí nào cũng tốt, vòng trong luôn chạy đủ $n$ bước.
Kết quả chạy thật: Bản C++ đúng 50/100 test, quá thời gian ở test 51–100; test đúng chạy lâu nhất 0.01s. Bản Python đúng 50/60 test (mình chỉ chạy test 1–60), quá thời gian ở test 51–60; test đúng chạy lâu nhất 0.37s. Bản C++ ăn trọn subtask 1 (50% điểm).
Cách 2Cửa sổ trượt với multiset – Subtask 1, 2
Cài đặt trực tiếp ý tưởng dãy gấp đôi: multiset giữ đúng $n$ giá trị $P_j, \ldots, P_{j+n-1}$ (dùng multiset vì các tổng tiền tố có thể trùng nhau).
*ms.begin()là phần tử nhỏ nhất củamultiset.ms.erase(ms.find(P[j]))chỉ xoá một bản sao của $P_j$. Nếu viếtms.erase(P[j])thì sẽ xoá tất cả các phần tử bằng $P_j$ – lỗi rất hay gặp!- Mảng
Pdài $2n$ nên khai báo $4 \cdot 10^6 + 5$ phần tử. - Python không có
multiset, mình dùngheapqkèm "xoá lười": phần tử nào có chỉ số đã ra khỏi cửa sổ thì khi nó nổi lên đỉnh heap mới bỏ đi.
Kết quả chạy thật: Bản C++ đúng 95/100 test, quá thời gian ở test 96–100; test đúng chạy lâu nhất 0.66s. Bản Python đúng 80/100 test, quá thời gian ở test 81–100; test đúng chạy lâu nhất 0.31s. Với $n = 2 \cdot 10^6$ thì cần khoảng $4 \cdot 10^6$ thao tác trên multiset, mỗi thao tác cỡ $\log_2(2 \cdot 10^6) \approx 21$ bước so sánh và rất nhiều lần cấp phát bộ nhớ – không kịp 1 giây.
Cách 3Min tiền tố và min hậu tố – Full điểm
Gọi $S_i = a_1 + \cdots + a_i$ là tổng tiền tố của dãy gốc. Các tổng cộng dồn khi cắt tại $j$ chia làm 2 nhóm:
- Đi từ $j$ tới $n$: tổng là $S_k - S_{j-1}$ với $k = j..n$. Tất cả dương $\iff$ $\text{suf}_j$ $= \min(S_j, \ldots, S_n) > S_{j-1}$.
- Vòng qua đầu dãy, đi từ $1$ tới $j-1$: tổng là $S_n - S_{j-1} + S_k$ với $k = 1..j-1$. Tất cả dương $\iff$ $S_n + \text{pre}_{j-1} > S_{j-1}$ với $\text{pre}_{j-1} = \min(S_1, \ldots, S_{j-1})$. (Khi $j = 1$ nhóm này rỗng.)
Mảng pre và suf tính trước bằng 2 vòng for, sau đó mỗi $j$ kiểm tra trong $O(1)$.
pre[0] = LLONG_MAXvàsuf[n + 1] = LLONG_MAXlà "lính canh" để công thứcminchạy đúng ở 2 đầu.- Điều kiện
(j == 1) ||đặt trước để khi $j = 1$ không cần dùngpre[0](cộngLLONG_MAXsẽ tràn số). - Bản Python dùng
itertools.accumulateđể tính tổng tiền tố và min tiền tố/hậu tố cho nhanh.
Kết quả chạy thật: Bản C++ đúng 100/100 test; test đúng chạy lâu nhất 0.18s. Bản Python đúng 95/100 test, quá thời gian ở test 96–100; test đúng chạy lâu nhất 0.97s.
Tổng kếtKết quả chạy thật trên bộ test
| Cách | Ngôn ngữ | Đúng | Lâu nhất | Ghi chú |
|---|---|---|---|---|
| 1. Thử từng vị trí $O(n^2)$ | C++ | 50/100 | > 1s | Qua subtask 1; quá thời gian test 51–100 |
| Python | 50/60 | > 1s | Mình chỉ chạy test 1–60; quá thời gian test 51–60 | |
2. Cửa sổ trượt + multiset | C++ | 95/100 | > 1s | Quá thời gian test 96–100 (n = 2·10^6) |
| Python | 80/100 | > 1s | heap + xoá lười; quá thời gian test 81–100 | |
| 3. Min tiền tố / hậu tố $O(n)$ | C++ | 100/100 | 0.18s | Full điểm |
| Python | 95/100 | > 1s | Quá thời gian test 96–100 |
multiset là công cụ rất tiện để lấy min/max của cửa sổ trượt, nhưng với $n$ cỡ vài triệu thì hằng số của nó lớn; khi cửa sổ có cấu trúc đặc biệt (như ở đây: luôn dài đúng $n$ trên dãy gấp đôi) thì min tiền tố/hậu tố cho lời giải $O(n)$ gọn và nhanh hơn hẳn.- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét