programming.in.th · ข้อ 2016
เติมช่องว่างในสาย DNA แล้วหาสายตัวที่ R ตามพจนานุกรม เมื่อ R ใหญ่ได้ถึงสองล้านล้าน และนิยามรูปแบบของสายเรียกตัวเอง
สาย DNA เขียนแทนด้วยตัวอักษรสี่ตัวคือ A C G T ตามชื่อนิวคลีโอไทด์สี่ชนิด
บางตำแหน่งนักชีววิทยาระบุไม่ได้ว่าเป็นตัวไหน จึงเขียนเป็น N ซึ่งแทนตัวไหนก็ได้ในสี่ตัวนั้น
เราเรียกลำดับที่มี N ว่าลำดับไม่สมบูรณ์ และลำดับที่ไม่มี N เลยว่า
ลำดับสมบูรณ์
ลำดับสมบูรณ์ตัวหนึ่ง เข้ากันได้ กับลำดับไม่สมบูรณ์ ถ้ามันเกิดจากการเติม N
ทุกตัวด้วยนิวคลีโอไทด์สักตัว เช่น ACCCT เข้ากันได้กับ ACNNT แต่ AGGAT ไม่เข้า
ต่อมาโจทย์นิยามรูปแบบของลำดับ โดยถือว่า A มาก่อน C มาก่อน
G มาก่อน T
AACCGT ACG ต่อกับ AT ได้ ACGAT
งานของเราคือ ให้ลำดับไม่สมบูรณ์ยาว M ตัวมา พร้อมเลข K และ R
แล้วหาลำดับสมบูรณ์รูปแบบ-K ตัวที่ R ตามลำดับพจนานุกรม ที่เข้ากันได้กับลำดับไม่สมบูรณ์นั้น
อินพุต / ขอบเขต / เอาต์พุต
M, K, R บรรทัดที่สองคือลำดับไม่สมบูรณ์ยาว M ตัวM ≤ 50,000, K ≤ 10, R ≤ 2 × 10^12
โจทย์รับประกันว่าลำดับรูปแบบ-K ที่เข้ากันได้มีไม่เกิน 4 × 10^18 ตัว และ R ไม่เกินจำนวนนั้น
เวลา 1 วินาที หน่วยความจำ 128 MB
RM ไม่เกิน 10| Input | Output |
|---|---|
| 9 3 5 ACANNCNNG | ACAAACCCG |
| 5 4 10 ACANN | ACAGC |
ใบ้
นิยามรูปแบบ-K เรียกตัวเอง ลองแทนรูปแบบ-(K−1) ด้วยนิยามของมันเองซ้ำไปเรื่อย ๆ สายรูปแบบ-K หน้าตาเป็นอย่างไร แล้วตัวอักษรคู่ติดกันแบบไหนที่บังคับให้ K ต้องใหญ่ขึ้น
ส่วน R ใหญ่ได้ถึงสองล้านล้าน (2 × 10^12)
ไล่เรียงทีละตัวจนถึงตัวที่ R ไม่ทันในหนึ่งวินาทีแน่ ๆ
ถ้าเรานับได้ว่า "ลำดับที่ขึ้นต้นด้วย ACA มีกี่ตัว" จะช่วยอะไรได้บ้าง
ช่องว่างสองช่อง ลองทุกแบบได้ในเวลาไม่นาน
แพตเทิร์น ACNN · ขอลำดับรูปแบบ-2 อันดับที่
3 จากทั้งหมด 16 แบบ
กดที่ช่อง N เพื่อวนเปลี่ยนตัวอักษร
สายตอนนี้ เป็นรูปแบบ-2 ตามนิยามของโจทย์
เกมตรวจสายด้วยนิยามในโจทย์ตรง ๆ และบอกแค่ว่าผ่านหรือไม่
ถ้าเดาสุ่มแล้วเหนื่อย ลองตรึงช่อง N ช่องแรกไว้ค่าหนึ่ง แล้วถามว่าจะเหลือสายที่ผ่านกี่แบบ เลขนั้นบอกอะไรกับอันดับที่ขอ
ที่มาของแนวคิดนี้
นิยามรูปแบบ-j เรียกตัวเอง และในรูปนั้นผมมองไม่ออกว่าจะนับมันอย่างไร งานแรกคือคลี่ให้เป็นเงื่อนไขที่ดูจากตัวสายได้ตรง ๆ
เงื่อนไขข้างล่างผมแกะเอง ก่อนเขียนโค้ดผมเอามันไปเทียบกับสองอย่างที่โจทย์ให้มา
คือเจ็ดตัวแรกของแพตเทิร์น ACANNCNNG และตัวอย่างว่าสายไหนเป็นหรือไม่เป็นรูปแบบ-1 กับรูปแบบ-3
ตรงทั้งสองชุด
ตัวอย่างในโจทย์ใช้ตรวจว่าเราตีความนิยามถูกหรือไม่ได้ ตั้งแต่ก่อนเขียนโค้ดบรรทัดแรก
คลี่นิยามทีละชั้น: รูปแบบ-2 คือสายรูปแบบ-1 หนึ่งหรือสองสายต่อกัน รูปแบบ-3 คือไม่เกินสามสาย และต่อไปเรื่อย ๆ
ดังนั้นรูปแบบ-K คือสายที่ตัดเป็นท่อนเรียงขึ้นได้ไม่เกิน K ท่อน คำถามต่อไปคือต้องตัดตรงไหนบ้าง
ตัดตรงรอยต่อทุกจุดก็ได้ท่อนที่เรียงขึ้นทุกท่อน และตัดน้อยกว่านั้นไม่ได้ จำนวนท่อนที่น้อยที่สุดคือจำนวนรอยต่อบวกหนึ่ง
ลำดับเป็นรูปแบบ-K ก็ต่อเมื่อมีรอยต่อ (ตำแหน่งที่ตัวถัดไปเล็กลง) ไม่เกิน K − 1 ตำแหน่ง
| ลำดับ | รอยต่อ | รูปแบบต่ำสุดที่เป็นได้ | โจทย์บอกว่า |
|---|---|---|---|
AACCGT | 0 | รูปแบบ-1 | เป็นรูปแบบ-1 ตรง |
AACGTC | 1 | รูปแบบ-2 | ไม่เป็นรูปแบบ-1 ตรง |
AACCC | 0 | รูปแบบ-1 | เป็นรูปแบบ-3 ตรง |
ACACC | 1 | รูปแบบ-2 | เป็นรูปแบบ-3 ตรง |
ACACA | 2 | รูปแบบ-3 | เป็นรูปแบบ-3 ตรง |
GCACAC | 3 | รูปแบบ-4 | ไม่เป็นรูปแบบ-3 ตรง |
ACACACA | 3 | รูปแบบ-4 | ไม่เป็นรูปแบบ-3 ตรง |
ระหว่างเติมจากซ้ายไปขวา ต้องจำแค่สองอย่าง
ตารางนับ f
f[i][c][d] = จำนวนวิธีเติมตำแหน่ง i ถึง M − 1 (นับตำแหน่งจาก 0)
เมื่อตัวก่อนหน้าคือ c และยังมีสิทธิ์ใช้อีก d ท่อน นับรวมท่อนที่กำลังเดินอยู่
c = 0 ถึง 3 แทน A C G T ส่วน c = 4 แปลว่ายังไม่มีตัวก่อนหน้าi = 0, c = 4, d = K และจำนวนลำดับที่ผ่านทั้งหมดคือ f[0][4][K]f[M][c][d] = 1 เมื่อ d ≥ 1 (เติมครบแล้ว)
f[i][c][d] = ผลรวมของ f[i+1][x][nd] ทุก x ใน A C G T ที่แพตเทิร์นตำแหน่ง i ยอม
nd = d − 1 ถ้า c < 4 และ x < c (เกิดรอยต่อ ขึ้นท่อนใหม่)
nd = d ถ้าไม่ใช่กรณีนั้น
ข้าม x ที่ทำให้ nd < 1 (ท่อนหมด) ลองคิดหนึ่งช่องบนตัวอย่างแรก
ตัวอย่างแรกมี M = 9 แถว f[9] ทุกช่องที่ d ≥ 1 เป็น 1
ช่อง f[7][C][1] คือเติมตำแหน่ง 7 ถึง 8
โดยตัวก่อนหน้าเป็น C และเหลือท่อนเดียว ตำแหน่ง 7 เป็น N ส่วนตำแหน่ง 8 เป็น G
รวมได้ f[7][C][1] = 2 คือสาย CG กับ GG
ตารางมี (M + 1) × 5 × (K + 1) ช่อง แต่ช่องที่ต้องคำนวณด้วยสูตรคือแถว i = 0 ถึง M − 1
ที่ d ≥ 1 รวม M × 5 × K ช่อง ที่ขอบเขตเต็มคือ 2,500,000 ช่อง
ช่องละลองสี่ตัวอักษร รวม 10,000,000 ครั้ง
ช่องที่ใหญ่เกิน 64 บิต
โจทย์รับประกันว่าลำดับที่ผ่านมีไม่เกิน 4 × 10^18 ตัว คำรับประกันนี้ครอบคลุมเฉพาะช่องที่เดินถึง
คือช่องที่มีคำนำหน้าจริงพาไปถึง แต่ตารางคำนวณทุกช่อง
ตัวอย่าง แพตเทิร์น TA ตามด้วย N อีก n ตัว และ K = 2
หลัง TA ใช้ไปแล้วสองท่อน ช่อง f[2][A][2] (ยังเหลือสองท่อน) จึงไม่มีคำนำหน้าไหนพาไปถึง
ช่องนั้นนับสายยาว n ที่มีรอยต่อไม่เกินหนึ่งรอย และเกินค่ามากที่สุดของ long long
(ราว 9.22 × 10^18) ตั้งแต่ n = 1,727 ขณะที่ลำดับที่ผ่านจริงตอนนั้นมีแค่ 861,456,960 ตัว
ช่องที่เดินถึงอ่านแต่ช่องที่เดินถึง เพราะต่อตัวอักษรที่ใช้ได้อีกหนึ่งตัวท้ายคำนำหน้าที่เดินถึง ก็ยังได้คำนำหน้าที่เดินถึง ค่าที่ล้นไม่มีทางไหลมาถึงคำตอบ
ถึงอย่างนั้น long long เป็นจำนวนเต็มแบบมีเครื่องหมาย และการล้นของมันเป็น
undefined behavior คือมาตรฐาน C++ ไม่กำหนดว่าผลจะออกมาเป็นอะไร
โค้ดเต็มเก็บค่าเป็น unsigned long long (จำนวนเต็ม 64 บิตที่ไม่มีค่าติดลบ) และบวกแบบมีเพดาน
ผลรวมที่เกิน 4 × 10^18 เก็บเป็น 4 × 10^18
ช่องที่เดินถึงไม่เกินเพดานนี้อยู่แล้ว ส่วน R ไม่เกิน 2 × 10^12
การเทียบ R กับช่องที่ชนเพดานให้ผลเหมือนใช้ค่าจริง
ตารางพร้อมแล้วก็เดินหาตัวที่ R ทีละตำแหน่ง ไล่ตัวอักษรตามลำดับพจนานุกรม
แล้วถามทีละตัวว่า "ถ้าเลือกตัวนี้ จะมีลำดับตามมาได้กี่แบบ" เรียกจำนวนนั้นว่า cnt
เริ่ม c = 4, d = K
ทุกตำแหน่ง i จาก 0 ถึง M − 1:
ไล่ x = A, C, G, T ที่แพตเทิร์นยอม (คำนวณ nd แบบเดียวกับข้างบน ข้าม nd < 1)
cnt = f[i+1][x][nd]
ถ้า R ≤ cnt: เลือก x ตั้ง c = x, d = nd แล้วไปตำแหน่งถัดไป
ไม่อย่างนั้น: R = R − cnt แล้วลอง x ตัวถัดไป string out; int c = 4, d = D; // c = ตัวก่อนหน้า (4 คือยังไม่มี), d = ท่อนที่เหลือใช้ได้
for (int i = 0; i < M; i++) {
for (int x = 0; x < 4; x++) { // ไล่ A C G T ตามลำดับพจนานุกรม
if (s[i] != 'N' && s[i] != AL[x]) continue;
int nd = (c < 4 && x < c) ? d - 1 : d; // ตัวนี้เล็กกว่าตัวก่อน แปลว่าขึ้นท่อนใหม่
if (nd < 1) continue;
ull cnt = f[i + 1][x][nd]; // ถ้าเลือกตัวนี้ จะมีลำดับตามมาได้กี่แบบ
if (R <= cnt) { out.push_back(AL[x]); c = x; d = nd; break; }
R -= cnt; // ตัวที่ตามหาอยู่หลังก้อนนี้ ข้ามไปทั้งก้อน
}
}
เจ็ดตัวแรกที่โปรแกรมสร้างได้คือ ACAAACAAG, ACAAACACG, ACAAACAGG, ACAAACCAG, ACAAACCCG, ACAAACCGG, ACAAACCTG
ตรงกับในโจทย์ทุกตัว ตัวที่ 5 คือ ACAAACCCG
ตารางข้างล่างนับตำแหน่งจาก 1 เมื่อเลือกตัวที่ตำแหน่ง p แล้ว ส่วนที่ยังต้องเติมคือ index p
ถึง M − 1 (นับจาก 0) จำนวนที่ตามมาจึงอ่านจาก f[p][x][nd]
กดถัดไปเพื่อเดินทีละตำแหน่ง
| ตำแหน่ง | R ที่เหลือ | ไล่ตัวเลือก (ตัว: จำนวนที่ตามมา) | เลือก |
|---|---|---|---|
| 1 | 5 | A:81 <- | A |
| 2 | 5 | C:81 <- | C |
| 3 | 5 | A:81 <- | A |
| 4 | 5 | A:36 <- | A |
| 5 | 5 | A:15 <- | A |
| 6 | 5 | C:15 <- | C |
| 7 | 2 | A:3 C:4 <- | C |
| 8 | 1 | A:1 C:1 <- | C |
| 9 | 1 | G:1 <- | G |
ตำแหน่งที่แพตเทิร์นกำหนดตัวอักษรไว้มีตัวเลือกเดียว จึงไม่มีการหักค่า
ส่วนช่อง N ลองจาก A ขึ้นไป และหัก R เฉพาะตัวที่จำนวนตามมาน้อยกว่า R
เมื่อ M ไม่เกิน 10 ลำดับที่เข้ากันได้มีมากที่สุด 4^10 = 1,048,576 ตัว
สร้างทั้งหมดแล้วหยิบตัวที่ R ได้สบาย
ถ้าเรียกซ้ำโดยไล่ A C G T ตามลำดับ ผลที่ได้ก็เรียงตามพจนานุกรมมาเอง ไม่ต้องเรียงทีหลัง
วิธีนี้ได้ 20 คะแนน และใช้ตรวจตารางกับการเดินของเวอร์ชันเต็มได้ เพราะมันไม่มีตาราง f
แต่มันนับรอยต่อแบบเดียวกัน การนับรอยต่อต้องตรวจกับนิยามเรียกซ้ำแยกอีกชั้น
บั๊กที่ง่ายที่สุดในการเดินคือเขียน R < cnt แทน R ≤ cnt
ซึ่งเท่ากับเดินแบบนับอันดับจาก 0 ทั้งที่ R นับจาก 1 ผลคือได้ตัวที่ R + 1
ตัวอย่างแรกจะตอบ ACAAACCGG ตัวอย่างที่สองตอบ ACAGG
และถ้า R เป็นตัวสุดท้ายพอดี โปรแกรมจะพิมพ์ออกมาไม่ครบ M ตัว
การ์ดข้างล่างนับรอบของลูปในสุดตามโค้ดของแต่ละวิธี
วิธีที่ 1
(4^M − 1)/3 × 4 + 4^M × (M − 1) รอบ
นับกรณี N ทั้งสาย: (4^M − 1)/3 คือจุดที่การเรียกซ้ำยังเติมไม่ครบ จุดละวนสี่ตัวอักษร
ส่วน 4^M คือสายที่เติมครบ สายละเทียบตัวติดกัน M − 1 คู่
ที่ M = 10 ได้ 10,835,284 รอบ ถ้า K = 10 ทุกสายผ่าน
ต้องเก็บ 1,048,576 สาย สายละ 32 ไบต์ รวม 33.6 MB
ที่ M = 50,000 N ทั้งสายยังเป็นอินพุตที่ถูกต้องเมื่อ K = 1
(สายที่ผ่านมีราว 2.1 × 10^13 สาย) แต่สายที่ต้องสร้างมีราว 10^30103 สาย
ได้ชุด 20 คะแนน
วิธีเต็ม
M × 5 × K × 4 + 4M รอบ
M × 5 × K คือช่องของตารางที่คำนวณ (ตำแหน่ง × ตัวก่อนหน้า × ท่อนที่เหลือ) ช่องละลองสี่ตัวอักษร
ส่วน 4M คือการเดิน ตำแหน่งละไม่เกินสี่ตัว
ที่ K = 10: M = 10 ได้ 2,040 รอบ และ M = 50,000 ได้ 10,200,000 รอบ
ตารางใช้ 22.0 MB จากลิมิต 128 MB
ได้ทุกชุด
| วิธี | ชุด 20 คะแนน (M ≤ 10) | ชุดที่เหลือ (M ถึง 50,000) |
|---|---|---|
| ไล่สร้างทุกลำดับ | ได้ | ไม่ทัน |
| นับก่อนแล้วเดิน | ได้ | ได้ |
M = 10 วิธีแรกใช้ 10,835,284 รอบ ยังทันสบาย แต่มันโตแบบทวีคูณตามจำนวน N
ส่วนวิธีเต็มโตแบบเชิงเส้นตาม M
ตาราง f เก็บเป็น unsigned long long ช่องละ 8 ไบต์ และโค้ดจองช่อง d ไว้ 11 ช่องเสมอ
ขนาดคือ (M + 1) × 5 × 11 ช่อง เต็มขอบเขต 22.0 MB จากลิมิต 128 MB
ตรวจอะไรไปแล้วบ้าง
K = 1 ถึง 8 รวม 174,752 คู่ ตรงทุกคู่
M 1 ถึง 8): คำตอบอ้างอิงมาจากไล่ทุกสาย เรียงด้วย
sorted แล้วกรองด้วยการแบ่งท่อน โดยไม่ใช้รอยต่อ โค้ดเต็มและโค้ดชุดย่อยตรงทุกชุด
M 9 ถึง 10: โค้ดเต็มตรงกับโค้ดชุดย่อยทุกชุดR < cnt บนทั้ง 700 ชุดข้างบน: ผลตรงกับที่อธิบายในหัวข้อไล่สร้างทุกชุดM = 50,000 และอยู่ในเงื่อนไขของโจทย์ (K = 10 มี N 30 ตัว ·
N ทั้งสาย K = 1 · TA ตามด้วย N K = 2)
M = 10 เป็น N ทั้งสาย K = 10: 0.05 วินาที 37.3 MBlong long บวกตรง ๆ ไม่มีเพดาน: ตอบตรงกันทั้งสามชุด
แต่ -fsanitize=undefined ฟ้องการล้นในชุด TA การนับรอยต่อมีเหตุผลรองรับในหัวข้อเฉลย ส่วนโค้ดนั้นทดสอบแล้ว ไม่ได้พิสูจน์ และยังไม่ได้ส่งเกรดเดอร์จริง
การนับก่อนแล้วเดินมีบทเต็มพร้อมโจทย์ฝึกที่ นับก่อน แล้วค่อยเดินไปหาตัวที่ต้องการ
ในหน้านี้