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

Xây dựng trạm phát sóng

Xây dựng trạm phát sóng
Nguồn: CLUE OJ – Chương 14: Cửa sổ trượt + Tham lam Độ khó: Trung bình Tham lam Tuyển sinh 10 PTNK 2017

Đề bài

Xây dựng trạm phát sóng (Đề tuyển sinh lớp 10 trường Phổ thông Năng khiếu năm 2017 – 2018)

Wonder Land là một thành phố thông minh với nền tảng là các thiết bị công nghệ cao cùng với hệ thống phần mềm ứng dụng luôn kết nối internet cung cấp dịch vụ tiện ích cho mọi cư dân. Wonder Land có $n$ khu dân cư được quy hoạch cách đều nhau một đơn vị độ dài dọc theo một con đường thẳng tắp. Như vậy Wonder Land được biểu diễn bởi trục số và vị trí $n$ khu dân cư là các điểm có tọa độ nguyên liên tiếp từ $0$ đến $n - 1$.

Nhằm nâng cao tốc độ kết nối các dịch vụ, chính quyền lên kế hoạch xây dựng mới các trạm phát sóng di động thế hệ đột phá (Innovation Generation) sao cho mọi khu dân cư đều nhận được sóng. Mỗi trạm phát sóng phủ một vùng bán kính $R$ với tâm là trạm phát sóng và mọi khu dân cư có khoảng cách đến trạm phát sóng không lớn hơn $R$ đều nhận được sóng từ trạm đó. Sau một thời gian khảo sát, chính quyền có được danh sách $m$ vị trí trên trục đường có thể xây dựng trạm, vị trí thứ $i$ có tọa độ $x_i$. Nhà quản lý muốn chọn ra một số vị trí lắp đặt sao cho số trạm cần phải xây dựng là ít nhất và mọi khu dân cư đều nhận được sóng của ít nhất một trạm.

Chẳng hạn $n = 12$, có thể xây dựng trạm tại 8 vị trí với tọa độ là $3, 1, 0, 5, 9, 6, 10, 11$ với bán kính phát sóng $R = 2$ ta chỉ cần xây 3 trạm tại các tọa độ $1, 6, 11$, tương ứng tại các vị trí 2, 6 và 8 thì có thể phủ sóng cho tất cả khu dân cư.

Ba trạm ở tọa độ 1, 6, 11 với bán kính 2 phủ sóng 12 khu dân cư từ 0 đến 11

Yêu cầu: Cho số khu dân cư $n$, dãy $m$ số nguyên $x_1, x_2, \ldots, x_m$ $(0 \le x_i \le n - 1;\ 1 \le m \le n)$ – tọa độ có thể xây dựng trạm phát sóng và số nguyên $R$ – bán kính phát sóng. Hãy tìm phương án chọn ra ít nhất các vị trí cần xây dựng trạm sao cho toàn bộ $n$ khu dân cư đều nhận sóng.

Dữ liệu: (tập tin STATION.INP)

  • Dòng đầu ghi 3 số $n$, $m$ và $R$.
  • Dòng thứ hai ghi $m$ số $x_1, x_2, \ldots, x_m$.

Kết quả: (tập tin STATION.OUT) ghi số $k$ – số trạm cần xây dựng.

Giới hạn: $1 \le n \le 10^6$. Subtask: 30% số test có $N \le 10^2$; 30% số test có $N \le 10^3$; 40% số test còn lại không có điều kiện gì thêm.

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

Ví dụ – input
12 8 2
3 1 0 5 9 6 10 11
Ví dụ – output
3
Lưu ý khi nộp trên CLUE OJ:
  • Kết quả chỉ cần xuất ra 1 số nguyên duy nhất là số trạm ít nhất cần xây dựng; nếu không thể phủ sóng hết toàn bộ thì xuất ra 0. Không cần xuất dòng thứ 2 (danh sách vị trí) như đề gốc – ví dụ mẫu chỉ cần in số 3.
  • Dữ liệu đọc/ghi bằng file STATION.INP / STATION.OUT giống y như đề. Code của mình dùng if (fopen("STATION.INP", "r")) {...} nên có file thì dùng file, không có thì đọc bàn phím.

Lời mở đầu

Mỗi trạm ở tọa độ $x$ phủ một đoạn $[x - R,\ x + R]$ trên trục số. Bài toán trở thành: chọn ít đoạn nhất để phủ kín đoạn $[0, n - 1]$ – một bài phủ đoạn kinh điển giải bằng tham lam. Mình sẽ trình bày cách tham lam "ngây thơ" (mỗi bước duyệt lại cả danh sách trạm), rồi tối ưu bằng sắp xếp + con trỏ chạy một chiều.

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:

TestnmRĐáp ánSố test in 0
1–103 … 103 … 100 … 80 … 91
11–3032 … 10027 … 980 … 510 … 340
31–60218 … 976188 … 7321 … 2022 … 1220
61–802.617 … 32.149587 … 21.0522 … 22549 … 6170
81–90101.892 … 130.3272.041 … 22.67018.959 … 120.1100 … 12
91–991.000.0005.646 … 29.381970.747 … 996.40210
1001.000.0001.000.00001.000.0000
  • Subtask trong đề khớp với bộ test: test 1–30 có $n \le 100$, test 31–60 có $n \le 1000$, test 61–100 lớn hơn.
  • Có 3 test in 0 (test 3, 81, 82): không có cách nào phủ hết, ví dụ $R = 0$ mà không phải khu nào cũng có trạm, hoặc có một khoảng trống quá dài.
  • Test 81–99 có $R$ rất lớn (gần bằng $n$) nhưng các tọa độ trạm chỉ nằm trong khoảng $0 \ldots 32\,767$ – thường chỉ cần 1 trạm.
  • Test 100 là test "nặng" nhất: $n = m = 10^6$, $R = 0$, mọi khu đều có trạm – đáp án là $10^6$ trạm.
  • Trong bộ test các tọa độ $x_i$ đôi một khác nhau, nhưng thứ tự lộn xộn (chưa sắp xếp).

Kết quả chạy thật theo subtask (giới hạn 1 giây):

SubtaskĐề ghiTestCách 1 C++Cách 1 PythonCách 2 C++Cách 2 Python
1 (30%)$N \le 10^2$1–3030/3030/3030/3030/30
2 (30%)$N \le 10^3$31–6030/3030/3030/3030/30
3 (40%)không giới hạn thêm ($N \le 10^6$)61–10039/4038/4040/4039/40

Ý tưởngTham lam: lo cho khu đầu tiên chưa có sóng

Xét từ trái sang phải. Gọi $p$ là khu dân cư đầu tiên chưa có sóng (lúc đầu $p = 0$). Chắc chắn phải có một trạm phủ được $p$, tức là trạm $x$ thoả $x - R \le p \le x + R$. Trong các trạm như vậy, nên chọn trạm nào?

Lựa chọn tham lam
Chọn trạm phủ được $p$ mà nằm xa nhất về bên phải (tọa độ $x$ lớn nhất). Mọi khu bên trái $p$ đã có sóng rồi, nên trạm càng lệch phải thì phần phủ "có ích" $[p,\ x + R]$ càng dài. Sau khi chọn, khu đầu tiên chưa có sóng mới là $p = x + R + 1$. Lặp lại tới khi $p \ge n$. Nếu có lúc không trạm nào phủ được $p$ thì in 0.

Vì sao đúng? Giả sử phương án tối ưu dùng trạm $y$ để phủ $p$. Thay $y$ bằng trạm tham lam $x \ge y$: đoạn $[x - R, x + R]$ vẫn chứa $p$ và kéo xa hơn về bên phải, còn các khu bên trái $p$ đã có sóng sẵn – nên phương án mới vẫn phủ hết và không tốn thêm trạm. Lặp lại lập luận cho từng bước.

Em nhập dữ liệu rồi bấm Tiếp để xem từng bước chọn trạm (ô xanh là khu đã có sóng, ô vàng là khu $p$ đang cần sóng, 📡 là các vị trí có thể đặt trạm):

Ví dụ nhanh:

Cách 1Tham lam, mỗi bước duyệt cả m trạm – $O(k \cdot m)$

Cài đặt thẳng ý tưởng: mỗi lần cần phủ khu $p$, duyệt hết $m$ trạm để tìm trạm phủ được $p$ có tọa độ lớn nhất.

C++▶ Chạy tay từng dòng codeCách 1: mỗi bước duyệt cả m trạm để tìm trạm phủ được p xa nhất – O(k·m).
Dữ liệu mẫu:
  1. Điều kiện x[i] - R <= p && p <= x[i] + R nghĩa là trạm $i$ phủ được khu $p$.
  2. best == -1 sau vòng duyệt nghĩa là không có trạm nào phủ được $p$ → in 0 và kết thúc luôn.
  3. Mỗi trạm được chọn phủ tới $best + R$, nên khu tiếp theo cần lo là $best + R + 1$.
Không gian$O(m)$
Thời gian$O(k \cdot m)$

Với $k$ là số trạm được chọn, mỗi bước tốn $O(m)$. Hầu hết các test có $k$ nhỏ (vài chục, vài trăm) nên rất nhanh; nhưng test 100 có $k = m = 10^6$ → $10^{12}$ phép tính!

Kết quả chạy thật: Bản C++ đúng 99/100 test, quá thời gian ở test 100; test đúng chạy lâu nhất 0.03s. Bản Python đúng 98/100 test, quá thời gian ở test 77, 100; test đúng chạy lâu nhất 0.38s. Cách 1 ăn trọn subtask 1, 2 và gần hết subtask 3.

Cách 2Sắp xếp + hai con trỏ – $O(m \log m)$

Sắp xếp các tọa độ tăng dần. Điều kiện "trạm phủ được $p$, lớn nhất" chính là: trạm cuối cùng có $x \le p + R$ (và phải có $x \ge p - R$). Khi $p$ tăng thì mốc $p + R$ cũng tăng, nên con trỏ $i$ trên mảng đã sắp xếp chỉ việc chạy tiếp về bên phải, không bao giờ quay lại – giống hệt một cửa sổ trượt.

C++▶ Chạy tay từng dòng codeCách 2: sắp xếp rồi cho con trỏ i chạy một chiều – O(m log m).
Dữ liệu mẫu:
  1. Vòng while(i < m && x[i] <= p + R) đi qua mọi trạm có $x - R \le p$; trạm cuối cùng đi qua là trạm lớn nhất, lưu vào best.
  2. Các trạm bị con trỏ bỏ qua (nhỏ hơn best) không bao giờ cần dùng lại: chúng phủ tới tối đa $best + R$, mà các khu đó đã có sóng.
  3. Kiểm tra best + R < p: trạm lớn nhất vẫn ở quá xa bên trái, không với tới $p$ → in 0. Nếu best == -1 thì không có trạm nào mới trong khoảng này (trạm cũ đã dùng hoặc không có).
  4. Tổng số lần i++ trong cả chương trình không quá $m$, nên phần tham lam chỉ $O(m)$; tốn nhất là bước sort. (Vì $x_i < n \le 10^6$ nên cũng có thể đánh dấu bằng mảng thay cho sắp xếp để được $O(n)$.)
Không gian$O(m)$
Thời gian$O(m \log m)$

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 99/100 test, quá thời gian ở test 100; test đúng chạy lâu nhất 0.05s.

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

CáchNgôn ngữĐúngLâu nhấtGhi chú
1. Tham lam, mỗi lần duyệt cả m trạmC++99/100> 1sQuá thời gian test 100
Python98/100> 1sQuá thời gian test 77, 100
2. Sắp xếp + hai con trỏC++100/1000.11sFull điểm
Python99/100> 1sQuá thời gian test 100
Ghi chú của mình: thời gian là thời gian CPU trên máy mình, giới hạn 1 giây. Cách 1 chỉ "gục" đúng ở test 100 (cần $10^6$ trạm), nghĩa là bộ test vẫn cho cách 1 gần trọn điểm – nhưng đó là may mắn; với dữ liệu xấu (nhiều trạm, $R$ nhỏ) chỉ cách 2 mới an toàn. Bản Python của cách 2 đọc nhanh bằng sys.stdin.buffer, nhưng với $10^6$ số thì riêng việc đọc + sắp xếp đã sát giới hạn.
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 😇