Tác giả: Nguyễn Tiến Trung Kiên
Đôi lời về tác giả:
Nguyễn Tiến Trung Kiên là cựu học sinh Chuyên Tổng Hợp, với 1 HCĐ IOI năm 2014 và 1 HCB IOI năm 2015. Kiên còn nổi tiếng với blog chứa code nhiều thuật toán và series Free contest.
Nhân ma trận thật sự hữu dụng. Có nhiều bài toán khi nhỏ, ta dùng DP (Dynamic Programming - Quy Hoạch Động) để giải. Nhưng khi lớn (khoảng ), ta phải dùng nhân ma trận để giảm độ phức tạp. Trong quá trình code nhân ma trận, việc sinh ra ma trận gốc không phải lúc nào cũng đơn giản. Tôi đã tìm ra một phương pháp tốt để giải những bài toán này mà không cần nhân ma trận.
Khi dùng phương pháp này, ta không cần phải sinh ma trận gốc và không cần cài phép toán nhân hai ma trận và luỹ thừa ma trận . Tuy nhiên, phương pháp này chỉ dùng được trong các bài toán đếm, nghĩa là nó không thể hoàn toàn thay thế nhân ma trận.
Để ví dụ, tôi sẽ dùng bài toán sau:
Đếm xem có bao nhiêu dãy ngoặc đúng độ dài mà độ sâu không quá . .
Ví dụ, khi và , thì ()()
là dãy ngoặc đúng duy nhất thoả mãn, còn (())
, ((()
, và ))((
thì không thoả mãn.
Bài toán này có thể giải bằng phương pháp Quy hoạch động như sau:
sum
: Khi gặp (
ta tăng sum
lên 1 đơn vị. Khi gặp )
ta giảm sum
đi 1 đơn vị. 1 dãy ngoặc là dãy ngoặc đúng nếu thỏa mãn 2 điều kiện sau:
sum
nhỏ hơn 0sum
bằng 0.sum
trong quá trình trên.Từ nhận xét trên, ta tìm ra công thức trong đó là số dãy mà phần còn lại cần xây dựng có độ dài và tổng hiện tại (sum) là . Mục tiêu của chúng ta là tính . Tất nhiên độ phức tạp của hàm là quá lớn.
Bây giờ, gọi là số dãy độ dài bắt đầu từ tổng và kết thúc tại tổng .
Xét các trường hợp:
Ngoài ra, chú ý đến trường hợp sau: nếu hoặc thì trả về 0.
Mục tiêu của ta là tính .
Độ phức tạp của phương pháp này là , nhanh bằng với nhân ma trận. Chú ý rằng ta chỉ có trạng thái, không phải là . Chẳng hạn khi , các giá trị của sẽ nằm trong tập sau: . Thế nên chỉ nhận khoảng giá trị trong tập hợp đó. Ta có thể dùng độ sâu của hàm để đại diện cho .
function f(n, h, h_0, Depth):
if h < 0 or h > L:
return 0
if n == 0:
return (h==h_0 ? 1 : 0)
if Saved[h][h_0][Depth]:
return Value[h][h_0][Depth]
if n is even:
Result = 0
for i in 0..L:
Result += f(n/2, h, i, Depth+1) * f(n/2, i, h_0, Depth+1)
else:
Result = f(n-1, h-1, h_0, Depth+1) + f(n-1, h+1, h_0, Depth+1)
Saved[h][h_0][Depth] = true
Value[h][h_0][Depth] = Result
input n, L
output f(n, 0, 0, 0)
Với trường hợp được tính từ
Có loại hoa . 4 trong loại hoa này là g
(gerbera), o
(orchid), a
(azalea) và h
(hydrangea). Ta dùng các loại hoa này để tạo một dãy chậu hoa . Có vài điều kiện được đặt ra như sau:
h
phải được đặt giữa một a
và một o
g
bất kì, phải có ít nhất chậu hoa loại khác .Giả sử có 5 loại hoa (): a
, h
, o
, g
, và b
(begonias).
Với , có 2906 dãy chậu đúng, 5 trong số đó là aoaaoo
, ahohag
, gbbbgo
, gbbbog
, bbbbbb
.
Những dãy sau đây không hợp lệ: ohoaha
(đoạn aha
không hợp lệ vì bên cạnh h
phải có một o
và một a
), gogbao
(giữa hai g
phải có ít nhất 3 hoa khác), ahoaha
(chậu h
cuối cùng không kề với một a
và một o
).
Không khó lắm để tìm ra công thức quy hoạch động: trả về số dãy chậu đúng. Trạng thái , , được mô tả như sau:
g
, nói cách khác tất cả các chậu hoa trong khoảng đến không phải là g
.a
hoặc o
, nghĩa là h
, nghĩa là các loại hoa còn lại (bao gồm g
và loại hoa khác).Hàm quy hoạch động trên có thể chạy với .
Bây giờ tôi sẽ nói cách giải đúng. Gọi nghĩa là: ta xuất phát từ trạng thái , có bao nhiêu cách đi đến trạng thái .
long f(int n, int x, int Just) {
if (x>=p) x=p;
if (Just==2) {
if (n==0) return 0;
return f(n-1, x+1, 1);
} else {
if (n==0) return 1;
if (F[x][Just].count(n)) return F[x][Just][n];
long Sum = f(n-1, x+1, 1) * 2;
if (Just==1) Sum += f(n-1, x+1, 2);
if (x>=p) Sum += f(n-1, 0, 0);
Sum += f(n-1, x+1, 0) * (t-4);
return F[x][Just][n] = Sum % M;
}
}
cout << f(n, ::p, 0) << endl;
Ta có các trường hợp:
Chú ý tại trường hợp , việc không có nghĩa đó là kết thúc của một dãy. Vì ta chia dãy thành các phần nhỏ hơn, chỉ có nghĩa là kết thúc của một phần nhỏ. Vì thế ta sẽ thêm một biến thuộc kiểu boolean. Khi , , ngược lại, tức là , .
map<int, int> G[21][3][21][3][2];
#define C p][Just][p0][Just0][Stop
long g(int n, int p, int Just, int p0, int Just0, bool Stop) {
if (p>=::p) p=::p;
if (n%2==1 || n==0) {
if (Just==2) {
if (n==0) return Stop ? 0 : p==p0 && Just==Just0;
return g(n-1, p+1, 1, p0, Just0, Stop);
} else {
if (n==0) return Stop ? 1 : p==p0 && Just==Just0;
if (G[C].count(n)) return G[C][n];
long Sum = g(n-1, p+1, 1, p0, Just0, Stop) * 2;
if (Just==1) Sum += g(n-1, p+1, 2, p0, Just0, Stop);
if (p>=::p) Sum += g(n-1, 0, 0, p0, Just0, Stop);
Sum += g(n-1, p+1, 0, p0, Just0, Stop) * (t-4);
return G[C][n] = Sum % M;
}
} else {
if (G[C].count(n)) return G[C][n];
long Sum = 0;
for (int i=0; i<=::p; i++)
for (int k=0; k<=2; k++) {
long G1 = g(n/2, p, Just, i, k, false);
long G2 = g(n/2, i, k, p0, Just0, Stop);
Sum += G1*G2;
}
return G[C][n] = Sum % M;
}
}
cout << g(n, ::p, 0, rand()%21, rand()%3, true) << endl;
Chú ý ở code trên, ::p
và p
là khác nhau. ::p
là biến p
toàn cục, tức là p
được nhập từ input. Còn p
là tham số ở trong hàm g
. Rand()%21
và rand()%3
là hai số mà ta có thể bỏ qua giá trị của chúng (khi nào mà Stop=true
thì p0
và Just0
không có ý nghĩa).
Độ phức tạp ở code trên là . Thực tế, ta có thể không dùng map
, bằng cách thêm một tham số là Depth
đại diện cho độ sâu của hàm quy hoạch động. Khi đó, độ phức tạp mất đi một thừa số , giảm xuống còn . Code trên tôi dùng map
cho nó dễ hiểu.
Bây giờ, chúng ta sẽ tính số fibonacci thứ (trong một modulo nào đó). Chắc hẳn là bạn đã biết cách dùng nhân ma trận, nó khá dễ. Tuy nhiên, bây giờ chúng ta sẽ thử giải bằng cách không dùng nhân ma trận. Xem bài toán sau:
Bạn đang đứng ở điểm trên trục Ox. Mỗi bước, bạn có thể di chuyển sang trái 1 hoặc 2 bước. Có bao nhiêu cách để bạn đi tới vị trí 0?
Không khó để nhận ra , trong đó và . Thế nên, là số fibonacci thứ .
Có hai trường hợp:
, ta có hai lựa chọn:
, bây giờ ta chia dãy thành hai đoạn và (đoạn thứ nhất độ dài , đoạn thứ hai dài ), ta lại có hai lựa chọn:
Lúc này độ phức tạp là . Bởi vì với mỗi độ sâu, chỉ có tối đa 4 giá trị .
map<long, long> F;
F[0]=F[1]=1;
long f(long n) {
if (F.count(n)) return F[n];
long k=n/2;
if (n%2==0) { // n=2*k
return F[n] = (f(k) * f(k) + f(k-1) * f(k-1)) % M;
} else { // n=2*k+1
return F[n] = (f(k) * f(k+1) + f(k-1) * f(k)) % M;
}
}