Đặt Trạm Phủ Sóng (MOBI – OLP 30/4 Khối 10)
Đề bài
MOBI – Đặt trạm phủ sóng (OLP 30/4 Khối 10)
Giới hạn thời gian: 1.0 giây · Giới hạn bộ nhớ: 128 megabyte
Nhà cung cấp dịch vụ viễn thông Mobi đã khảo sát số lượng người sẽ dùng dịch vụ trên một con đường thẳng mới được xây dựng và đánh dấu lại những vị trí trên con đường này. Đầu con đường được đánh tọa độ bắt đầu từ 0. Tại vị trí có tọa độ $X$ ($X$ nguyên dương) có số lượng người sẽ sử dụng dịch vụ là $Y$. Trước mắt, nhà cung cấp dịch vụ cần đặt một trạm phát sóng có bán kính phủ sóng là $K$ đơn vị chiều dài để phủ sóng cho một số người sử dụng dịch vụ trên con đường này.
Yêu cầu: Bạn hãy xác định vị trí đặt trạm phát sóng (tọa độ nguyên dương) sao cho trạm có thể phục vụ được số lượng người sử dụng nhiều nhất có thể.
Dữ liệu:
- Dòng đầu tiên ghi hai số nguyên $N$ và $K$ $(0 \lt N \le 10^6,\ 0 \lt K \le 2 \cdot 10^6)$, trong đó $N$ là số điểm dân cư đã được đánh dấu, $K$ là bán kính phủ sóng của trạm.
- Trong $N$ dòng tiếp theo, dòng thứ $i$ $(i = 1..N)$ ghi hai số nguyên $X$ và $Y$ cho biết tại vị trí $X$ có số lượng người dùng là $Y$ $(0 \le X \le 10^6,\ 0 \le Y \le 10^4)$. Các số trên cùng dòng viết cách nhau ít nhất một dấu cách.
Kết quả: một số nguyên cho biết số người dùng nhiều nhất sẽ được phục vụ.
4 3
7 4
15 10
2 2
1 511Chọn vị trí đặt trạm phủ sóng tại $X = 4$. Như vậy có thể phủ sóng đến các vị trí có toạ độ $1, 2, 7$. Số lượng người sử dụng lớn nhất là $11$.
Lời mở đầu
Bài này áp dụng một kỹ thuật rất hay gặp có tên tiếng Anh là Sliding Window (tiếng Việt gọi đại khái là "cửa sổ trượt"). Bạn nào chưa biết kỹ thuật này thì cứ lấy từ khoá tiếng Anh đó search Google rồi đọc thêm các bài trên GeeksforGeeks nhé. Còn trong bài này mình áp dụng luôn, đọc xong là các bạn cũng hiểu được phần nào rồi 😉.
Mình giải thích ví dụ mẫu để bạn nào chưa hiểu đề sẽ hiểu rõ. Bán kính $K = 3$, trên đường có 4 điểm dân cư: vị trí $1$ có $5$ người, vị trí $2$ có $2$ người, vị trí $7$ có $4$ người, vị trí $15$ có $10$ người. Nếu đặt trạm ở $X = 4$ thì trạm phủ sóng đoạn từ $4 - 3 = 1$ đến $4 + 3 = 7$, gom được các vị trí $1, 2, 7$ ⇒ $5 + 2 + 4 = 11$ người. Còn nếu ham vị trí $15$ (10 người) mà đặt trạm ở $X = 15$ thì đoạn phủ là $[12, 18]$, chỉ được đúng $10$ người thôi. Không có cách đặt nào được hơn $11$ nên đáp án là $11$.
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:
Nhận xétTrạm phủ sóng một đoạn dài 2K
Các bạn xem nhé, trạm phát sóng đặt ở đâu thì phủ sóng được trong bán kính $K$ đơn vị. Vì mình đang xét trên một con đường thẳng, nên trạm đặt tại điểm $X$ sẽ phủ sóng đoạn đường từ điểm $X - K$ đến điểm $X + K$. Ví dụ $K = 3$, $X = 5$ thì phủ sóng đoạn từ $2$ đến $8$.
Vấn đề là đầu con đường bắt đầu từ $0$. Nếu đặt trạm ở $X = 0$ với $K = 3$ thì "về lý thuyết" nó phủ từ $-3$ đến $3$, nhưng thực tế chỉ là từ $0$ đến $3$ thôi, vì làm gì có các điểm $-3, -2, -1$. Đặt trạm sát đầu đường như vậy thì phí mất một phần sóng, nên ta chỉ cần quan tâm các vị trí có $X - K \ge 0 \Leftrightarrow X \ge K$. Khi đó:
| Vị trí trạm X (K = 3) | Đoạn được phủ sóng |
|---|---|
| 3 | 0 → 6 |
| 4 | 1 → 7 |
| 5 | 2 → 8 |
| 6 | 3 → 9 |
| … | … |
Các bạn có nhìn ra quy luật không? Đoạn đầu tiên bắt đầu từ $0$ và kết thúc ở $2K$. Đoạn tiếp theo thì cả đầu lẫn cuối cùng tăng lên 1 đơn vị: từ $1$ đến $2K + 1$, rồi từ $2$ đến $2K + 2$, và cứ thế… Tất cả các đoạn đều có cùng độ dài $2K + 1$ vị trí.
Cách 1Mảng đánh dấu + cửa sổ trượt – O(10^6)
Trước hết, vì toạ độ $X$ chỉ nằm trong $[0, 10^6]$, mình tạo mảng $a$ có $10^6 + 1$ (một triệu lẻ một) phần tử, chỉ số từ $0$ đến $10^6$ ứng với từng vị trí trên đường. Đọc mỗi điểm dân cư $(x, y)$ thì ghi $y$ vào ô $a[x]$ – đây chính là kỹ thuật mảng đánh dấu.
Tiếp theo, mình tính sum là tổng số người trong đoạn đầu tiên từ $0$ đến $2K$, rồi khởi tạo Max = sum. Sang đoạn tiếp theo từ $1$ đến $2K + 1$, ta không cần chạy vòng lặp cộng lại từ đầu cả đoạn. Bản chất là ta chỉ:
- bỏ đi vị trí $0$ ở đầu trái:
sum -= a[0]; - nhận thêm vị trí $2K + 1$ ở đầu phải:
sum += a[2K + 1].
Thế là có ngay sum của đoạn mới, đem so với Max, lớn hơn thì cập nhật. Tổng quát: đoạn thứ $i$ là $[i, 2K + i]$, cứ trừ đi ô đầu tiên của đoạn trước và cộng thêm 1 ô mới bên phải. Còn $2K + i \le 10^6$ thì còn xét tiếp, $2K + i \gt 10^6$ thì dừng.
Đoạn đang xét giống như một cánh cửa sổ có bề rộng cố định, trượt dần từ trái qua phải: mỗi bước một ô rơi ra bên trái, một ô lọt vào bên phải. Kỹ thuật này gọi là Sliding Window là vậy đó 😄.
start++, finish++). Mỗi bước chỉ tốn $O(1)$ thay vì $O(K)$.Minh hoạ cửa sổ trượt
Em nhập bán kính $K$ và các điểm dân cư dạng x y (cách nhau dấu ;), rồi bấm Tiếp để xem cửa sổ trượt. Để dễ nhìn, con đường được "thu ngắn" lại chỉ tới vị trí xa nhất cần xét (code thật thì chạy tới $10^6$, phần sau toàn số 0 nên không đổi kết quả).
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 1e6;
int a[SIZE + 1];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n, k;
cin >> n >> k;
while(n--){
int x, y;
cin >> x >> y;
a[x] += y;
}
int start = 0;
int finish = min(2 * k, SIZE);
long long Max = 0, sum = 0;
for(int i = start; i <= finish; ++i){
sum += a[i];
}
Max = sum;
start++;
finish++;
while(finish <= SIZE){
sum -= a[start - 1];
sum += a[finish];
Max = max(Max, sum);
start++;
finish++;
}
cout << Max;
return 0;
}
import sys
SIZE = int(1e6)
a = [0] * (SIZE + 1)
data = list(map(int, sys.stdin.read().split()))
ptr = 0
n = data[ptr]
ptr += 1
k = data[ptr]
ptr += 1
for _ in range(n):
x = data[ptr]
ptr += 1
a[x] += data[ptr]
ptr += 1
start = 0
finish = min(2 * k, SIZE)
Max = 0
sum_ = 0
for i in range(start, finish + 1):
sum_ += a[i]
Max = sum_
start += 1
finish += 1
while finish <= SIZE:
sum_ -= a[start - 1]
sum_ += a[finish]
Max = max(Max, sum_)
start += 1
finish += 1
print(Max)
while phải chạy cả triệu lần), nếu bài này cho thời gian giới hạn riêng cho Python (ví dụ 3 giây) thì sẽ AC.Trong khung chạy tay dưới đây, vòng while thật sự chạy tới tận $10^6$, nên khi cửa sổ đã trượt qua hết các điểm dân cư (từ đó về sau chỉ toàn trừ 0 cộng 0), mình gộp phần còn lại thành một bước cho các bạn đỡ phải bấm cả triệu lần 😅.
Giải thích một số chỗ trong code:
- Mảng có $10^6 + 1$ phần tử là khá lớn, khai báo bên trong hàm
maindễ bị tràn bộ nhớ stack, nên mình khai báo mảng làm biến toàn cục (vừa an toàn, vừa được tự khởi tạo toàn số 0). while(n--)là cách viết gọn cho vòng lặp chạy đủ $n$ lần. Biếnxnhận vị trí dân cư, chính là chỉ số của mảnga, nêna[x]chứa số người tại vị trí đó.- Mỗi đoạn ta xét có chỉ số từ
startđếnfinish. Ban đầustart = 0,finishtheo lẽ là $2K$. Nhưng đề cho $K$ tới $2 \cdot 10^6$, nên $2K$ có thể vượt quá chỉ số cuối cùng của mảng là $10^6$. Vì thế mình lấymin(2 * k, SIZE): nếu $2K$ quá lớn thì chỉ cộng hết toàn bộ mảng thôi. - Sau khi có
sumcủa đoạn đầu thì gánMax = sum, rồistart++; finish++;để trượt qua đoạn tiếp theo. Vòngwhilekiểm trafinish <= SIZE, tức cửa sổ vẫn còn nằm trên đường, chưa trượt lố. Trong thân vòng lặp: trừ đia[start - 1](phải $-1$ vìstartđã tăng rồi, $-1$ để trở về ôstartcũ) và cộng thêma[finish](ô mới ngay sau ôfinishcũ). Cósummới thì so vớiMaxvà cập nhật. Khi vòngwhilekết thúc thì inMax.
- Code gốc của mình trên Facebook đọc thẳng
cin >> a[x]. Đề không nói rõ các toạ độ $X$ có phân biệt hay không, nên mình đổi thànha[x] += y: lỡ có 2 dòng cùng toạ độ thì cộng dồn cả hai, không bị dòng sau ghi đè dòng trước. Nếu toạ độ phân biệt thì hai cách như nhau. - Đề gốc thi offline đọc/ghi file
MOBI.INP/MOBI.OUT(dùngfreopen). Trên hệ thống chấm online thì đọc/ghi chuẩn (bàn phím/màn hình) nên code ở trên mình bỏfreopenđi. Đi thi offline thì các bạn nhớ thêm lại nhé.
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(V)$ với $V = 10^6$ là toạ độ lớn nhất theo đề, vì mình tạo mảng $10^6 + 1$ phần tử.
- Thời gian: đọc $N$ điểm mất $O(N)$, cộng đoạn đầu và trượt cửa sổ thì mỗi ô của mảng được cộng vào đúng 1 lần, trừ ra tối đa 1 lần ⇒ $O(V)$. Tổng cộng $O(N + V)$, tức cỡ $O(10^6)$ trong bài này. (Ở đây
^là ký hiệu lũy thừa nhé: $10^6$ là một triệu, chứ không phải phép xor bit đâu nha các bạn 😁.)
Với độ phức tạp này thì yên tâm không bị TLE: giới hạn 1 giây chạy được khoảng $(3 \to 5) \cdot 10^7$, cao lắm là $10^8$ phép tính, mà $10^6$ còn ít hơn ngưỡng đó cả 50 đến 100 lần.
int (khoảng $2.1 \cdot 10^9$). Vì vậy sum và Max phải là long long.- 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