Xâu Fibonacci (FIB2)
Đề bài
XÂU FIBONACCI
Xâu Fibonacci thường được sử dụng để rèn luyện kỹ năng xử lý khi giới thiệu các giải thuật xử lý xâu.
Xét dãy xâu $F_0, F_1, F_2, \dots$ được xây dựng theo quy tắc sau:
- $F_0 = \text{'a'}$,
- $F_1 = \text{'b'}$,
- $F_n = F_{n-2} + F_{n-1}$, $n \gt 1$.
Yêu cầu: Cho hai số nguyên $n, k$ $(1 \le n \le 45)$, $k$ không vượt quá độ dài xâu $F_n$. Hãy xác định số lượng ký tự $\text{'a'}$ xuất hiện trong $k$ ký tự đầu tiên của xâu $F_n$.
Dữ liệu: vào từ tập tin văn bản FIB2.INP
- Dòng đầu tiên chứa số nguyên $T$ – số lượng tests cần xử lý $(1 \le T \le 100)$,
- Mỗi dòng trong $T$ dòng sau chứa 2 số nguyên $n, k$.
Kết quả: ghi ra tập tin văn bản FIB2.OUT, kết quả mỗi test đưa ra trên một dòng dưới dạng số nguyên.
Lưu ý: trên hệ thống chấm online, chương trình đọc dữ liệu từ bàn phím (stdin) và ghi ra màn hình (stdout), không dùng file.
4
0 1
1 1
3 2
7 71
0
1
3Lời mở đầu
Bài này nhìn đề thì tưởng dễ: cứ dựng xâu ra rồi đếm thôi. Nhưng chính cái "dễ" đó lại là cái bẫy 😁. Mình sẽ đi từ cách làm theo đúng đề để các bạn thấy nó "toang" ở đâu, rồi mới tới tư tưởng chia để trị (divide and conquer) để giải trọn vẹn.
Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJMình giải thích ví dụ mẫu cho bạn nào chưa hiểu đề. Liệt kê vài xâu đầu tiên (dấu | để các bạn thấy mỗi xâu được ghép từ 2 xâu trước như thế nào):
| $F_i$ | Xâu | Độ dài | Số 'a' |
|---|---|---|---|
| F0 | a | 1 | 1 |
| F1 | b | 1 | 0 |
| F2 | a|b | 2 | 1 |
| F3 | b|ab | 3 | 1 |
| F4 | ab|bab | 5 | 2 |
| F5 | bab|abbab | 8 | 3 |
| F6 | abbab|bababbab | 13 | 5 |
| F7 | bababbab|abbabbababbab | 21 | 8 |
0 1: 1 ký tự đầu của $F_0$ =a⇒ có 1 chữ 'a'.1 1: 1 ký tự đầu của $F_1$ =b⇒ 0.3 2: 2 ký tự đầu của $F_3$ =ba⇒ 1.7 7: 7 ký tự đầu của $F_7$ =bababba⇒ 3.
0 1 ($n = 0$). Vậy code của mình phải xử lý được cả $n = 0$ cho chắc ăn nha.Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Làm đúng theo đề: dựng xâu rồi đếm
Cách đơn giản nhất là làm y như đề nói: có $F_0$, $F_1$ ban đầu rồi tạo dần $F_i = F_{i-2} + F_{i-1}$ tới $F_n$, sau đó duyệt $k$ ký tự đầu tiên của $F_n$ đếm xem có bao nhiêu chữ 'a'. Làm vậy cho từng test trong $T$ test.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main(){
// freopen("FIB2.INP", "r", stdin); // khi thi bằng file thì bỏ // ở 2 dòng này
// freopen("FIB2.OUT", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--){
int n, k;
cin >> n >> k;
string f[n + 2];
f[0] = "a";
f[1] = "b";
for(int i = 2; i <= n; ++i){
f[i] = f[i - 2] + f[i - 1];
}
int cnt = 0;
for(int i = 0; i < k; ++i){
if(f[n][i] == 'a'){
cnt++;
}
}
cout << cnt << endl;
}
return 0;
}
import sys
# sys.stdin = open("FIB2.INP", "r") # khi thi bằng file thì bỏ # ở 2 dòng này
# sys.stdout = open("FIB2.OUT", "w")
input_data = sys.stdin.read().split()
it = iter(input_data)
t = int(next(it))
while t:
t -= 1
n = int(next(it))
k = int(next(it))
f = [''] * (n + 2)
f[0] = 'a'
f[1] = 'b'
for i in range(2, n + 1):
f[i] = f[i - 2] + f[i - 1]
cnt = 0
for i in range(k):
if f[n][i] == 'a':
cnt += 1
print(cnt)
freopen (Python là 2 dòng open) ở dạng chú thích. Khi đi thi bằng file thì các bạn bỏ dấu chú thích ra là được.Nhưng code này nộp lên hệ thống sẽ bị lỗi TLE (Time Limit Exceeded – chạy quá thời gian) và MLE (Memory Limit Exceeded – dùng quá bộ nhớ). Tại sao vậy? Các bạn nhìn cột độ dài ở bảng trên: $1, 1, 2, 3, 5, 8, 13, 21, \dots$ – chính là dãy Fibonacci! Nên $F_{45}$ chắc chắn dài khủng khiếp.
Đánh giá độ phức tạp: cả bộ nhớ lẫn thời gian đều tỉ lệ với độ dài xâu, tức là $O(|F_n|) \approx O(1.618^n)$ cho mỗi test.
Cách 2Chia để trị theo độ dài Fibonacci – O(T·45)
Bản chất để giải bài này là tư tưởng "chia để trị". Ta không cần dựng xâu, chỉ cần 2 mảng nhỏ: độ dài và số chữ 'a' của từng $F_i$.
Thử với ví dụ của đề: $n = 7$, $k = 7$. $F_7 = F_5 + F_6$, với $F_5$ dài 8, $F_6$ dài 13. Vì $k = 7 \lt 8$ nên 7 ký tự đầu của $F_7$ nằm gọn trong $F_5$ – chỉ cần xét $F_5$ thôi.
- $F_5 = F_3 + F_4$ ($F_3$ dài 3, $F_4$ dài 5): lấy trọn $F_3$, còn cần 4 ký tự đầu của $F_4$.
- $F_4 = F_2 + F_3$ ($F_2$ dài 2, $F_3$ dài 3): lấy trọn $F_2$, còn cần 2 ký tự đầu của $F_3$.
- $F_3 = F_1 + F_2$ ($F_1$ dài 1, $F_2$ dài 2): lấy trọn $F_1$, còn cần 1 ký tự đầu của $F_2$.
- $F_2 = F_0 + F_1$: lấy trọn $F_0$. Xong!
Tổng kết lại: 7 ký tự đầu của $F_7$ gồm trọn $F_3, F_2, F_1, F_0$ ⇒ số chữ 'a' là $1 + 1 + 0 + 1 = 3$ ✔.
Như vậy nếu ngay từ đầu ta tính sẵn số chữ 'a' của từng $F_i$ thì chỉ việc cộng lại. Việc này rất dễ, vì $F_i = F_{i-2} + F_{i-1}$ nên số 'a' cũng cộng y như vậy:
- Độ dài:
a[0] = a[1] = 1,a[i] = a[i-2] + a[i-1]⇒ $1, 1, 2, 3, 5, 8, 13, 21, \dots$ - Số chữ 'a':
b[0] = 1($F_0$ = a),b[1] = 0($F_1$ = b),b[i] = b[i-2] + b[i-1]⇒ $1, 0, 1, 1, 2, 3, 5, 8, \dots$
Gom khối: luôn lấy khối dài nhất có thể
Giờ thử test $n = 7$, $k = 14$. Nếu tách theo cách vừa nói thì 14 ký tự đầu gồm trọn $F_5, F_4, F_1$. Nhưng nhìn kỹ: trọn $F_5$ và trọn $F_4$ thì số chữ 'a' cũng chính bằng số chữ 'a' của $F_6$ (vì $F_6 = F_4 + F_5$) 😉. Nên ta chỉ cần quan tâm độ dài: với $k = 14$ thì khối dài nhất lấy được là $13$ (tức $F_6$), còn lại $1$.
Tương tự $k = 10$: khối dài nhất $\le 10$ là $8$ ($F_5$), còn $2$ – chính là $F_2$. Vậy $k = 10$ gồm trọn $F_5, F_2$. Dễ thấy quy luật chưa ^^
idx lớn nhất có a[idx] <= k (dùng upper_bound rồi lùi 1), cộng b[idx] vào đáp án, rồi k -= a[idx].Chỗ phức tạp một chút là khi còn dư đúng 1 ký tự: độ dài 1 thì có tới 2 khả năng $F_0$ (='a') hay $F_1$ (='b'). Phần này các bạn thử tự nhìn ra quy luật trước nhé, còn đây là gợi ý của mình: để ý ký tự đầu của $F_m$: là 'a' khi $m$ chẵn và 'b' khi $m$ lẻ (vì $F_m$ bắt đầu bằng $F_{m-2}$, cứ lùi dần về $F_0$ = a hoặc $F_1$ = b), và ký tự dư đó "đóng vai" ký tự đầu của khối kế tiếp $F_{idx+1}$. Các bạn thử nháp với $k = 4$ ($F_3$ + 1 ký tự, idx = 3 lẻ → 'a': baba… ✔) hay $k = 9$ ($F_5$ + 1 ký tự, idx = 5 lẻ → 'a': bababbaba… ✔). Vậy:
idx (khối cuối cùng vừa lấy) là số lẻ thì ký tự đó là 'a' ⇒ cộng thêm 1. Còn nếu ngay từ đầu $k = 1$ thì chỉ là ký tự đầu của $F_n$: là 'a' khi $n$ chẵn.Kiểm tra lại ví dụ đề: $(0, 1)$ → $n = 0$ chẵn → 1; $(1, 1)$ → lẻ → 0; $(3, 2)$ → lấy khối dài 2 ($F_2$, có 1 chữ a) → 1; $(7, 7)$ → khối 5 ($F_4$, 2 chữ a), còn 2 → khối 2 ($F_2$, 1 chữ a) → 3 ✔.
Minh hoạ: gom khối và so với xâu thật
Em nhập $n$ (nhỏ để còn vẽ được xâu) và $k$, bấm Tiếp để xem từng lần lấy khối. Phía trên là xâu $F_n$ thật với $k$ ký tự đầu được tô màu (vàng = 'a') để em đối chiếu kết quả:
Source code giải theo tư tưởng ở trên (code này mình đã nộp thành công), các bạn xem để hiểu rõ hơn:
#include <bits/stdc++.h>
using namespace std;
int solve(int n, int k){
if(k == 1){
if(n % 2 == 0){
return 1;
}
return 0;
}
int a[46], b[46];
a[0] = a[1] = b[0] = 1;
b[1] = 0;
for(int i = 2; i <= 45; ++i){
a[i] = a[i - 2] + a[i - 1];
b[i] = b[i - 2] + b[i - 1];
}
int sum = 0;
int idx;
while(k >= 2){
idx = upper_bound(a, a + 46, k) - a;
idx--;
sum += b[idx];
k -= a[idx];
}
if(k == 1 && idx % 2 != 0){
sum++;
}
return sum;
}
int main(){
// freopen("FIB2.INP", "r", stdin); // khi thi bằng file thì bỏ // ở 2 dòng này
// freopen("FIB2.OUT", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--){
int n, k;
cin >> n >> k;
cout << solve(n, k) << endl;
}
return 0;
}
import sys
import bisect
def solve(n, k):
if k == 1:
if n % 2 == 0:
return 1
return 0
a = [0] * 46
b = [0] * 46
a[0] = a[1] = b[0] = 1
b[1] = 0
for i in range(2, 46):
a[i] = a[i - 2] + a[i - 1]
b[i] = b[i - 2] + b[i - 1]
sum_ = 0
while k >= 2:
idx = bisect.bisect_right(a, k) - 1
sum_ += b[idx]
k -= a[idx]
if k == 1 and idx % 2 != 0:
sum_ += 1
return sum_
# sys.stdin = open("FIB2.INP", "r") # khi thi bằng file thì bỏ # ở 2 dòng này
# sys.stdout = open("FIB2.OUT", "w")
input_data = sys.stdin.read().split()
it = iter(input_data)
t = int(next(it))
for _ in range(t):
n = int(next(it))
k = int(next(it))
print(solve(n, k))
Đánh giá độ phức tạp của cách làm này:
- Không gian: chỉ 2 mảng 46 phần tử ⇒ $O(1)$ (cỡ 45).
- Thời gian: mỗi test tính bảng mất 45 bước; vòng gom khối mỗi lần lấy khối dài nhất $\le k$ thì phần còn lại nhỏ hơn khối kế tiếp nên số lần lặp không quá ~45, mỗi lần
upper_boundtốn $O(\log 46)$. Tổng cộng $O(T \cdot 45)$ – chạy tức thì.
int vẫn an toàn. Nhưng nếu đề nới lên $n = 46$ thì độ dài vượt int, lúc đó nhớ đổi sang long long nha.n + 2 phần tử thay vì n + 1, vì với test $n = 0$ của đề thì f[1] = "b" sẽ ghi ra ngoài mảng.- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét