Site logoTungTT

Mô hình hóa câu đố Skyscraper

6 phút đọc
1123 từ

1. Giới thiệu#

Skyscraper là một câu đố logic được mô tả như sau, cho một lưới n×nn×n tượng trưng cho một khu phố nhìn từ trên cao, người chơi cần điền vào mỗi ô chiều cao của một tòa nhà, từ 11 đến nn, thỏa mãn các điều kiện sau:

  • Mỗi hàng và mỗi cột chứa đủ các chiều cao từ 11 đến nn, mỗi giá trị xuất hiện đúng một lần.
  • Các số đặt ở ngoài lưới cho biết số lượng tòa nhà có thể nhìn thấy khi nhìn vào lưới từ phía đó, trong đó một tòa nhà được coi là nhìn thấy nếu nó cao hơn tất cả các tòa nhà khác đứng giữa nó và người quan sát.

Nguồn: Conceptis Puzzles

Để dễ hình dung, hãy nhìn vào ví dụ dưới đây.

skyscraper_visibility

Nhìn từ bên trái, tòa nhà cao 33 được nhìn thấy (không có gì đứng trước nó), tòa nhà cao 11 bị che khuất phía sau, tòa nhà cao 55 vượt lên và được nhìn thấy, còn hai tòa còn lại (2244) đều thấp hơn 55 nên vẫn bị che khuất - vậy có đúng 22 tòa nhà được nhìn thấy.

Dưới đây là ví dụ cho một câu đố Skyscraper cỡ 5×5 và lời giải.

skyscraper_example

2. Mô hình hóa câu đố#

Một số ký hiệu dùng trong bài toán

  • nn là cỡ của lưới
  • FF là tập hợp các cặp (i,j)(i, j) biểu diễn tọa độ của các ô đã điền sẵn trên lưới
  • V(i,j)V(i, j) là chiều cao được điền trong ô (i,j)F(i, j) \in F
  • L\mathcal{L} là tập hợp các đường ngắm: đọc mỗi hàng theo hai chiều trái-sang-phải và phải-sang-trái, cùng mỗi cột theo hai chiều trên-xuống-dưới và dưới-lên-trên, ta thu được tổng cộng 4n4n đường ngắm. Với một đường ngắm LLL \in \mathcal{L}, Lp=(rp,cp)L_p = (r_p, c_p) (0p<n0 \leq p < n) là ô đứng ở vị trí thứ pp tính từ người quan sát, do đó L0L_0 là ô gần người quan sát nhất
  • v(L)v(L) là số lượng tòa nhà nhìn thấy của đường ngắm LL

2.1. Biến quyết định#

Với mỗi ô (i,j)(i, j) trong lưới và một chiều cao kk, xây dựng biến x(i,j,k)x(i, j, k) thỏa mãn

x(i,j,k)={1neˆˊoˆ (i,j) coˊ chieˆˋu cao k0ngược lạix(i, j, k) = \begin{cases} 1 & \text{nếu ô } (i, j) \text{ có chiều cao } k \\ 0 & \text{ngược lại} \end{cases}

2.2. Các ràng buộc#

  • Mỗi ô có đúng một chiều cao
k=1nx(i,j,k)=1,0i,j<n\sum\limits_{k = 1}^{n}{x(i, j, k)} = 1, \\ \forall 0 \leq i, j < n
  • Mỗi hàng chứa đủ các chiều cao từ 11 đến nn, mỗi giá trị đúng một lần
j=0n1x(i,j,k)=1,0i<n,1kn\sum\limits_{j = 0}^{n - 1}{x(i, j, k)} = 1, \\ \forall 0 \leq i < n, 1 \leq k \leq n
  • Mỗi cột chứa đủ các chiều cao từ 11 đến nn, mỗi giá trị đúng một lần
i=0n1x(i,j,k)=1,0j<n,1kn\sum\limits_{i = 0}^{n - 1}{x(i, j, k)} = 1, \\ \forall 0 \leq j < n, 1 \leq k \leq n
  • Các ô đã điền sẵn
x(i,j,V(i,j))=1,(i,j)Fx(i, j, V(i, j)) = 1, \\ \forall (i, j) \in F
  • Số lượng tòa nhà nhìn thấy trên mỗi đường ngắm phải đúng bằng yêu cầu.

    Nhìn dọc theo một đường ngắm LL từ phía người quan sát, một tòa nhà được coi là nhìn thấy nếu nó cao hơn mọi tòa nhà đứng giữa nó và người quan sát. Với mỗi đường ngắm LLL \in \mathcal{L}, vị trí 0p<n0 \leq p < n và chiều cao 1kn1 \leq k \leq n, ta xây dựng thêm biến trung gian zL(p,k)z_L(p, k) thỏa mãn

zL(p,k)={1neˆˊu toˋa nhaˋ tại Lp coˊ chieˆˋu cao k vaˋ khoˆng coˊ toˋa nhaˋ naˋo tại L0,,Lp1 cao hơn k0ngược lạiz_L(p, k) = \begin{cases} 1 & \text{nếu tòa nhà tại } L_p \text{ có chiều cao } k \text{ và không có tòa nhà nào tại } L_0, \ldots, L_{p - 1} \text{ cao hơn } k \\ 0 & \text{ngược lại} \end{cases}

Gọi

TL(p,k)=q=0p1k=k+1nx(rq,cq,k)T_L(p, k) = \sum\limits_{q = 0}^{p - 1}{\sum\limits_{k' = k + 1}^{n}{x(r_q, c_q, k')}}

là số tòa nhà đứng giữa người quan sát và vị trí pp trên đường ngắm LL mà cao hơn kk (tổng này đơn giản bằng 00 khi p=0p = 0, vì không có tòa nhà nào đứng trước tòa nhà đầu tiên). Bằng phương pháp hình học ta có thể biểu diễn biến zL(p,k)z_L(p, k) thông qua các biến xxTL(p,k)T_L(p, k) bằng các điều kiện sau

{TL(p,k)+pzL(p,k)px(rp,cp,k)zL(p,k)0x(rp,cp,k)TL(p,k)zL(p,k)0LL,0p<n,1kn\begin{cases} T_L(p, k) + p \cdot z_L(p, k) \leq p \\ x(r_p, c_p, k) - z_L(p, k) \geq 0 \\ x(r_p, c_p, k) - T_L(p, k) - z_L(p, k) \leq 0 \end{cases} \\ \forall L \in \mathcal{L}, 0 \leq p < n, 1 \leq k \leq n

Chú ý là ta không cần coi tòa nhà gần nhất hay tòa nhà cao nhất là hai trường hợp đặc biệt. Khi p=0p = 0, TL(0,k)=0T_L(0, k) = 0 với mọi kk, nên các điều kiện trên rút gọn thành zL(0,k)=x(r0,c0,k)z_L(0, k) = x(r_0, c_0, k): tòa nhà gần nhất luôn được nhìn thấy, bất kể chiều cao của nó là bao nhiêu. Tương tự, khi k=nk = n, TL(p,n)=0T_L(p, n) = 0 với mọi pp, vì không có tòa nhà nào cao hơn nn, nên zL(p,n)=x(rp,cp,n)z_L(p, n) = x(r_p, c_p, n): tòa nhà cao nhất luôn được nhìn thấy, dù nó đứng ở đâu. Cả hai trường hợp biên này tự động đúng theo công thức chung, ta không cần thêm gì cả.

Cuối cùng, yêu cầu về số tòa nhà cần được nhìn thấy của đường ngắm LL được thỏa mãn bằng điều kiện

p=0n1k=1nzL(p,k)=v(L),LL\sum\limits_{p = 0}^{n - 1}{\sum\limits_{k = 1}^{n}{z_L(p, k)}} = v(L), \\ \forall L \in \mathcal{L}

2.3. Hàm mục tiêu#

Bài toán này không có hàm mục tiêu, bởi vì ta không cần tối thiểu hóa hay tối đa hóa gì cả, việc cần làm chỉ là tìm ra một nghiệm chấp nhận được. Về mặt kỹ thuật, khi lập trình ta có thể để hàm mục tiêu là một hằng số (mình hay chọn số 0).

3. Kết luận#

Điểm nhấn của câu đố này nằm ở việc mô hình hóa điều kiện về số tòa nhà nhìn thấy trên 1 đường ngắm. Nhìn qua thì khá đơn giản nhưng lại rất phức tạp khi ta phải tạo thêm cả một hệ biến và điều kiện trung gian để giải quyết.

Tham khảo thêm các câu đố Skyscraper và biến thể tại Krazydad. Code mô hình hóa Skyscraper bằng python có tại Tung-hehe.

Happy modeling!