Đường đi (Tin học trẻ toàn quốc 2025 – Vòng khu vực miền Bắc & miền Trung – Bảng C1, C2)
Đề bài
Cho số nguyên dương $k$. Hãy tự xây dựng một đồ thị có hướng gồm không quá $5 \cdot 10^5$ đỉnh và không quá $10^6$ cạnh, thoả mãn đồng thời:
- giữa hai đỉnh bất kỳ có không quá một cạnh;
- đồ thị không có chu trình;
- có đúng $k$ đường đi từ đỉnh $1$ tới đỉnh có chỉ số lớn nhất (đỉnh $N$).
Dữ liệu vào: một dòng chứa số nguyên $k$ ($1 \le k \le 10^{100}$).
Kết quả: dòng đầu là hai số $N, M$ – số đỉnh và số cạnh (đỉnh đánh số $1..N$); $M$ dòng sau, mỗi dòng hai số $u\ v$ mô tả một cạnh có hướng từ $u$ tới $v$. Có nhiều đồ thị thoả mãn thì in đồ thị nào cũng được.
Subtask:
- Subtask 1 (15%): $k \le 100\,000$.
- Subtask 2 (20%): $k \le 10^{18}$.
- Subtask 3 (15%): $k = 2^x$ ($x$ nguyên không âm).
- Subtask 4 (50%): không có ràng buộc gì thêm.
Giới hạn thời gian 1 giây. Dữ liệu nhập từ bàn phím, in ra màn hình.
46 8
1 2
1 3
1 4
1 5
2 6
3 6
4 6
5 6Giải thích: đồ thị của ví dụ (đỉnh 1 nối tới 4 đỉnh giữa, 4 đỉnh giữa cùng nối tới đỉnh 6):
Có 4 đường đi từ đỉnh 1 tới đỉnh 6: $(1, 2, 6)$, $(1, 3, 6)$, $(1, 4, 6)$, $(1, 5, 6)$.
Lời mở đầu
Lại một bài "xây dựng": không có gì để tính, chỉ cần nghĩ ra một đồ thị. Cái khó nằm ở chỗ $k$ có thể lớn tới $10^{100}$, trong khi số đỉnh chỉ được tới $5 \cdot 10^5$ – nên không thể "mỗi đường đi một đỉnh" như ví dụ. Chìa khoá là hệ nhị phân: dựng các đỉnh có $1, 2, 4, 8, \dots$ đường đi rồi "cộng" lại. Phần còn lại là kỹ thuật đổi một số 101 chữ số sang nhị phân.
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 OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Bộ testBộ test có gì?
Bộ test của mình có 20 test (giống bộ trên CLUE). Vì có vô số đồ thị đúng nên bài được chấm bằng trình chấm: kiểm tra $N \le 5 \cdot 10^5$, $M \le 10^6$, không có khuyên, không có hai cạnh nối cùng cặp đỉnh, không có chu trình (sắp xếp tô-pô), rồi đếm số đường đi từ 1 tới $N$ bằng số lớn và so với $k$. Mình chia test theo subtask "nhỏ nhất" mà nó thoả:
| Nhóm | Các test | Số chữ số của $k$ | Số bit của $k$ | Số bit 1 | Đồ thị Cách 3 in ra $N$ lớn nhất / $M$ lớn nhất |
|---|---|---|---|---|---|
| Subtask 1 | 1–3 | 1 – 6 | 1 – 17 | 1 – 6 | 34 / 54 |
| Subtask 2 | 4–7 | 18 – 19 | 58 – 60 | 24 – 59 | 120 / 233 |
| Subtask 3 | 8–10 | 19 – 100 | 64 – 333 | 1 – 1 | 666 / 997 |
| Subtask 4 | 11–20 | 60 – 101 | 198 – 333 | 2 – 332 | 666 / 1325 |
- Test 2 là $k = 1$: đồ thị chỉ cần 2 đỉnh và cạnh $1 \to 2$.
- Các test $k = 2^x$ (subtask 3) là test 1, 2, 8–10 – trong đó có $k = 2^{63}$ vẫn vừa
unsigned long long, còn các test khác thì $k$ lên tới hơn 300 bit. - Đồ thị của Cách 3 (full) lớn nhất cũng chỉ khoảng 666 đỉnh và 1.200 cạnh – rất xa giới hạn.
Kết quả chạy thật của 3 cách bên dưới theo từng subtask:
| Subtask | Điều kiện | Test | Cách 1 hình quạt | Cách 2 nhị phân 64 bit | Cách 3 nhị phân số lớn |
|---|---|---|---|---|---|
| Subtask 1 (15%) | $k \le 100\,000$ | 1–3 | qua | qua | qua |
| Subtask 2 (20%) | $k \le 10^{18}$ | 4–7 | 0/4 test | qua | qua |
| Subtask 3 (15%) | $k = 2^x$ | 8–10 | 0/3 test | 1/3 test | qua |
| Subtask 4 (50%) | không ràng buộc thêm | 11–20 | 0/10 test | 0/10 test | qua |
Lời giải Cách 3 mình đã nộp thử trên trang chấm chính thức tinhoctre.vn và đạt 100 điểm (AC).
Ý tưởngĐếm đường đi và "nhân đôi" bằng hình thoi
Trong đồ thị không chu trình, gọi $f(v)$ là số đường đi từ đỉnh 1 tới $v$. Ta có $f(1) = 1$ và $$f(v) = \sum_{u \to v} f(u)$$ (mỗi đường tới $v$ phải đi qua đúng một cạnh cuối $u \to v$). Vậy muốn có đỉnh với $f = k$, ta cần các đỉnh có số đường đi "đẹp" rồi cộng chúng lại.
Viết $k$ trong hệ nhị phân: $k = \sum_{i:\ \text{bit } i = 1} 2^i$. Thêm đỉnh cuối $T$ và nối $v_i \to T$ với mọi bit $i$ bằng 1, ta được $f(T) = k$. Đánh số $v_i = 2i + 1$, $a_i = 2i + 2$, còn $T = 2L + 2$ là số lớn nhất (đỉnh $a_L$ không cần nên dùng số đó cho $T$). Đồ thị chỉ có cạnh từ số nhỏ sang số lớn nên chắc chắn không có chu trình.
Với $k \le 10^{100} < 2^{333}$: $L \le 332$, số đỉnh $\le 666$, số cạnh $\le 3 \cdot 332 + 333$. Em nhập $k$ (tới 4095) rồi bấm Tiếp để xem đồ thị được dựng dần, số dưới mỗi đỉnh là số đường đi từ đỉnh 1:
Cách 1Hình quạt như ví dụ (subtask 1)
Làm y như ví dụ: đỉnh 1 nối tới $k$ đỉnh giữa $2, 3, \dots, k+1$, mỗi đỉnh giữa nối tới đỉnh cuối $k + 2$. Mỗi đỉnh giữa cho đúng một đường đi, tổng cộng $k$ đường. Cần $k + 2$ đỉnh và $2k$ cạnh – chỉ dùng được khi $k + 2 \le 5 \cdot 10^5$.
Kết quả chạy thật: bản C++ đúng 3/20 test, sai test 8–20, quá thời gian test 4–7; các test đúng chạy lâu nhất 0,027 giây. Bản Python đúng 3/20 test, quá thời gian test 4–20; các test đúng chạy lâu nhất 0,075 giây.
Đúng trọn subtask 1. Từ test 4 trở đi $k$ quá lớn: bản C++ đọc $k$ vào long long nên với $k$ 100 chữ số bị tràn (in ra đồ thị sai), còn với $k \approx 10^{18}$ thì in không xuể – quá thời gian.
Cách 2Xương sống nhị phân với unsigned long long (subtask 1, 2)
Cài đặt đúng ý tưởng hình thoi. Bit cao nhất $L$ của $k$ lấy bằng 63 - __builtin_clzll(k) (hàm đếm số bit 0 ở đầu), bit thứ $i$ là k >> i & 1.
- Với $k = 1$: $L = 0$, không có hình thoi nào, $T = 2$ và chỉ có cạnh $1 \to 2$.
- Với $k = 2^L$: chỉ bit $L$ bằng 1, nên chỉ có một cạnh $v_L \to T$.
- Thứ tự cạnh in ra không quan trọng.
Kết quả chạy thật: bản C++ đúng 8/20 test, sai test 8, 9, 11–20; các test đúng chạy lâu nhất 0,004 giây. Bản Python đúng 20/20 test; các test đúng chạy lâu nhất 0,027 giây.
Bản C++ đúng mọi test có $k < 2^{64}$ (subtask 1, 2 và test $k = 2^{63}$); các test $k$ 60–101 chữ số thì cin đọc tràn nên sai. Riêng bản Python đúng hết vì số nguyên Python tự lớn tuỳ ý – với Python, Cách 2 đã là lời giải full!
Cách 3Đổi số lớn sang nhị phân bằng "chia cho 2 trên xâu" (full)
C++ không có kiểu số nguyên 333 bit, nên ta giữ $k$ ở dạng xâu thập phân và đổi sang nhị phân như cách làm tay: chia liên tiếp cho 2, các số dư (từ lần chia đầu) chính là các bit từ thấp tới cao.
Chia một xâu cho 2 cũng như chia dọc ở tiểu học: đi từ chữ số đầu, mang theo số dư $r$; ở mỗi chữ số $c$ tính $cur = 10r + c$, ghi chữ số thương $\lfloor cur / 2 \rfloor$, dư mới $r = cur \bmod 2$. Ví dụ $13 : 2$: chữ số 1 → thương 0 dư 1; chữ số 3 → $cur = 13$, thương 6 dư 1. Thương "06" → bỏ số 0 đầu → "6", dư 1.
Gợi ý của subtask 3 ($k = 2^x$) cũng chính là xương sống không có cạnh phụ nào tới $T$ ngoài $v_x \to T$ – nhưng vẫn phải tìm $x$ từ xâu, nên làm luôn cách tổng quát.
- Mỗi lần gọi
divTwotốn $O(\text{độ dài xâu}) \le 101$, gọi khoảng 333 lần – chưa tới $4 \cdot 10^4$ phép tính. - Khi thương bằng 0,
find_first_not_of('0')trả vềnposnên ta gán"0"để vòng lặp dừng. - Phần dựng đồ thị y hệt Cách 2, chỉ thay
k >> i & 1bằngbits[i].
Kết quả chạy thật: bản C++ đúng 20/20 test; các test đúng chạy lâu nhất 0,004 giây. Full điểm. (Bản Python chính là Cách 2 ở trên.)
Tổng kếtKết quả chạy thật trên bộ test
| Cách | Ngôn ngữ | Đúng | Lâu nhất (test đúng) | Ghi chú |
|---|---|---|---|---|
| 1. Hình quạt $k$ đỉnh giữa | C++ | 3/20 | 0,027s | Sai test 8–20, quá thời gian test 4–7 |
| Python | 3/20 | 0,075s | quá thời gian test 4–20 | |
2. Nhị phân, unsigned long long | C++ | 8/20 | 0,004s | Sai test 8, 9, 11–20 |
| 2. Nhị phân (Python: số nguyên tự lớn) | Python | 20/20 | 0,027s | Full điểm |
| 3. Nhị phân, đổi cơ số trên xâu | C++ | 20/20 | 0,004s | Full điểm |
Nhận xét
Đăng nhận xét