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

Chia gia tài (DOWRY)

Chia gia tài (DOWRY)
Nguồn: CLUE OJ – Chương 11: Mảng 2 chiều Độ khó: Trung bình Mảng cộng dồn 2 chiều Duyệt

Đề bài

Đề tuyển sinh lớp 10 trường Phổ thông Năng khiếu năm 2022 – 2023.

Mảnh đất 3 dòng 4 cột của Phú Ông, mỗi lô đất có một giá trị

Do tuổi đã cao, Phú Ông quyết định chia toàn bộ ruộng đất của mình cho 3 người con. Đất của Phú Ông là một hình chữ nhật khá rộng lớn.

Bằng $n + 1$ đường rào ngang và $m + 1$ đường rào dọc song song, Phú Ông ngăn đất thành $n$ dòng, $m$ cột. Mảnh đất giới hạn bởi các đường rào ngang và dọc gọi là lô đất, như vậy Phú Ông có $n \times m$ lô đất. Lô đất ở dòng $i$, cột $j$ có giá trị $a_{ij}$.

Để chia đất cho 3 người con, ban đầu ông tách đất thành 2 miếng rời nhau bằng 1 đường rào ngang hoặc rào dọc. Miếng đất thứ nhất được chia cho người con đầu. Với miếng đất thứ hai, ông cũng tách thành 2 miếng rời nhau bằng 1 đường rào ngang hoặc rào dọc trên miếng đất này và chia cho 2 người còn lại. Kết quả là mỗi người con nhận được 1 miếng đất có dạng hình chữ nhật (số lô đất trong mỗi miếng có thể khác nhau). Để công bằng, Phú Ông muốn các miếng đất phải có giá trị bằng nhau. Giá trị của miếng đất bằng tổng giá trị tất cả lô đất thuộc miếng đất đó.

Yêu cầu: hãy giúp Phú Ông tìm ra một cách chia thỏa mong muốn hoặc thông báo không thể thực hiện.

Dữ liệu: vào từ tập tin văn bản DOWRY.INP

  • Dòng đầu tiên chứa 2 số nguyên $n$ và $m$ $(1 \le n, m \le 1000)$.
  • Dòng thứ $i$ trong $n$ dòng sau chứa $m$ số nguyên $a_{i1}, a_{i2}, \dots, a_{im}$ $(1 \le |a_{ij}| \le 10^6)$ – giá trị của $m$ lô đất tương ứng trên dòng $i$.

Kết quả: ghi ra tập tin văn bản DOWRY.OUT

  • Dòng đầu tiên ghi thông báo YES nếu tìm được một cách chia, ngược lại ghi thông báo NO.
  • Trường hợp tìm được một cách chia thì 2 dòng tiếp theo chứa mô tả đường tách thứ nhất và thứ hai tương ứng. Mỗi đường tách được mô tả theo dạng c i – trong đó c $\in$ {N, D} cho biết đường tách là ngang hay dọc và i là đường tách liền kề với dòng hoặc cột thứ i (đường nằm ngay dưới dòng i / ngay bên phải cột i). Dòng và cột được đánh số bắt đầu từ 1.

Giải thích ví dụ 1: đường ngang N 1 tách dòng 1 (giá trị $3 + 8 + 1 - 6 = 6$) cho người con đầu; miếng còn lại (dòng 2–3) được đường dọc D 2 tách thành 2 miếng giá trị $2 - 3 - 1 + 8 = 6$ và $2 + 2 + 1 + 1 = 6$. Ví dụ 2 không có cách chia.

Ghi chú của đề trên CLUE OJ: đề bài không nói nhưng nếu có nhiều cách chia đúng khác nhau thì các bạn cứ in ra 1 cách chia đúng bất kỳ, hệ thống sẽ tự kiểm tra. Dữ liệu đọc ghi với file với tên file như trong đề bài.

Ví dụ 1 – input
3 4
3 8 1 -6
2 -3 2 2
-1 8 1 1
Ví dụ 1 – output
YES
N 1
D 2
Ví dụ 2 – input
2 2
-8 6
10 -3
Ví dụ 2 – output
NO

Lời mở đầu

Hai đường rào chỉ có vài nghìn vị trí có thể đặt, nên ý tưởng "thử hết" là đúng hướng. Cái khó nằm ở chỗ tính tổng một miếng hình chữ nhật cho nhanh – đây là chỗ mảng cộng dồn 2 chiều tỏa sáng. Ngoài ra bộ test của bài có vài điểm "lệch" so với đề mà nếu không biết, các bạn có thể mất điểm oan; mình đã chạy thử nhiều phiên bản code để chỉ ra từng điểm một.

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, đề không chia subtask. Nhìn vào dữ liệu và đáp án mẫu thì các test được sinh theo từng "dạng" chia:

TestSố testDạng$n$ lớn nhất$m$ lớn nhất$n \cdot m$ lớn nhất$|a_{ij}|$ lớn nhấtYES / NO
1 – 2020ngẫu nhiên nhỏ1010701020 / 0
21 – 40203 dải ngang (N, N)9001000900.00013276820 / 0
41 – 60203 dải dọc (D, D)1000900900.00013276820 / 0
61 – 8020dải ngang + tách dọc (N, D)3000300900.00013276820 / 0
81 – 9818tay, rất nhỏ4466418 / 0
99 – 1002lớn, đáp án NO100010001.000.0001327680 / 2

Những điều cần biết (mình kiểm tra trực tiếp trên bộ test):

  • Test 74 – 80 có $n = 1500$ hoặc $3000$, vượt giới hạn $n \le 1000$ của đề (tích $n \cdot m$ vẫn không quá $10^6$). Khai báo mảng tĩnh p[1005][1005] sẽ bị truy cập ngoài mảng → lỗi chạy (RE). Hãy dùng vector cấp phát theo $n, m$ thật.
  • Giá trị thật chỉ trong khoảng $\pm 132\,768$; vài test nhỏ (test 1, 3, 4...) có ô bằng 0 dù đề ghi $|a_{ij}| \ge 1$ – không ảnh hưởng thuật toán. Tổng cả ruộng có thể tới $10^6 \cdot 1{,}3 \cdot 10^5 \approx 1{,}3 \cdot 10^{11}$ → phải dùng long long.
  • Người con đầu có thể nhận miếng dưới / miếng phải. Có 11 test mà đáp án gốc giao cho con cả miếng phải hoặc miếng dưới. Ví dụ ruộng 5 6 / 5 -1 (tổng 15, $T = 5$): đường D 1 tách ra cột 2 có giá trị $6 - 1 = 5$ cho con cả, cột 1 được tách tiếp bằng N 1 thành $5 \mid 5$. Nếu chỉ xét con cả nhận miếng trên/trái thì ruộng này bị trả lời NO.
  • Tổng bằng 0 thì đáp án chấp nhận miếng rỗng. Các test 82, 85, 89, 93, 96 có tổng cả ruộng bằng 0 và đáp án là YES với đường tách thứ hai nằm sát mép (chẳng hạn ruộng kiểu -3 -4 / 3 4: đường D 1 cho cột 1 tổng 0, cột 2 chỉ chia được nếu đặt đường thứ hai sát mép – người con thứ ba nhận miếng không có lô nào, giá trị 0). Nếu bắt mọi miếng phải có ít nhất 1 lô thì sẽ trả lời NO và sai các test này. Vì vậy trong code mình cho đường tách thứ hai được đặt tới tận mép miếng đất.

Kết quả chạy thật trên từng nhóm test (chấm bằng checker kiểm tra tổng 3 miếng, giới hạn 1 giây):

TestDạngCách 1
vét cạn
Cách 2
cộng dồn
Cách 2 +
mảng tĩnh
Chỉ cho con cả
miếng trên/trái
Cấm miếng
rỗng
1 – 20ngẫu nhiên nhỏ✔✔✔✘ 5 WA✔
21 – 403 dải ngang (N, N)✔✔✔✔✔
41 – 603 dải dọc (D, D)✘ 3 TLE✔✔✔✔
61 – 80dải ngang + tách dọc (N, D)✘ 3 TLE✔✘ 7 RE✔✔
81 – 98tay, rất nhỏ✔✔✔✘ 6 WA✘ 5 WA
99 – 100lớn, đáp án NO✘ 2 TLE✔✔✔✔
Tổng92/100100/10093/10089/10095/100

Ý tưởngThử mọi vị trí 2 đường rào

Gọi $S$ là tổng giá trị cả ruộng. Nếu $S$ không chia hết cho 3 thì chắc chắn NO. Ngược lại mỗi người con phải nhận đúng $T = S / 3$.

  1. Đường tách thứ nhất: ngang (dưới dòng $i$, $1 \le i < n$) hoặc dọc (bên phải cột $i$, $1 \le i < m$). Nó chia ruộng thành miếng trước (trên/trái) và miếng sau (dưới/phải).
  2. Miếng nào có tổng đúng bằng $T$ thì có thể giao cho người con đầu; miếng còn lại (tổng $2T$) phải tách tiếp.
  3. Đường tách thứ hai: thử mọi đường ngang và dọc trên miếng còn lại; nếu phần đầu có tổng $T$ thì phần sau cũng tổng $2T - T = T$ – xong!

Có tối đa $n + m$ vị trí cho đường thứ nhất, mỗi vị trí thử tối đa $n + m$ vị trí cho đường thứ hai: chỉ cỡ $(n+m)^2 \approx 1{,}6 \cdot 10^7$ lần thử. Việc còn lại là tính tổng mỗi miếng thật nhanh. Em nhập ruộng (các dòng cách nhau bởi ;) rồi bấm Tiếp để xem máy thử từng đường rào:

Ví dụ nhanh:

Cách 1Vét cạn: cộng tay tổng mỗi miếng

Hàm sum(r1, c1, r2, c2) cộng tay từng lô trong hình chữ nhật. Hàm tach thử tách một miếng có tổng $2T$ thành 2 miếng tổng $T$ và ghi lại đường tách vào res.

C++▶ Chạy tay từng dòng codeCách 1: thử mọi vị trí 2 đường rào, tổng mỗi miếng cộng tay.
Dữ liệu mẫu:
Mỗi lời gọi sum (cộng tay cả miếng) mình gộp vào bước của dòng gọi nó; đọc dữ liệu gộp mỗi dòng 1 bước.
  1. Điều kiện (A == T && tach(B)) || (B == T && tach(A)): thử cả 2 khả năng con cả nhận miếng trước hoặc miếng sau. Nhờ "ngắt mạch" của &&, hàm tach chỉ được gọi khi miếng kia có tổng đúng $T$.
  2. Trong tach, vòng j chạy tới tận r2 (hoặc c2): khi đó phần sau rỗng, chỉ khớp được khi $T = 0$ – đúng như đáp án của bộ test.
  3. Mỗi lần gọi sum tốn tới $n \cdot m$ phép cộng, nên trường hợp xấu nhất là $O((n+m)^2 \cdot nm)$.
Không gian$O(nm)$
Thời gian$O((n+m)^2 \cdot nm)$

Kết quả chạy thật: đúng 92/100 test (sai 058, 059, 060, 078, 079, 080, 099, 100 – TLE), test đúng chậm nhất 0.38 giây. Cách này qua được nhiều hơn mình tưởng vì nó dừng ngay khi tìm được cách chia và hàm tach chỉ được gọi khi miếng đầu đúng bằng $T$ (rất hiếm). Nó chỉ chậm ở các test mà đường tách nằm xa (dòng/cột thứ vài trăm) hoặc phải thử hết vì đáp án NO (test 99, 100).

Cách 2Mảng cộng dồn 2 chiều – mỗi miếng O(1)

Dựng mảng $p[i][j]$ = tổng hình chữ nhật từ lô $(1, 1)$ tới lô $(i, j)$:

p[i][j] = p[i-1][j] + p[i][j-1] - p[i-1][j-1] + a[i][j]

rồi tổng miếng $(r_1, c_1) \to (r_2, c_2)$ là p[r2][c2] - p[r1-1][c2] - p[r2][c1-1] + p[r1-1][c1-1]. Phần tìm kiếm giữ nguyên, chỉ thay hàm sum:

C++▶ Chạy tay từng dòng codeCách 2: giống cách 1 nhưng tổng mỗi miếng lấy từ mảng cộng dồn 2 chiều trong O(1).
Dữ liệu mẫu:
Lời gọi sum (tra mảng p) gộp vào bước của dòng gọi nó.
  1. p là vector cấp phát đúng $(n+1) \times (m+1)$ nên chạy được cả các test $n = 3000$.
  2. Không cần lưu mảng a: đọc tới đâu tính p tới đó.
  3. Dòng if(S % 3 != 0) dùng được với cả số âm vì C++ cho -6 % 3 == 0, -5 % 3 == -2 != 0.
Không gian$O(nm)$
Thời gian$O(nm + (n+m)^2)$

Kết quả chạy thật: đúng 100/100 test, test đúng chậm nhất 0.09 giây. Bản Python: đúng 100/100 test, test đúng chậm nhất 0.62 giây.

Còn nếu khai báo mảng tĩnh theo đúng giới hạn đề:

long long p[1005][1005];   // theo đúng giới hạn đề n, m <= 1000

thì kết quả là đúng 93/100 test (sai 074, 075, 076, 077, 078, 079, 080 – RE), test đúng chậm nhất 0.09 giây – chính là các test có $n > 1000$.

Ghi chú của mình: bản C++ cách 2 đạt 100/100 test (chấm bằng checker riêng của mình: kiểm tra YES/NO và tổng 3 miếng; thời gian đo bằng CPU time). Do có nhiều đáp án đúng, mình chấp nhận đường tách thứ hai cắt miếng trước hoặc miếng sau – đáp án gốc của bộ test cũng dùng cả hai kiểu. Các phiên bản "lệch" đều được chạy thật: mảng tĩnh 93/100, chỉ cho con cả miếng trên/trái 89/100, cấm miếng rỗng 95/100.
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