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

Vòng số nguyên tố

Vòng số nguyên tố
Nguồn: CLUE OJ – Chương 20: Quay lui (Mr Lê Minh Hoàng) Độ khó: Trung bình Quay lui Cắt nhánh

Đề bài

Một vòng tròn chứa $2 \times n$ vòng tròn nhỏ (xem hình vẽ). Các vòng tròn nhỏ được đánh số từ $1$ đến $2 \times n$ theo chiều kim đồng hồ. Cần điền các số tự nhiên từ $1$ đến $2 \times n$, mỗi số vào một vòng tròn nhỏ, sao cho tổng của hai số trên hai vòng tròn nhỏ liên tiếp là số nguyên tố. Số điền ở vòng tròn nhỏ $1$ luôn là số $1$.

Vòng tròn 6 ô điền 1, 4, 3, 2, 5, 6 theo chiều kim đồng hồ

Input: số nguyên dương $n$ $(1 < n < 10)$.

Output:

  • Dòng đầu tiên ghi ra số $k$ là số cách tìm được.
  • $k$ dòng tiếp theo, mỗi dòng ghi ra 1 cách điền các số vào các vòng tròn nhỏ (số ở vòng 1, vòng 2, …, vòng $2n$). Cách điền nào có thứ tự từ điển nhỏ hơn thì xếp trước. Nếu $k > 10000$ thì chỉ cần ghi ra $10000$ cách đầu tiên.
Ví dụ – input
4
Ví dụ – output
4
1 2 3 8 5 6 7 4
1 2 5 8 3 4 7 6
1 4 7 6 5 8 3 2
1 6 7 4 3 8 5 2

Lưu ý: vì là vòng tròn nên ô $2n$ cũng kề với ô $1$ – tổng của số cuối và số $1$ cũng phải là số nguyên tố. Ở dòng đầu, $k$ là tổng số cách (kể cả khi chỉ in 10000 cách).

Lời mở đầu

Bài này là "hoán vị có điều kiện": ta cần liệt kê các hoán vị của $2, 3, \dots, 2n$ (số 1 đứng cố định ở đầu) sao cho hai số kề nhau có tổng nguyên tố. Mình sẽ đi từ cách sinh mọi hoán vị (chỉ qua được $n$ nhỏ), tới quay lui có cắt nhánh, rồi thêm một nhận xét chẵn lẻ để chạy nhanh gấp đôi.

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ì?

Test12345678
n43256789
Số cách k422961.0242.88081.024770.144
Số dòng cách in ra422961.0242.88010.00010.000

Bộ test có 8 test, mỗi giá trị $n$ từ 2 đến 9 đúng một test. Hai test cuối có $k > 10000$ nên chỉ in 10000 cách, nhưng dòng đầu vẫn là tổng số cách thật. Đề không chia subtask, mình tự chia 2 mức theo độ lớn của $n$ (giới hạn 1 giây):

MứcĐiều kiệnTestCách 1
(hoán vị)
Cách 2
(quay lui)
Cách 3
(+ chẵn lẻ)
1$n \le 6$1–55/55/55/5
2$7 \le n \le 9$6–80/33/33/3

Ý tưởngĐiền lần lượt từng ô, sai là quay lui ngay

Ta điền số cho ô $2, 3, \dots, 2n$ theo thứ tự. Ở ô $i$, thử các số $v$ từ nhỏ đến lớn mà:

  • $v$ chưa được dùng (mảng đánh dấu dd[v]);
  • $a_{i-1} + v$ là số nguyên tố.

Nếu thoả thì điền $v$, gọi đệ quy sang ô $i + 1$; gọi xong thì bỏ đánh dấu $v$ (quay lui). Khi đã điền tới ô $2n$ thì kiểm tra thêm cặp "khép vòng" $a_{2n} + a_1$.

Vì sao thứ tự từ điển đúng sẵn?
Ô trước được điền trước, và ở mỗi ô ta thử $v$ tăng dần, nên các cách được tìm ra đúng theo thứ tự từ điển – không cần sắp xếp. 10000 cách đầu tiên tìm được chính là 10000 cách nhỏ nhất.

Vì đề bắt in $k$ trước các cách, mà $k$ chỉ biết khi đã quay lui xong, ta lưu tạm tối đa 10000 cách vào một xâu rồi mới in. Em chọn $n$ rồi bấm Tiếp để xem cây quay lui (nút đỏ = ngõ cụt, nút xanh = một cách hợp lệ):

Ví dụ nhanh:

Cách 1Sinh mọi hoán vị – O((2n−1)!·n)

Giữ $a_1 = 1$, dùng next_permutation sinh mọi hoán vị của $a_2..a_{2n}$ (tự động theo thứ tự từ điển), với mỗi hoán vị kiểm tra $2n$ cặp kề nhau.

C++▶ Chạy tay từng dòng codeCách 1: sinh mọi hoán vị a[2..m], kiểm tra từng cặp kề nhau.
Dữ liệu mẫu:
Vòng for kiểm tra được gộp vào bước ở dòng if(!nt(...)); các bước tạo xâu s gộp vào bước lưu.
  1. j = (i == m) ? 1 : i + 1: ô kề sau ô cuối là ô 1 (vòng tròn).
  2. next_permutation(a + 2, a + m + 1) chỉ hoán vị đoạn $a_2..a_m$, số 1 đứng yên.
Không gian$O(n)$ + 10000 dòng lưu
Thời gian$O((2n-1)! \cdot n)$

Có $(2n-1)!$ hoán vị: $n = 6$ là $11! \approx 4 \cdot 10^7$ – vừa kịp; $n = 7$ đã là $13! \approx 6 \cdot 10^9$ – hết cứu.

Kết quả chạy thật: Bản C++ đúng 5/8 test, quá thời gian ở test 6–8; test đúng chạy lâu nhất 0.22s.

Cách 2Quay lui + cắt nhánh

Kiểm tra điều kiện nguyên tố ngay khi điền từng ô: chỉ cần một cặp sai là bỏ cả nhánh phía sau. Các số nguyên tố đến $4n$ được tính sẵn vào mảng nt[] để tra trong $O(1)$.

C++▶ Chạy tay từng dòng codeCách 2: quay lui điền từng ô, cắt nhánh khi tổng không nguyên tố.
Dữ liệu mẫu:
Vòng tạo xâu kết quả được gộp vào 1 bước.
  1. if(!dd[v] && nt[a[i - 1] + v]): chỉ đi tiếp khi $v$ chưa dùng và tạo tổng nguyên tố với ô trước – đây là chỗ cắt nhánh.
  2. if(i < m) Try(i + 1); còn ô thì đi sâu tiếp; else if(nt[a[m] + a[1]]) là ô cuối thì kiểm tra cặp khép vòng.
  3. k kiểu long long và vẫn tăng sau khi đã đủ 10000 cách; chỉ việc lưu là dừng ở 10000.
Không gian$O(n)$ + 10000 dòng lưu
Thời giantỉ lệ với số nút cây quay lui

Kết quả chạy thật: Bản C++ đúng 8/8 test; test đúng chạy lâu nhất 0.62s. Còn bản Python đúng 7/8 test, quá thời gian ở test 8; test đúng chạy lâu nhất 0.96s.

Cách 3Thêm nhận xét chẵn lẻ

Tổng hai số kề nhau $\ge 1 + 2 = 3$, mà số nguyên tố lớn hơn 2 đều lẻ. Tổng lẻ thì hai số phải khác tính chẵn lẻ. Vì $a_1 = 1$ lẻ, nên dọc theo vòng: ô lẻ chứa số lẻ, ô chẵn chứa số chẵn. Vậy ở ô $i$ chỉ cần thử $v$ cùng tính chẵn lẻ với $i$ – vòng for nhảy bước 2, bớt một nửa số lần thử ở mỗi tầng.

C++▶ Chạy tay từng dòng codeCách 3: như cách 2 nhưng ô chẵn chỉ thử số chẵn, ô lẻ chỉ thử số lẻ.
Dữ liệu mẫu:
Vòng tạo xâu kết quả được gộp vào 1 bước.

Chỉ khác cách 2 ở dòng for: v bắt đầu từ 2 (ô chẵn) hoặc 3 (ô lẻ) và tăng 2 mỗi lần.

Không gian$O(n)$ + 10000 dòng lưu
Thời giankhoảng một nửa cách 2

Kết quả chạy thật: Bản C++ đúng 8/8 test; test đúng chạy lâu nhất 0.37s. Còn bản Python đúng 7/8 test, quá thời gian ở test 8; test đúng chạy lâu nhất 0.61s.

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

CáchNgôn ngữĐúngLâu nhấtGhi chú
1. Sinh mọi hoán vịC++5/8> 1sQuá thời gian test 6–8 (n = 7, 8, 9)
2. Quay lui + cắt nhánhC++8/80.62sFull
Python7/8> 1sQuá thời gian test 8 (n = 9)
3. Quay lui + chẵn lẻC++8/80.37sFull
Python7/8> 1sQuá thời gian test 8 (n = 9)
Ghi chú của mình: sinh mọi hoán vị chỉ qua được $n \le 6$. Quay lui có cắt nhánh qua hết 8 test (test nặng nhất $n = 9$ có $770\,144$ cách mà vẫn phải đếm đủ), thêm nhận xét chẵn lẻ thì nhanh hơn gần gấp đôi. Bản Python của cả hai cách quay lui đều quá thời gian ở $n = 9$ – bài này nên nộp C++.
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