Cách nhìn khác đối với một số lớp bài toán quen thuộc
Cao Minh Anh
Các bạn đã bao giờ thích thú khi tìm ra một cách nhìn nhận mới cho những bài mình đã
biết? Thật là thú vị nếu ta tìm cho mình một phương pháp khác mà tính tối ưu vẫn không
giảm.
Sau đây tôi sẽ trình bày với các bạn một cách nhìn nhận khác đối với lớp bài toán như xác
định hoán vị thứ k, nhị phân thứ k, …
Tư tưởng thuật toán
- Giả sử cần tìm 1 trạng thái nào đó gồm n phần tử của một dạng xác định. Đề bài cho biết
đặc điểm của trạng thái trên là M.
- Với M ta có thể xác định phần tử đầu tiên của trạng thái, nhưng n-1 phần tử còn lại thì
không xác định được. Vấn đề bây giờ là làm sao tìm được n-1 trạng thái còn lại. Rất đơn
giản, bởi vì nếu biết được M ta sẽ xác định được phần tử đầu tiên, tại sao ta không thử n-1
phần tử còn lại có phải thuộc một trạng thái nào đó gồm n-1 phần tử có đặc điểm M1. Từ
M1 này ta suy được phần tử đầu tiên của trạng thái mới, tức là đã tìm được phần tử thứ 2
của trạng thái ban đầu. Cứ làm như thế ta sẽ tìm được hết các phần tử của trạng thái ban
đầu.
- Cái khó của thuật toán trên là đòi hỏi người lập trình phải tìm được cách xác định phần tử
đầu tiên dựa vào đặc điểm M, thứ hai là phải tìm được các đặc điểm M
1
, M
2
,..., M
n-1
tương
ứng với các trạng thái mới có ít phần tử hơn.
Bài toán 1
Xác định dãy nhị phân thứ k gồm n phần tử (n≤200).
File nhiphan.inp
n k
File nhiphan.out
a
1
a
2
a
3
.. a
n
(dãy nhị phân thứ k)
Ví dụ
nhiphan.inp
4 7
nhiphan.out
0 1 1 0
Thuật toán
Dựa vào ví dụ dưới ta nhận thấy rằng:
- Có 1 nửa trạng thái đầu tiên có số 0 đứng đầu, còn nửa còn lại là số 1 đứng đầu. Hay có
2
n-1
trạng thái đầu tiên bắt đầu là số 0, còn lại 2
n-1
trạng thái bắt đầu là số 1. Như vậy chỉ
cần so sánh k với 2
n-1
thì ta có thể biết được phần tử đầu tiên của dãy nhị phân thứ k là 0
hay 1.
If k>2
n-1
then a[1]=1 else a[1]=0.
- Vấn đề còn lại là làm sao xác định được n-1 phần tử còn lại.
- Ta nhận thấy rằng nếu phần tử đầu tiên là 0 thì n-1 phần tử còn lại giống với các phần tử
của dãy nhị phân thứ k có n-1 phần tử, còn nếu phần tử đầu tiên là 1 thì n-1 phần tử còn lại
giống với các phần tử của dãy nhị phân thứ (k-2
n-1
) có n-1 phần tử. Bây giờ ta lại làm bài
toán nhỏ hơn là xác định dãy nhị phân thứ k’ nào đó gồm n-1 phần tử. Cứ làm như thế ta sẽ
tìm được hết các phần còn lại.
Từ những nhận xét trên ta có chương trình đơn giản như sau:
For I:=n downto 1 do
Begin
If k>1 shl (I-1) then
Begin
Write(‘1’);
K:=k-(1 shl (I-1));
End
Else write(‘0’);
End;
Vì bài này dữ liệu lớn, ta chỉ cần làm thêm chương trình nhân số lớn để tính 2
h
, và hàm so
sánh 2 xâu k với 2
h
.
Bài 2
Tìm hoán vị thứ k gồm n phần tử (n≤200).
Thuật toán
Nhận xét
- (n-1)! Trạng thái đầu tiên có phần tử đầu tiên là 1, tiếp theo (n-1)! Tiếp là 2, tiếp tới là 3,
và cuối cùng là 4.
- Dựa vào k ta có thể xác định phần tử đầu tiên. Nếu tìm được phần tử đầu tiên là h thì
dùng mảng b để đánh dấu h, để n-1 phần tử còn lại không có h.
- Bây giờ cần xác định vị trí k’ của n-1 phần tử còn lại.
- K’ = k-v (với v là vị trí đầu tiên bắt đầu bằng số h). Việc tính v rất dễ dàng.
Đây là phần giải tạm với dữ liệu k≤2
32
.
Fillchar(b,sizeof(b),1);
T:=1;
For I:=1 to n do T:=T*I;
For I:=n downto 1
Begin
T:=T div I;
For j:=1 to n do
If b[j] then
Begin
if K>T then k:=k-T
else
begin
write(j:4);
b[j]:=false;
break;
end;
end;
end;
Ta có thể nâng dữ liệu lên n≤1000, chỉ cần làm chương trình nhân số lớn và hàm so sánh
hai số lớn là xong.
Bài 3
(Đề 2 - dãy nhị phân olympic tin học sinh viên khối chuyên tin)
Xét tập S các dãy nhị phân độ dài N trong đó không có hai bit 1 nào kề nhau. Các dãy này
được xếp theo chiều tăng dần của số nguyên mà nó biểu diễn, theo thứ tự đó mỗi dãy có
một số hiệu, chẳng hạn n=5.
Cho số nguyên dương N≤100 hãy nhị phân có số hiệu M.
Ví dụ
BINSEQ.inp
5 5
BINSEQ.out
0 0 1 0 1
Thuật toán
Đây là một bài rất khó để ta xác định cách xác định phần tử đầu tiên.
Nhận xét :
Với n=5 có 13 dãy tất cả. Trong đó có 8 dãy đầu tiên bắt đầu bằng 0,5 dãy còn lại bắt đầu
bằng 1. Với n=6 thì có tất cả 21 dãy, 13 cái đầu là 0,8 cái sau là 1. Ta thấy nó có liên quan
đến số fibonaci.
Chỉ cần kiểm tra M với a[n-1], nếu M>a[n-1] thì đó là số 0 đầu tiên, ngược lại là số 1 đầu
tiên. Nếu số đầu tiên là 0 thì trạng thái mới cần tìm gồm n-1 phần tử có số hiệu vẫn là M,
ngược lại có số hiệu là M-a[n-1].
Bài làm rất đơn giản như sau:
a[0]:=1;a[1]:=1;
For I:=2 to n do a[I]:=a[I-1]+a[I-2];(Tạo các số fibo)
For I:=n downto 1 do
Begin
If M>a[I-1] then
Begin
Write(‘1’);
M:=M-a[I-1];
End
Else write(‘0’);
End;
Thật là đơn giản phải không các bạn? Vấn đề ở chỗ phải tìm ra đặc điểm để xác định phần
tử đầu tiên và đặc điểm của các phần tử còn lại. Hi vọng các bạn sẽ nhận thêm được kinh
nghiệm nào đó từ bài viết này.