rev4: Một nhận xét rất hùng hồn của người dùng Sammaron đã lưu ý rằng, có lẽ, câu trả lời này trước đây bị nhầm lẫn từ trên xuống và từ dưới lên. Mặc dù ban đầu câu trả lời này (rev3) và các câu trả lời khác nói rằng "từ dưới lên là ghi nhớ" ("giả sử các bài toán con"), nó có thể là nghịch đảo (nghĩa là "từ trên xuống" có thể là "giả sử các bài toán con" và " từ dưới lên "có thể là" soạn các bài toán con "). Trước đây, tôi đã đọc về ghi nhớ là một loại lập trình động khác với các kiểu lập trình động. Tôi đã trích dẫn quan điểm đó mặc dù không đăng ký nó. Tôi đã viết lại câu trả lời này là bất khả tri về thuật ngữ cho đến khi các tài liệu tham khảo thích hợp có thể được tìm thấy trong tài liệu. Tôi cũng đã chuyển đổi câu trả lời này sang wiki cộng đồng. Hãy thích các nguồn học tập. Danh sách tài liệu tham khảo:} {Văn học: 5 }
Tóm tắt
Lập trình động là tất cả về việc sắp xếp các tính toán của bạn theo cách tránh tính toán lại công việc trùng lặp. Bạn có một vấn đề chính (gốc của cây các bài toán con) và các bài toán con (bài phụ). Các bài toán con thường lặp lại và chồng chéo .
Ví dụ, hãy xem xét ví dụ yêu thích của bạn về Fibonnaci. Đây là cây đầy đủ của các bài toán con, nếu chúng ta thực hiện một cuộc gọi đệ quy ngây thơ:
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(Trong một số vấn đề hiếm gặp khác, cây này có thể là vô hạn trong một số nhánh, đại diện cho sự không chấm dứt, và do đó, đáy của cây có thể vô cùng lớn. Hơn nữa, trong một số vấn đề bạn có thể không biết cây đầy đủ trông như thế nào trước thời gian. Do đó, bạn có thể cần một chiến lược / thuật toán để quyết định những bài toán con nào sẽ tiết lộ.)
Ghi nhớ, lập bảng
Có ít nhất hai kỹ thuật chính của lập trình động không loại trừ lẫn nhau:
Ghi nhớ - Đây là một cách tiếp cận laissez-faire: Bạn cho rằng bạn đã tính toán tất cả các bài toán con và bạn không biết thứ tự đánh giá tối ưu là gì. Thông thường, bạn sẽ thực hiện một cuộc gọi đệ quy (hoặc một số lần lặp tương đương) từ gốc và hy vọng bạn sẽ tiến gần đến thứ tự đánh giá tối ưu hoặc có được bằng chứng rằng bạn sẽ giúp bạn đạt được thứ tự đánh giá tối ưu. Bạn sẽ đảm bảo rằng cuộc gọi đệ quy không bao giờ tính toán lại một bài toán con vì bạn lưu trữ kết quả và do đó, các cây con trùng lặp không được tính toán lại.
- Ví dụ: Nếu bạn đang tính toán chuỗi Fibonacci
fib(100)
, bạn sẽ chỉ gọi nó và nó sẽ gọi fib(100)=fib(99)+fib(98)
, sẽ gọi fib(99)=fib(98)+fib(97)
, ... vv ..., sẽ gọi fib(2)=fib(1)+fib(0)=1+0=1
. Sau đó, nó cuối cùng sẽ giải quyết fib(3)=fib(2)+fib(1)
, nhưng nó không cần phải tính toán lại fib(2)
, bởi vì chúng tôi đã lưu trữ nó.
- Điều này bắt đầu ở ngọn cây và đánh giá các bài toán con từ lá / cây trở lại về phía gốc.
Lập bảng - Bạn cũng có thể nghĩ về lập trình động như một thuật toán "điền vào bảng" (mặc dù thường là đa chiều, 'bảng' này có thể có hình học phi Euclide trong các trường hợp rất hiếm *). Điều này giống như ghi nhớ nhưng tích cực hơn và bao gồm một bước bổ sung: Bạn phải chọn trước thời hạn, thứ tự chính xác mà bạn sẽ thực hiện tính toán của mình. Điều này không có nghĩa là thứ tự phải tĩnh, nhưng bạn có tính linh hoạt hơn nhiều so với ghi nhớ.
- Ví dụ: Nếu bạn đang thực hiện fibonacci, bạn có thể chọn để tính toán các con số theo thứ tự này:
fib(2)
, fib(3)
, fib(4)
... bộ nhớ đệm mỗi giá trị, do đó bạn có thể tính toán những cái tiếp theo dễ dàng hơn. Bạn cũng có thể nghĩ về nó như điền vào một bảng (một hình thức lưu trữ khác).
- Cá nhân tôi không nghe thấy từ 'lập bảng' nhiều, nhưng đó là một thuật ngữ rất hay. Một số người coi đây là "lập trình động".
- Trước khi chạy thuật toán, lập trình viên xem xét toàn bộ cây, sau đó viết một thuật toán để đánh giá các bài toán con theo một thứ tự cụ thể về phía gốc, thường điền vào một bảng.
- * chú thích: Đôi khi, 'bảng' không phải là một bảng hình chữ nhật có kết nối giống như lưới, mỗi giây. Thay vào đó, nó có thể có cấu trúc phức tạp hơn, chẳng hạn như cây hoặc cấu trúc dành riêng cho miền vấn đề (ví dụ: các thành phố trong khoảng cách bay trên bản đồ) hoặc thậm chí là sơ đồ lưới, trong khi, giống như lưới, không có cấu trúc kết nối từ trên xuống dưới bên trái, v.v. Ví dụ: user3290797 đã liên kết một ví dụ lập trình động về việc tìm tập hợp độc lập tối đa trong cây , tương ứng với việc điền vào chỗ trống trong cây.
(Nói chung nhất, trong mô hình "lập trình động", tôi sẽ nói rằng lập trình viên xem xét toàn bộ cây, sau đóviết một thuật toán thực hiện chiến lược đánh giá các bài toán con có thể tối ưu hóa bất kỳ thuộc tính nào bạn muốn (thường là sự kết hợp giữa độ phức tạp thời gian và độ phức tạp không gian). Chiến lược của bạn phải bắt đầu ở đâu đó, với một số bài toán con cụ thể và có lẽ có thể tự điều chỉnh dựa trên kết quả của những đánh giá đó. Theo nghĩa chung của "lập trình động", bạn có thể thử lưu trữ các biểu tượng con này và nói chung, cố gắng tránh xem lại các bài toán con với sự phân biệt tinh tế có lẽ là trường hợp của đồ thị trong các cấu trúc dữ liệu khác nhau. Rất thường xuyên, các cấu trúc dữ liệu này là cốt lõi của chúng như mảng hoặc bảng. Giải pháp cho các bài toán con có thể bị vứt đi nếu chúng ta không cần chúng nữa.)
[Trước đây, câu trả lời này đã đưa ra tuyên bố về thuật ngữ từ trên xuống so với từ dưới lên; rõ ràng có hai cách tiếp cận chính được gọi là Ghi nhớ và Lập bảng có thể phù hợp với các điều khoản đó (mặc dù không hoàn toàn). Thuật ngữ chung mà hầu hết mọi người sử dụng vẫn là "Lập trình động" và một số người nói "Ghi nhớ" để chỉ loại phụ cụ thể của "Lập trình động". Câu trả lời này từ chối cho biết đó là từ trên xuống và từ dưới lên cho đến khi cộng đồng có thể tìm thấy tài liệu tham khảo phù hợp trong các bài báo học thuật. Cuối cùng, điều quan trọng là phải hiểu sự khác biệt hơn là thuật ngữ.]
Ưu và nhược điểm
Dễ mã hóa
Ghi nhớ rất dễ mã hóa (nói chung bạn có thể * viết một chức năng chú thích hoặc trình bao bọc "ghi nhớ" tự động làm điều đó cho bạn), và nên là cách tiếp cận đầu tiên của bạn. Nhược điểm của việc lập bảng là bạn phải đưa ra yêu cầu.
* (điều này thực sự chỉ dễ dàng nếu bạn tự viết hàm và / hoặc mã hóa bằng ngôn ngữ lập trình không tinh khiết / không chức năng ... ví dụ: nếu ai đó đã viết một fib
hàm được biên dịch trước , nó nhất thiết phải thực hiện các cuộc gọi đệ quy cho chính nó và bạn không thể ghi nhớ một cách kỳ diệu chức năng mà không đảm bảo các cuộc gọi đệ quy đó gọi chức năng ghi nhớ mới của bạn (và không phải là chức năng không được ghi nhớ ban đầu))
Đệ quy
Lưu ý rằng cả từ trên xuống và từ dưới lên có thể được thực hiện với đệ quy hoặc điền vào bảng lặp, mặc dù nó có thể không tự nhiên.
Mối quan tâm thực tế
Với khả năng ghi nhớ, nếu cây rất sâu (ví dụ fib(10^6)
), bạn sẽ hết dung lượng ngăn xếp, bởi vì mỗi phép tính bị trì hoãn phải được đặt vào ngăn xếp và bạn sẽ có 10 ^ 6 trong số chúng.
Sự tối ưu
Cách tiếp cận có thể không tối ưu về thời gian nếu thứ tự bạn xảy ra (hoặc cố gắng) truy cập các bài toán con không tối ưu, cụ thể là nếu có nhiều hơn một cách để tính toán một bài toán con (thông thường bộ nhớ đệm sẽ giải quyết điều này, nhưng về mặt lý thuyết thì bộ nhớ đệm có thể không trong một số trường hợp kỳ lạ). Ghi nhớ thường sẽ thêm vào độ phức tạp thời gian của bạn vào độ phức tạp không gian của bạn (ví dụ: với bảng, bạn có quyền tự do hơn để loại bỏ các phép tính, như sử dụng bảng với Fib cho phép bạn sử dụng không gian O (1), nhưng ghi nhớ với Fib sử dụng O (N) không gian ngăn xếp).
Tối ưu hóa nâng cao
Nếu bạn cũng đang thực hiện một vấn đề cực kỳ phức tạp, bạn có thể không có lựa chọn nào khác ngoài việc lập bảng (hoặc ít nhất là đóng vai trò tích cực hơn trong việc điều khiển ghi nhớ nơi bạn muốn đến). Ngoài ra nếu bạn ở trong tình huống tối ưu hóa là cực kỳ quan trọng và bạn phải tối ưu hóa, việc lập bảng sẽ cho phép bạn thực hiện tối ưu hóa mà việc ghi nhớ sẽ không cho phép bạn thực hiện một cách lành mạnh. Theo ý kiến khiêm tốn của tôi, trong công nghệ phần mềm thông thường, cả hai trường hợp này đều không xuất hiện, vì vậy tôi sẽ chỉ sử dụng ghi nhớ ("một hàm lưu trữ câu trả lời của nó") trừ khi một cái gì đó (chẳng hạn như không gian ngăn xếp) làm cho việc lập bảng cần thiết ... về mặt kỹ thuật để tránh việc xả ngăn xếp, bạn có thể 1) tăng giới hạn kích thước ngăn xếp trong các ngôn ngữ cho phép hoặc 2) ăn một yếu tố liên tục của công việc phụ để ảo hóa ngăn xếp của bạn (ick),
Ví dụ phức tạp hơn
Ở đây chúng tôi liệt kê các ví dụ về mối quan tâm đặc biệt, đó không chỉ là các vấn đề DP chung, mà là phân biệt thú vị việc ghi nhớ và lập bảng. Ví dụ: một công thức có thể dễ dàng hơn nhiều so với công thức kia hoặc có thể có một tối ưu hóa về cơ bản yêu cầu lập bảng:
- thuật toán để tính toán khoảng cách chỉnh sửa [ 4 ], thú vị như một ví dụ không tầm thường của thuật toán điền vào bảng hai chiều