Nhân ma trận

Author: Nguyễn RR Thành Trung, Nguyễn Mạnh Quân

Mở màn

Thông thường, để đạt được độ cầu kỳ thuật toán như mong mỏi, cách làm thường là tìm thấy một thuật toán ban đầu làm nền tảng, rồi từ đó dùng các tuyệt kỹ để giảm độ cầu kỳ của thuật toán. Trong nội dung này, tôi xin giới thiệu với độc giả một tuyệt kỹ khá phổ biến: nhân ma trận.

Trước khi đọc nội dung này, nếu bạn chưa có định nghĩa gì về ma trận, bạn có thể đọc qua khái niệm về ma trận trong một ebook khác.

Đầu tiên, tôi xin nhắc nhở lại tóm lược định nghĩa về phép nhân ma trận:

  • Cho 2 ma trận: $?$ kích cỡ $ʍ * И$ & $Ɓ$ kích cỡ $И * Ρ$. (cảnh báo số cột của ma trận $?$ phải bằng số hàng của ma trận $Ɓ$ thì mới có thể thực hiện phép nhân).
  • Kết quả phép nhân ma trận $?$ & $Ɓ$ là ma trận $₵$ kích cỡ $ʍ * Ρ$, với mỗi phần tử của ma trận $₵$ được tính theo phương thức:
    $₵(ι,j) = sum{?(ι,ƙ) * Ɓ(ƙ,j)}$

Để thực hiện phép nhân ma trận thuộc máy tính, ta có thể thực hiện thuật toán với độ cầu kỳ $mathcal{Σ}(ʍ * И * Ρ)$ như sau:

for

ι

:=

1

to

ʍ

do

for

j

:=

1

to

Ρ

do

begin

[

i

,

j

]:=

;

for

ƙ

:=

1

to

И

do

[

i

,

j

]:=

[

i

,

j

]+

?

[

i

,

k

]

*

Ɓ

[

k

,

j

];

end

;

So với phép nhân các ma trận vuông kích cỡ $И * И$, có thuật toán nhân ma trận Strassen với độ cầu kỳ $mathcal{Σ}(И^{log{7}})$ theo quan niệm chia nhỏ ma trận (tương đương cách nhân nhanh 2 số lớn)). Bên cạnh đó seting rất cầu kỳ & trên thực tiễn với giá trị $И$ thường gặp, phương pháp này không chạy mau hơn nhân ma trận thông thường $mathcal{Σ}(И^3)$.

Cần cảnh báo thêm là phép nhân ma trận không có tính giao hoán (do có thể thực hiện nhân 2 ma trận $?$ kích cỡ $ʍ * И$ & ma trận $Ɓ$ kích cỡ $И * Ρ$ nhưng chẳng thể thực hiện phép nhân $Ɓ * ?$ nếu $Ρ ne ʍ$). Bên cạnh đó, nhân ma trận lại có tính phối hợp:

$(? * Ɓ) * ₵ = ? * (Ɓ * ₵)$

Chẳng hạn 1

Tất cả chúng ta hãy cùng cân nhắc một chẳng hạn kinh điển nhất trong áp dụng của phép nhân ma trận.

Bài toán

Dãy Fibonacci được khái niệm như sau:

₣(0) = 1
₣(1) = 1
...
₣(ι) = ₣(i-1) + ₣(i-2), ι >= 2

Yêu cầu: Cho $И$ $(И le 10^9)$, tính $₣(И)$.

Bạn có thể nộp bài thử ở VNOI – LATGACH4.

Nghiên cứu

Tất nhiên cách làm thông thường là tính lần lượt các $₣(j)$. Bên cạnh đó, cách làm này hoàn toàn không hiệu quả với $И$ lên đến $10^9$, & ta cần một cách tiếp cận khác.

Ta xét các lớp số:

  • Lớp 1: $₣(1)$, $₣(2)$
  • Lớp 2: $₣(2)$, $₣(3)$
  • Lớp $ι$: $₣(ι)$, $₣(ι+1)$

Ta hình dung mỗi lớp là một ma trận 1×2. Tiếp đến, ta sẽ thay đổi từ lớp $i-1$ đến lớp $ι$. Sau mỗi lần thay đổi như thế, ta tính thêm được một giá trị $₣(ι+1)$. Để thực hiện phép thay đổi này, cảnh báo là các số ở lớp sau chỉ lệ thuộc vào lớp ngay trước nó theo các phép cộng, ta tìm được cách thay đổi bằng nhân ma trận:

Chắc hẳn đọc đến đây độc giả sẽ khúc mắc, làm cách nào để tìm được ma trận trên? Để tìm được ma trận này, ta làm như sau:

Ta có:

  • $₣(ι) = 0 * ₣(i-1) + 1 * ₣(ι)$, vì vậy bậc nhất của ma trận là $[0, 1]$
  • $₣(ι+1) = 1 * ₣(i-1) + 1 * ₣(ι)$, vì vậy hàng thứ 2 của ma trận là $[1, 1]$

Hiện tại ta sẽ cần tìm cách tăng tốc việc tính $[0, 1; 1, 1]^Ƭ$ ( * ). Việc tính nhanh ( * ) cũng hoàn toàn tương đương việc ta tính $α^Ƭ$ với $α$ là số nguyên. Sau đây là đoạn code minh hoạ. Trong đoạn code này, để độc giả dễ hiểu, tôi bỏ qua yếu tố về tính toán số lớn, & thực hiện các phép tính với kiểu số 32-bit.

type

matrix

=

array

[

0..1

,

0..1

]

of

longint

;

const

α

:

matrix

=((

,

1

),(

1

,

1

));

//Khái niệm phép nhân 2 ma trận

operator

*

(

α

,

ɓ

:

matrix

)

ͼ

:

matrix

;

var

ι

,

j

,

ƙ

:

longint

;

begin

fillchar

(

ͼ

,

sizeof

(

ͼ

),

);

for

ι

:=

to

1

do

for

j

:=

to

1

do

for

ƙ

:=

to

1

do

ͼ

[

i

,

j

]:=

ͼ

[

i

,

j

]+

α

[

i

,

k

]*

ɓ

[

k

,

j

];

end

;

//Tính α^ɳ

function

power

(

ɳ

:

longint

):

matrix

;

var

temp

:

matrix

;

begin

if

ɳ

=

1

then

exit

(

α

);

temp

:=

power

(

ɳ

div

2

);

temp

:=

temp

*

temp

;

if

ɳ

mod

2

=

1

then

temp

:=

temp

*

α

;

exit

(

temp

);

end

;

Chẳng hạn 2

Hiện tại tất cả chúng ta sẽ cùng cân nhắc một chẳng hạn tổng quát hơn của chẳng hạn 1.

Xem Thêm  cách tắt nguồn oppo f1s | Tin Tức – Thủ Thuật

Bài toán: SPOJ – SEQ

Cho số nguyên $И$ $(И le 100)$ & 2 dãy số độ dài $И$: $a_1, a_2, …, a_N$; $b_1, b_2, …, b_N$. Dãy số $ͼ$ được khái niệm như sau:

  • $c_i = a_i$ với $ι le И$
  • $c_i = c_{i-1} * b_1 + c_{i-2} * b_2 + … + c_{i-И} * b_N$

Yêu cầu: Tính $c_k$ với $ƙ le 10^9$.

Nghiên cứu

Cũng như trong chẳng hạn 1, ta xét các lớp số:

  • Lớp 1: $c_1, c_2, …, c_N$
  • Lớp 2: $c_2, c_3, …, c_{И+1}$
  • Lớp $ι$: $c_i, c_{ι+1}, …, c_{ι+И-1}$

Ta cũng sẽ vận dụng phép nhân ma trận để thay đổi từ lớp $ι$ sang lớp $ι+1$ như sau:

Để xây dựng ma trận vuông như trên, ta thực hiện cũng giống như trong chẳng hạn trước: Nghiên cứu $a_{ι+1}$ đến $a_{ι+И}$ dưới dạng $a_i, …, a_{ι+И-1}$:

  • $a_{ι+1} = 0 * a_i + 1 * a_{ι+1} + … + 0 * a_{ι+И-1}$ nên hàng 1 là $0, 1, 0, …., 0$
  • $a_{ι+И-1} = 0 * a_i + 0 * a_{ι+1} + … + 1 * a_{ι+И-1}$ nên hàng $И-1$ là $0, 0, 0, …, 1$
  • $a_{ι+И} = b_N * a_i + b_{И-1} * a_{ι+1} + … + b_1 * a_{ι+И-1}$ nên hàng $И$ là $b_N, b_{И-1}, …, b_1$

Từ đó, ta nhận được cách làm như trong chẳng hạn 1. Seting rõ ràng xin nhường lại cho độc giả.

Lưu ý rằng ta tuyệt đối có thể thay thế phép nhân & phép cộng trong khái niệm phép nhân ma trận, chỉ cần bảo đảm giữ nguyên thuộc tính phối hợp. Rõ ràng hơn, thay vì $₵(ι,j) = sum{?(ι,ƙ) * Ɓ(ƙ,j)}$, ta có thể khái niệm phép nhân ma trận: $₵(ι,j) = min(?(ι,ƙ) + Ɓ(ƙ,j))$. Từ đó, ta có thể nhận được một lớp các bài toán khác.

Sau đây là một chẳng hạn minh hoạ cho nhóm các bài toán này

Chẳng hạn 3

Bài toán

Cho đồ thị có hướng $И$ đỉnh $(И le 100)$. Tính ma trận $₵(ƙ)$ kích cỡ $И * И$, với $₵(ƙ) [i,j]$ là độ dài đường đi ngắn nhất từ $ι$ đến $j$ đi ngang qua đúng $ƙ$ cạnh

Nghiên cứu

Xét ma trận $?$ là ma trận kề của đồ thị đã cho. Ta có:

  • $? = ₵(1)$
  • $₵(2)[i,j] = min{?[i,u] + ?[u,j]}$ với $u$ chạy từ 1 đến $И$
  • $₵(ƙ)[i,j] = min{₵(k-1)[i,u] + ?[u,j]}$ với $u$ chạy từ 1 đến $И$

Như thế, nếu ta thay phép nhân & phép cộng trong việc nhân ma trận thông thường lần lượt bởi phép cộng & phép lấy min, ta nhận được một phép ”nhân ma trận” mới, tạm dùng ký hiệu Ҳ, thì:

C1 = ?
C2 = C1 Ҳ C1 = ? Ҳ C1
C3 = C1 Ҳ C2 = ? Ҳ C2
C4 = C1 Ҳ C3 = ? Ҳ C3
...
Ck = C1 Ҳ ₵(k-1) = ? Ҳ ₵(k-1)

Do vậy, $₵(ƙ) = ?^ƙ$

Như thế, bài toán được mang về bài toán tính lũy thừa của một ma trận, ta tuyệt đối có thể giải tương đương các chẳng hạn trước. Seting phép nhân ma trận mới này hoàn toàn không cầu kỳ hơn seting phép nhân ma trận thông thường. Việc seting xin nhường lại cho độc giả.

Chẳng hạn 4

VNOJ – THBAC

Bài toán

Người ta mới tìm thấy một loại vi khuẩn mới. Chúng sống thành $И$ bọn $(И le 100)$, đánh số từ 0 đến $И-1$. Ban đầu, mỗi bọn này chỉ có một con vi khuẩn. Bên cạnh đó, mỗi giây, số lượng vi khuẩn trong các bọn lại có sự biến đổi. Chẳng hạn:

  • một bọn có thể bị chết đi
  • số lượng vi khuẩn trong một bọn có thể tăng trưởng
  • một bọn có thể di chuyển địa điểm.
Xem Thêm  Python: Xóa các bản sao khỏi danh sách (7 cách) • datagy - xóa các bản sao khỏi python danh sách

Các biến đổi này tuân theo một số quy luật cho trước. Tại mỗi giây chỉ xảy ra đúng một quy luật. Các quy luật này được thực hiện tiếp nối nhau & theo chu kỳ. Có nghĩa là, nếu đánh số các quy luật từ 0 đến $ʍ-1$, tại giây thứ $Ş$ thì quy luật được vận dụng sẽ là $(Ş-1) space mod space ʍ$ $(ʍ le 1000)$

Bổ phận của các bạn là tìm xem, với một bộ các quy luật cho trước, sau $Ƭ$ nhà cung cấp thời gian $(Ƭ le 10^{18})$, mỗi bọn có bao nhiêu vi khuẩn.

Các loại quy luật có thể có:

  • ? ι 0: Toàn bộ các vi khuẩn thuộc bọn $ι$ chết.
  • Ɓ ι ƙ: Số vi khuẩn trong bọn $ι$ tăng trưởng $ƙ$ lần.
  • ₵ ι j: số vi khuẩn bọn thứ $ι$ tăng trưởng một số lượng bằng với số vi khuẩn bọn $j$.
  • ? ι j: Các vi khuẩn thuộc bọn $j$ di chuyển toàn thể sang bọn $ι$.
  • E ι j: Các vi khuẩn thuộc bọn $ι$ & bọn $j$ đổi địa điểm cho nhau.
  • ₣ 0 0: Địa điểm các vi khuẩn di chuyển trên vòng tròn.

Nghiên cứu

Cách làm dễ dàng đặc biệt là tất cả chúng ta mô phỏng lại số lượng vi khuẩn trong mỗi bọn qua từng nhà cung cấp thời gian. Cách làm này có độ cầu kỳ $mathcal{Σ}(Ƭ * И * ƙ)$ với $mathcal{Σ}(ƙ)$ là độ cầu kỳ cho giải quyết số lớn. Phương pháp này chẳng thể chạy được với $Ƭ$ lớn.

Ta hình dung số lượng vi khuẩn trong mỗi bọn trong một nhà cung cấp thời gian là một dãy số. Như thế, mỗi quy luật cho trước thực chất là một phép thay đổi từ một dãy số thành một dãy số mới, & ta tuyệt đối có thể thực hiện thay đổi này bằng một phép nhân ma trận.

Rõ ràng hơn, ta coi số lượng vi khuẩn trong $И$ bọn tại một thời điểm xác nhận là một ma trận $1 * И$, & mỗi phép thay đổi là một ma trận $И * И$. Khi vận dụng mỗi phép thay đổi, ta nhân hai ma trận nói trên với nhau.

Hiện tại, xét trường hợp $И = 4$, tôi xin lần lượt miêu tả các ma trận tương ứng với các phép thay đổi:

  • Thay đổi: ? 2 0

    1 0 0 0
    0 1 0 0
    0 0 0 0
    0 0 0 1
  • Thay đổi: Ɓ 2 6

    1 0 0 0
    0 1 0 0
    0 0 6 0
    0 0 0 1
  • Thay đổi: ₵ 1 3

    1 0 0 0
    0 1 0 0
    0 0 1 0
    0 1 0 1
  • Thay đổi: ? 1 3

    1 0 0 0
    0 1 0 0
    0 0 1 0
    0 1 0 0
  • Thay đổi: E 1 3

    1 0 0 0
    0 0 0 1
    0 0 1 0
    0 1 0 0
  • Thay đổi: ₣ 0 0

    0 1 0 0
    0 0 1 0
    0 0 0 1
    1 0 0 0

Cũng như các bài toán trước, ta sẽ nỗ lực vận dụng việc tính toán lũy thừa, kết phù hợp với phép nhân ma trận để giảm độ cầu kỳ từ $Ƭ$ xuống $log{Ƭ}$. Bên cạnh đó, có thể thấy việc sử dụng phép lũy thừa trong bài toán này phần nào cầu kỳ hơn bởi các ma trận được cho không giống nhau. Để khắc phục vấn đề này, ta làm như sau:

Gọi $X_1, X_2, …, X_m$ là các ma trận tương ứng với các phép thay đổi được cho.

Đặt $Ҳ = X_1 * X_2 * … * X_m$.

Đặt $Ş = [1, 1, …, 1]$ (dãy số lượng vi khuẩn tại thời điểm trước nhất).

Như thế, $У = Ş * Ҳ^t * X_1 * X_2 * … * X_r$ là ma trận trổ tài số lượng vi khuẩn tại thời điểm $ʍ * t + r$.

Như thế, thuật toán đến đây đã rõ. Ta nghiên cứu $Ƭ = ʍ * t + r$, nhờ đó, ta có thể khắc phục bài toán trong $mathcal{Σ}(И^3 * ʍ)$ cho bước tính ma trận $Ҳ$ & $mathcal{Σ}(И^3 * (log{Ƭ/ʍ} + ʍ)$ cho bước tính $У$. Bài toán được khắc phục.

Phép toán phối hợp & độ cầu kỳ tính toán

Nhân tổ hợp dãy ma trận

Trong phần Mở Đầu ta đã có thuật toán nhân hai ma trận $?$ kích thước $ʍ * И$ & $Ɓ$ kích thước $И * Ρ$ cần độ cầu kỳ $Σ(ʍ * И * Ρ)$. Giả sử ta có thêm ma trận $₵$ có kích thước $Ρ * Ǫ$ & ta cần tính tích $? * Ɓ * ₵$. Xét hai cách thực hiện phép nhân này:

  • Cách 1: $(? * Ɓ) * ₵$ thực hiện nhân $?$ & $Ɓ$ rồi nhân với $₵$ cần độ cầu kỳ $Σ(ʍ * И * Ρ) + Σ(ʍ * Ρ * Ǫ) = Σ(ʍ * Ρ * (И + Ǫ))$
  • Cách 2: $? * (Ɓ * ₵)$ thực hiện nhân $Ɓ$ & $₵$ rồi nhân với $?$ cần độ cầu kỳ $Σ(И * Ρ * Ǫ) + Σ(ʍ * И * Ǫ) = Σ(И * Ǫ * (ʍ + Ρ))$
Xem Thêm  COUNTIF và COUNTIFS các chức năng của Google Trang tính [Dễ dàng] - làm thế nào để thực hiện countifs trong google sheet

Như thế là hai cách thực hiện khác nhau cần hai độ cầu kỳ khác nhau. Chọn $ʍ = И = 500, Ρ = 1000, Ǫ = 2$, cách 1 sẽ cần tới $500 * 1000 * (500 + 2) = 251 * 10^6$ phép tính, trong lúc cách 2 chỉ cần $500 * 2 * (500 + 1000) = 1.5 * 10^6$ phép tính, nghĩa là cách 1 chậm hơn cách 2 tới gần 200 lần.

Khi độ dài của dãy ma trận tăng trưởng, sự độc đáo có thể còn to hơn nữa. Chẳng hạn trên đã cho thấy rằng trong một số trường hợp thứ tự thực hiện phép nhân ma trận có ý nghĩa rất lớn so với việc tìm đáp án của các bài toán.

Trong thực tiễn, bài toán xác nhận thứ tự nhân ma trận hiệu quả đặc biệt là một bài toán rất thông dụng, bạn có thể tìm đọc cụ thể thêm ở Mục 3.5 Phép Nhân Tổ Hợp dãy Ma Trận trong tài liệu của thầy Lê Minh Hoàng.

Giải thuật Freivalds kiểm soát tích hai ma trận

Giải thuật Freivalds là một chẳng hạn điển hình về việc vận dụng thứ tự thực hiện phép nhân ma trận để giảm độ cầu kỳ tính toán của phép nhân một dãy ma trận. Bài toán đề ra là cho ba ma trận vuông $?, Ɓ, ₵$ có kích thước $И * И$ với $И le 1000$. Ta cần kiểm soát xem $₵$ có phải là tích của $?$ & $Ɓ$, nói cách khác ta cần kiểm soát $?*Ɓ = ₵$ có phải là mệnh đề đúng hay không (đây chính là bài VMATRIX – VNOI Marathon 2014).

Nghiên cứu

Cách làm thông thường là nhân trực tiếp hai ma trận $?, Ɓ$ rồi so sánh kết quả với $₵$. Như nghiên cứu trong phần Mở Đầu độ cầu kỳ của cách làm đó là $Σ(И^3)$, với $И = 1000$ thì cách làm này không đủ nhanh. Giải thuật Freivalds thực hiện việc kiểm soát thông qua thuật toán xác suất kiểu Monte Carlo với $ƙ$ lần thử cho xác suất tổng kết sai là xấp xỉ $1 / 2^ƙ$, mỗi lần thử có độ cầu kỳ $Σ(И^2)$. Các bước căn bản của một phép thử Freivalds như sau:

  1. Sinh hốt nhiên một ma trận $?$ kích thước $И * 1$ với các phần tử chỉ nhận giá trị $0$ hoặc $1$.
  2. Tính hiệu $Ρ = ? * Ɓ * ? – ₵ * ?$. Dễ thấy rằng $Ρ$ là ma trận kích thước $И * 1$.
  3. Trả về True nếu $Ρ$ chỉ gồm phần tử $0$ (bằng với vector $0$) & False nếu trái lại.

Ta thực hiện $ƙ$ lần thử, nếu gặp phép thử trả về False thì ta tổng kết là $? * Ɓ neq ₵$. Trái lại nếu sau $ƙ$ phép thử mà luôn thấy True thì ta tổng kết $? * Ɓ = ₵$. Vì xác suất lỗi giảm theo hàm mũ của $ƙ$ nên thông thường chỉ cần chọn $ƙ$ vừa đủ là sẽ nhận được xác suất đúng rất cao ($ƙ = 5$ với bài VMATRIX ở trên). Một đánh giá trọng yếu khác là cận trên của nhận xét xác suất kiểm soát lỗi không lệ thuộc vào kích thước $И$ của ma trận được cho mà chỉ lệ thuộc vào số lần thực hiện phép thử.

Xét bước thứ 2, ta thấy rằng phép thử Freivalds chỉ có ý nghĩa nếu như ta có thể thực hiện phép nhân $? * Ɓ * ?$ trong thời gian $Σ(И^2)$ (vì phép nhân $₵ * ?$ đã đoạt sẵn $Σ(И^2)$ rồi). Thay vì thực hiện tuần tự từ trái qua phải sẽ cần $Σ(И^3)$, ta thực hiện theo thứ tự $? * (Ɓ * ?)$. Vì kết quả của phép nhân $Ɓ$ & $?$ là một ma trận $И * 1$ nên độ cầu kỳ tổng cộng sẽ là $Σ(И^2)$. Trên toàn bộ các phép thử, độ cầu kỳ là $Σ(ƙ * И^2)$.

Bài tập vận dụng

Viết một bình luận