Xóa Phần Tử Trong Ma Trận (TABLEDEL)
Đề bài
Cho một bảng ô vuông gồm $n$ dòng và $m$ cột. Các dòng được đánh số từ $1$ đến $n$, các cột được đánh số từ $1$ đến $m$. Ô nằm ở dòng $i$ và cột $j$ được gọi là ô $(i, j)$. Có $k$ ô màu đen trên bảng, ô đen thứ $i$ nằm ở vị trí $(x_i, y_i)$. Các ô còn lại trong bảng đều có màu trắng.
Bạn có thể thực hiện một trong hai loại thao tác sau (mỗi thao tác có thể được thực hiện nhiều lần hoặc không lần nào):
- Chọn một dòng chỉ gồm các ô màu trắng, và xóa dòng đó khỏi bảng.
- Chọn một cột chỉ gồm các ô màu trắng, và xóa cột đó khỏi bảng.
Hãy tìm cách thực hiện các loại thao tác trên, sao cho số ô còn lại trong bảng là nhỏ nhất có thể.
Dữ liệu vào:
- Dòng đầu tiên gồm ba số nguyên $n, m, k$ $(1 \le n, m, k \le 10^5)$ – số dòng, số cột của bảng và số ô đen.
- $k$ dòng tiếp theo, dòng thứ $i$ gồm hai số nguyên $x_i, y_i$ $(1 \le x_i \le n,\ 1 \le y_i \le m)$ – vị trí của ô đen thứ $i$. Dữ liệu vào đảm bảo không có hai ô đen nào ở cùng vị trí.
Dữ liệu ra: In ra số ô còn lại nhỏ nhất có thể sau khi thực hiện hai loại thao tác trên.
Giới hạn: 1 giây, 256 MB.
3 4 3
2 1
2 4
3 364 4 4
2 1
3 4
4 1
4 46Gợi ý (của đề):
Bảng lúc ban đầu trong ví dụ thứ nhất, và ta có thể thực hiện thao tác biến đổi bảng như sau (các ô màu đỏ tương ứng với dòng hoặc cột được xóa):
Bảng lúc ban đầu trong ví dụ thứ hai, và ta có thể thực hiện thao tác biến đổi bảng như sau:
Lời mở đầu
Bài này nhìn đề có vẻ "ma trận", "xóa dòng xóa cột" nghe rất hoành tráng, nhưng thực ra chỉ cần một nhận xét nhỏ cùng kỹ thuật mảng đánh dấu là xử lý gọn gàng. Cái bẫy lớn nhất của bài lại nằm ngay ở cách làm "ngây thơ" đúng như đề mô tả – mình sẽ phân tích kỹ vì sao nó "toang" ngay từ bước đầu tiên nhé 😇.
Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
Mình giải thích ví dụ mẫu để bạn nào chưa hiểu đề sẽ hiểu rõ. Ở ví dụ 1, bảng có 3 dòng × 4 cột = 12 ô, ô đen nằm ở $(2,1)$, $(2,4)$, $(3,3)$. Dòng 1 không có ô đen nào nên xóa được (mất 4 ô). Còn lại 2 dòng; cột 2 không có ô đen nào nên xóa được (mất thêm 2 ô). Các dòng, cột còn lại đều có ô đen nên không xóa được nữa ⇒ còn $12 - 4 - 2 = 6$ ô.
Ở ví dụ 2, bảng 4 × 4 = 16 ô. Dòng 1 trắng hoàn toàn, cột 2 và cột 3 cũng trắng hoàn toàn. Xóa hết 1 dòng và 2 cột đó thì còn lại bảng 3 dòng × 2 cột = 6 ô.
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:
Cách 1Làm đúng y như đề mô tả – không ổn!
Nếu bạn nào làm theo đúng cách đề bài mô tả, tức là tạo ra ma trận có $N$ dòng $M$ cột, ví dụ ma trận kiểu bool (true/false), rồi đánh dấu tọa độ các ô đen trên ma trận là false; sau đó duyệt ma trận xét dòng nào toàn true (ô trắng) thì xóa dòng đó đi; xóa hết các dòng rồi thì xét qua các cột, cột nào toàn true thì xóa cột đó; sau cùng đếm số ô còn lại là kết quả.
Thì nếu làm như vậy các bạn đã "toang" ngay từ bước khởi đầu rồi, tức là bước tạo ra ma trận $N$ dòng, $M$ cột. Giả sử ma trận kiểu bool, mỗi phần tử chỉ chiếm 1 byte, thì ma trận cần $N \cdot M$ byte. Mà $N, M$ tối đa tới $10^5$ ⇒ $N \cdot M$ có thể lên tới $10^{10}$ (10 tỷ) phần tử, tức là 10 tỷ byte. Thử làm phép tính nhé:
9,765,625 / 1024 ≈ 9,536.743 MB
Trong khi giới hạn bộ nhớ chỉ có 256 MB, tức là ta gấp cỡ 37 lần giới hạn ⇒ chắc chắn bị MLE (Memory Limit Exceeded). Thậm chí giả sử bộ nhớ cho phép đi nữa (thực tế không bao giờ được) thì việc duyệt hết $10^{10}$ phần tử cũng bị TLE (Time Limit Exceeded), vì 1 giây chỉ chạy được cỡ $10^8$ phép tính – $10^{10}$ là vượt cả 100 lần rồi.
Cách 2Mảng đánh dấu dòng và cột – O(N + M + K)
Vẫn dựa theo một phần tư tưởng xử lý như đề mô tả, nhưng ta không cần đánh dấu từng ô trong ma trận, mà chỉ đánh dấu cả dòng, cả cột: dòng nào, cột nào có sự xuất hiện của ô đen. Đó chính là kỹ thuật mảng đánh dấu.
Lấy ví dụ 1: ma trận 3 dòng × 4 cột. Ta tạo mảng đánh dấu a có $3 + 1 = 4$ phần tử (index 0..3, bỏ qua index 0, chỉ dùng 1..3 ứng với số thứ tự dòng), khởi tạo tất cả là false. Tương tự tạo mảng b có $4 + 1 = 5$ phần tử cho các cột, cũng khởi tạo false.
Với mỗi ô đen $(i, j)$ đọc vào, ta đánh dấu a[i] = true; b[j] = true;. Sau khi đọc xong ví dụ 1 ta có:
| Dòng | a[1] | a[2] | a[3] | – |
|---|---|---|---|---|
| false | true | true | – | |
| Cột | b[1] | b[2] | b[3] | b[4] |
| true | false | true | true |
Giờ duyệt mảng a xem có bao nhiêu dòng trắng: dòng nào còn false nghĩa là chưa có ô đen nào – dòng trắng. Ở đây chỉ có a[1] là false ⇒ 1 dòng trắng. Mỗi dòng có $M$ ô nên số ô bỏ đi $= 1 \cdot M = 1 \cdot 4 = 4$.
Tiếp tục xét mảng b: chỉ có cột 2 trắng. Số ô trên một cột chính là số dòng của ma trận, nhưng nhớ là trước đó ta đã bỏ 1 dòng rồi, nên lúc này ma trận chỉ còn 2 dòng ⇒ số ô bỏ đi trên cột 2 là $1 \cdot 2 = 2$.
Cả thảy bỏ đi $4 + 2 = 6$ ô ⇒ còn lại $N \cdot M - 6 = 12 - 6 = 6$ ô. Đáp án in ra 6 ✅.
Thử với ví dụ 2: a = [false, true, true, true] (dòng 1..4), b = [true, false, false, true] (cột 1..4).
- Xóa 1 dòng (dòng 1) ⇒ số ô bị xóa $= 1 \cdot 4 = 4$.
- Xóa 2 cột (cột 2, 3), lúc này còn 3 dòng ⇒ số ô bị xóa $= 2 \cdot 3 = 6$.
- Tổng số ô bị xóa $= 4 + 6 = 10$ ⇒ còn lại $16 - 10 = 6$. Đáp án in ra 6 ✅.
temp_n = số dòng còn lại (dòng có ô đen). Số ô bị xóa: $cnt = (n - temp\_n) \cdot m + (\text{số cột trắng}) \cdot temp\_n$. Đáp án: $n \cdot m - cnt$.Minh hoạ đánh dấu và xóa
Em nhập kích thước bảng và các ô đen (mỗi ô là cặp x y, cách nhau bởi dấu ;), rồi bấm Tiếp để xem từng ô đen "nhuộm" dòng/cột của nó, sau đó các dòng/cột trắng bị tô đỏ và xóa:
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;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
long long n, m, k;
cin >> n >> m >> k;
bool a[n + 1] = {false}, b[m + 1] = {false};
while(k--){
int x, y;
cin >> x >> y;
a[x] = true;
b[y] = true;
}
long long temp_n = n;
for(int i = 1; i <= n; ++i){
if(a[i] == false){
temp_n--;
}
}
long long cnt = (n - temp_n) * m;
for(int i = 1; i <= m; ++i){
if(b[i] == false){
cnt += temp_n;
}
}
cout << (n * m) - cnt;
return 0;
}
import sys
def main():
data = sys.stdin.buffer.read().split()
n, m, k = int(data[0]), int(data[1]), int(data[2])
a = [False] * (n + 1)
b = [False] * (m + 1)
p = 3
for _ in range(k):
x, y = int(data[p]), int(data[p + 1])
p += 2
a[x] = True
b[y] = True
temp_n = n
for i in range(1, n + 1):
if a[i] == False:
temp_n -= 1
cnt = (n - temp_n) * m
for i in range(1, m + 1):
if b[i] == False:
cnt += temp_n
print(n * m - cnt)
main()
Giải thích một số chỗ trong code:
bool a[n + 1] = {false};nghĩa là khởi tạo mảngacó $n + 1$ phần tử, tất cả đều làfalse(trong lập trình 0 ⇔ false, 1 ⇔ true). Nói rõ hơn thì chỗ này chỉ gán giá trị cho ô đầu tiêna[0]là 0 thôi, nhưng nguyên tắc khi khởi tạo mảng bằng danh sách{...}thì toàn bộ các ô còn lại không được liệt kê sẽ mặc định là 0. Nên toàn bộ mảng đều là 0 hết.- Biến
temp_nlưu số dòng còn lại sau khi xóa dòng. Biếncntđếm số ô đã bị xóa. Sau khi xóa dòng, số ô bị xóa = (số dòng bị xóa) × (số cột), mà số dòng bị xóa $= n - temp\_n$, nên mới cócnt = (n - temp_n) * m;. Rồi cứ thấy cột nào bị xóa thì cộng thêm số dòng còn lại:cnt += temp_n;. Đáp án là(n * m) - cnt. - Phải khai báo
n, m, temp_n, cntkiểulong long(8 byte) để tránh tràn số. Nếun, mlàintthì phépn * mđược tính trong vùng nhớ tạm kiểuint, mà khi $n = m = 10^5$ thì $n \cdot m = 10^{10}$ –intkhông chứa nổi ⇒ tràn số và sai. Cứ để hếtlong longcho chắc nha các bạn.
b[n + 1], đúng ra mảng cột phải là b[m + 1]. Khi $m \gt n$ (vd: $n = 1$, $m = 10^5$) thì b[y] sẽ ghi ra ngoài mảng – có thể vẫn "lọt" test nhưng là lỗi tiềm ẩn. Code ở trên đã sửa thành b[m + 1].vector<bool> a(n + 1, false), logic y hệt.Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(N)$ cho mảng
a+ $O(M)$ cho mảngb, tức $O(N + M)$ – theo quy tắc cộng Big-O có thể viết $O(\max(N, M))$. Tính chính xác với trường hợp xấu nhất $N = M = 10^5$: $2 \cdot 10^5$ byte ≈ 195.3 KB ≈ 0.2 MB. Giới hạn cho tới 256 MB mà ta dùng có 0.2 MB thôi hehe. - Thời gian: $O(N)$ khởi tạo mảng
a+ $O(M)$ khởi tạo mảngb+ $O(K)$ đọc tọa độ các ô đen + $O(N)$ duyệt mảnga+ $O(M)$ duyệt mảngb⇒ $O(N + M + K)$, hay $O(\max(N, M, K))$. Trường hợp xấu nhất $N = M = K = 10^5$ thì tổng cỡ $5 \cdot 10^5$ lần lặp – ít hơn cả trăm lần so với ngưỡng 1 giây (cỡ $10^8$), yên tâm không TLE.
- 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