Lập trình động và sự tương đồng giữa phân chia và chinh phục
Như tôi thấy bây giờ tôi có thể nói rằng lập trình động là một phần mở rộng của mô hình phân chia và chinh phục .
Tôi sẽ không coi chúng là một cái gì đó hoàn toàn khác. Bởi vì cả hai đều hoạt động bằng cách chia đệ quy một vấn đề thành hai hoặc nhiều vấn đề phụ cùng loại hoặc liên quan, cho đến khi những vấn đề này trở nên đủ đơn giản để được giải quyết trực tiếp. Các giải pháp cho các vấn đề phụ sau đó được kết hợp để đưa ra giải pháp cho vấn đề ban đầu.
Vậy tại sao chúng ta vẫn có các tên mô hình khác nhau và tại sao tôi gọi lập trình động là một phần mở rộng. Đó là bởi vì phương pháp lập trình động chỉ có thể được áp dụng cho vấn đề nếu vấn đề có những hạn chế hoặc điều kiện tiên quyết nhất định . Và sau đó, lập trình động mở rộng cách tiếp cận phân chia và chinh phục bằng kỹ thuật ghi nhớ hoặc lập bảng .
Hãy đi từng bước một
Điều kiện tiên quyết / hạn chế lập trình động
Như chúng ta vừa phát hiện, có hai thuộc tính chính phân chia và chinh phục vấn đề phải có để áp dụng lập trình động:
Cấu trúc tối ưu - giải pháp tối ưu có thể được xây dựng từ các giải pháp tối ưu của các bài toán con của nó
Các vấn đề phụ chồng chéo - vấn đề có thể được chia thành các bài toán con được sử dụng lại nhiều lần hoặc thuật toán đệ quy cho bài toán giải quyết cùng một bài toán con thay vì luôn tạo ra các bài toán con mới
Một khi hai điều kiện này được đáp ứng, chúng ta có thể nói rằng vấn đề phân chia và chinh phục này có thể được giải quyết bằng cách sử dụng phương pháp lập trình động.
Phần mở rộng lập trình động cho phân chia và chinh phục
Phương pháp lập trình động mở rộng cách tiếp cận phân chia và chinh phục bằng hai kỹ thuật ( ghi nhớ và lập bảng ) mà cả hai đều có mục đích lưu trữ và sử dụng lại các giải pháp cho các vấn đề phụ có thể cải thiện đáng kể hiệu năng. Ví dụ, việc thực hiện đệ quy ngây thơ của hàm Fibonacci có độ phức tạp về thời gian trong O(2^n)
đó giải pháp DP làm tương tự chỉ với O(n)
thời gian.
Ghi nhớ (điền vào bộ đệm từ trên xuống) đề cập đến kỹ thuật lưu trữ và sử dụng lại các kết quả được tính toán trước đó. Do đó, fib
chức năng ghi nhớ sẽ trông như thế này:
memFib(n) {
if (mem[n] is undefined)
if (n < 2) result = n
else result = memFib(n-2) + memFib(n-1)
mem[n] = result
return mem[n]
}
Tabulation (điền vào bộ đệm từ dưới lên) là tương tự nhưng tập trung vào việc điền vào các mục của bộ đệm. Tính toán các giá trị trong bộ đệm được thực hiện lặp đi lặp lại dễ dàng nhất. Phiên bản lập bảng của fib
sẽ trông như thế này:
tabFib(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
Bạn có thể đọc thêm về ghi nhớ và so sánh bảng ở đây .
Ý tưởng chính bạn nên nắm bắt ở đây là bởi vì vấn đề phân chia và chinh phục của chúng ta có các vấn đề phụ chồng chéo, việc lưu bộ đệm của các giải pháp cho vấn đề phụ trở nên khả thi và do đó ghi nhớ / lập bảng bước lên hiện trường.
Vì vậy, sự khác biệt giữa DP và DC sau tất cả
Vì hiện tại chúng tôi đã quen thuộc với các điều kiện tiên quyết của DP và các phương pháp của nó, chúng tôi sẵn sàng đưa tất cả những gì được đề cập ở trên vào một bức tranh.
Nếu bạn muốn xem các ví dụ về mã, bạn có thể xem phần giải thích chi tiết hơn ở đây nơi bạn sẽ tìm thấy hai ví dụ thuật toán: Tìm kiếm nhị phân và Khoảng cách chỉnh sửa tối thiểu (Khoảng cách Levenshtein) minh họa sự khác biệt giữa DP và DC.