Lập trình động là gì?


33

Xin lỗi trước nếu câu hỏi này nghe có vẻ ngu ngốc ...

Theo tôi biết, việc xây dựng một thuật toán sử dụng lập trình động hoạt động theo cách này:

  1. diễn đạt vấn đề như một mối quan hệ tái phát;
  2. thực hiện mối quan hệ lặp lại hoặc thông qua ghi nhớ hoặc thông qua cách tiếp cận từ dưới lên.

Theo như tôi biết, tôi đã nói mọi thứ về lập trình động. Ý tôi là: lập trình động không đưa ra các công cụ / quy tắc / phương pháp / định lý để thể hiện quan hệ lặp lại, cũng như không biến chúng thành mã.

Vậy, điều gì đặc biệt về lập trình động? Nó mang lại cho bạn điều gì, ngoài một phương pháp mơ hồ để tiếp cận một loại vấn đề nào đó?


11
Thông tin lịch sử (nhận xét này sẽ không giúp bạn, nhưng Bellman thực sự là một người dẫn đầu tốt nếu bạn muốn lý thuyết nặng về lập trình động): khi Bellman nghĩ ra cái được gọi là lập trình động, ông gọi ý tưởng là "lập trình động "Bởi vì công việc lý thuyết thuần túy sẽ không bay cùng với chủ nhân của anh ta vào thời điểm đó, vì vậy anh ta cần một cái gì đó thông dụng hơn mà không thể được sử dụng theo cách miệt thị .
G. Bạch

3
Theo tôi biết đó chính xác là hai điểm bạn đề cập. Nó trở nên đặc biệt khi nó tránh được sự bùng nổ theo cấp số nhân vì các bài toán con chồng chéo. Đó là tất cả. À, nhân tiện, giáo sư của tôi thích "mô hình thuật toán" hơn "phương pháp mơ hồ".
Hendrik ngày

"Lập trình động" dường như chủ yếu là một từ thông dụng (từ đó đã mất đi tiếng vang). Điều đó không có nghĩa là nó không hữu ích trong khóa học.
dùng253751

3
Không xứng đáng với câu trả lời, nhưng với tôi lập trình động chắc chắn là "thứ bạn sử dụng khi bạn cố gắng giải quyết vấn đề một cách đệ quy, nhưng cuối cùng bạn lại lãng phí thời gian để xem lại các bài toán con tương tự."
hobbs

@hobbs: Chính xác, nhưng kỹ năng là tìm ra cách lãng phí thời gian ban đầu đó;)
j_random_hacker

Câu trả lời:


27

Lập trình động cung cấp cho bạn một cách để suy nghĩ về thiết kế thuật toán. Điều này thường rất hữu ích.

Phương pháp ghi nhớ và từ dưới lên cung cấp cho bạn một quy tắc / phương pháp để biến quan hệ lặp lại thành mã. Ghi nhớ là một ý tưởng tương đối đơn giản, nhưng những ý tưởng tốt nhất thường là!

Lập trình động cung cấp cho bạn một cách có cấu trúc để suy nghĩ về thời gian chạy thuật toán của bạn. Thời gian chạy về cơ bản được xác định bởi hai số: số lượng các bài toán con bạn phải giải và thời gian cần thiết để giải từng bài toán con. Điều này cung cấp một cách dễ dàng thuận tiện để suy nghĩ về vấn đề thiết kế thuật toán. Khi bạn có mối quan hệ lặp lại của ứng viên, bạn có thể xem xét nó và nhanh chóng hiểu được thời gian chạy có thể là gì (ví dụ, bạn thường có thể nhanh chóng cho biết sẽ có bao nhiêu bài toán con, giới hạn thấp hơn về thời gian hoạt động, nếu có nhiều bài toán con bạn phải giải theo cấp số nhân, thì việc tái phát có lẽ sẽ không phải là một cách tiếp cận tốt). Điều này cũng giúp bạn loại trừ phân rã ứng viên phụ. Chẳng hạn, nếu chúng ta có một chuỗi , xác định một bài toán con bằng tiền tố S [ 1 .. i ] hoặc hậu tố S [ j . . n ] hoặc chuỗi con S [ i . . j ] có thể hợp lý (số lượng các bài toán con là đa thức trong n ), nhưng việc xác định một bài toán con bằng một phần tiếp theo của S không có khả năng là một cách tiếp cận tốt (số lượng các bài toán con là theo cấp số nhân trong n ). Điều này cho phép bạn cắt tỉa "không gian tìm kiếm" của các đợt tái phát có thể xảy ra.S[1..n]S[1..i]S[j..n]S[i..j]nSn

Lập trình động cung cấp cho bạn một cách tiếp cận có cấu trúc để tìm kiếm các mối quan hệ tái phát của ứng viên. Theo kinh nghiệm, phương pháp này thường hiệu quả. Cụ thể, có một số phương pháp phỏng đoán / mẫu chung mà bạn có thể nhận ra đối với các cách phổ biến để xác định các bài toán con, tùy thuộc vào loại đầu vào. Ví dụ:

  • Nếu đầu vào là một số nguyên dương , một ứng cử viên cách để xác định một bài toán con là bằng cách thay thế n với một số nguyên nhỏ hơn n ' (st 0 n 'n ).nnn0nn

  • Nếu đầu vào là một chuỗi , một số cách ứng cử viên để xác định một bài toán con bao gồm: thay S [ 1 .. n ] bằng tiền tố S [ 1 .. i ] ; thay S [ 1 .. n ] bằng hậu tố S [ j . . n ] ; thay S [ 1 .. n ] bằng chuỗi con S [ i . . j ]S[1..n]S[1..n]S[1..i]S[1..n]S[j..n]S[1..n]S[i..j]. (Ở đây, bài toán con được xác định bởi sự lựa chọn của .)i,j

  • Nếu đầu vào là một danh sách , hãy làm tương tự như bạn làm cho một chuỗi.

  • Nếu đầu vào là cây , một cách ứng cử viên để xác định một bài toán con là thay thế T bằng bất kỳ cây con nào của T (nghĩa là chọn một nút x và thay thế T bằng cây con gốc ở x ; biểu đồ con được xác định bởi sự lựa chọn của x ).TTTxTxx

  • Nếu đầu vào là một cặp , thì hãy xem xét đệ quy loại x và loại y để xác định cách chọn một bài toán con cho mỗi bài. Nói cách khác, một ứng cử viên cách để xác định một bài toán con là để thay thế ( x , y ) bởi ( x ' , y ' ) nơi x ' là một bài toán con cho xy ' là một bài toán con cho y . (Bạn cũng có thể xem xét các biểu tượng con của biểu mẫu ( x , y(x,y)xy(x,y)(x,y)xxyy Hoặc ( x ' , y ) .)(x,y)(x,y)

Và như vậy. Điều này cung cấp cho bạn một heuristic rất hữu ích: chỉ cần nhìn vào chữ ký loại của phương thức, bạn có thể đưa ra một danh sách các cách ứng cử viên để xác định các bài toán con. Nói cách khác, chỉ bằng cách xem báo cáo vấn đề - chỉ nhìn vào các loại đầu vào - bạn có thể đưa ra một số cách ứng cử viên để xác định một bài toán con.

Điều này thường rất hữu ích. Nó không cho bạn biết mối quan hệ lặp lại là gì, nhưng khi bạn có một lựa chọn cụ thể về cách xác định bài toán con, thường thì không quá khó để tìm ra mối quan hệ lặp lại tương ứng. Vì vậy, nó thường biến thiết kế của một thuật toán lập trình động thành một trải nghiệm có cấu trúc. Bạn viết ra giấy phế liệu một danh sách các cách ứng cử viên để xác định các bài toán con (sử dụng phương pháp phỏng đoán ở trên). Sau đó, đối với mỗi ứng cử viên, bạn cố gắng viết ra một mối quan hệ lặp lại và đánh giá thời gian chạy của nó bằng cách đếm số lượng các bài toán con và thời gian dành cho mỗi bài toán con. Sau khi thử từng ứng cử viên, bạn giữ một ứng cử viên tốt nhất mà bạn có thể tìm thấy. Cung cấp một số cấu trúc cho quy trình thiết kế thuật toán là một trợ giúp lớn, vì nếu không thì thiết kế thuật toán có thể đáng sợ (có '


Vì vậy, bạn xác nhận rằng lập trình động không cung cấp "quy trình" cụ thể để tuân theo. Đó chỉ là "một cách nghĩ", như bạn đã nói. Lưu ý rằng tôi không tranh luận rằng DP là vô dụng (ngược lại!), Tôi chỉ đang cố gắng để hiểu nếu có điều gì đó mà tôi đang thiếu hoặc nếu tôi chỉ nên thực hành nhiều hơn.
hey hey

@heyhey, tốt, vâng ... và không. Xem câu trả lời sửa đổi của tôi để biết thêm chi tiết. Nó không phải là viên đạn bạc, nhưng nó cung cấp một số quy trình bán cụ thể thường hữu ích (không được đảm bảo để hoạt động, nhưng thường không chứng minh được sự hữu ích).
DW

Cảm ơn nhiều! Bằng cách thực hành, tôi ngày càng quen thuộc hơn với một số "quy trình bán cụ thể" mà bạn đang mô tả.
hey hey

"Nếu có nhiều vấn đề con theo cấp số nhân mà bạn phải giải quyết, thì việc tái phát có lẽ sẽ không phải là một cách tiếp cận tốt". Đối với nhiều vấn đề không có thuật toán thời gian đa thức đã biết. Tại sao đây phải là một tiêu chí để sử dụng DP?
Chiel ten Brinke

@Chiel, đó không phải là một tiêu chí để sử dụng DP. Nếu bạn gặp vấn đề trong đó bạn sẽ hài lòng với thuật toán theo thời gian theo cấp số nhân, thì bạn có thể bỏ qua nhận xét đặc biệt đó. Đó chỉ là một ví dụ để cố gắng minh họa điểm chung mà tôi đang đưa ra - không phải là điều bạn nên quá nghiêm túc hoặc diễn giải như một quy tắc khó và nhanh.
DW

9

Sự hiểu biết của bạn về lập trình động là chính xác ( afaik ), và câu hỏi của bạn là hợp lý.

Tôi nghĩ rằng không gian thiết kế bổ sung mà chúng ta có được từ loại tái phát mà chúng ta gọi là "lập trình động" có thể được nhìn thấy rõ nhất so với các sơ đồ khác của phương pháp đệ quy.

Hãy giả vờ đầu vào của chúng tôi là mảng để làm nổi bật các khái niệm.A[1..n]

  1. Cách tiếp cận quy nạp

    Ở đây, ý tưởng là làm cho vấn đề của bạn nhỏ hơn, giải quyết phiên bản nhỏ hơn và tìm ra giải pháp cho bản gốc. Sơ đồ,

    f(A)=g(f(A[1..nc]),A)

    với hàm / thuật toán dịch giải pháp.g

    Ví dụ: Tìm siêu sao trong thời gian tuyến tính

  2. Phân chia & chinh phục

    Phân vùng đầu vào thành nhiều phần nhỏ hơn, giải quyết vấn đề cho từng phần và kết hợp. Sơ đồ (cho hai phần),

    .f(A)=g(f(A[1..c]),f(A[c+1..n]),A)

    Ví dụ: Hợp nhất- / Quicksort, Khoảng cách cặp ngắn nhất trong mặt phẳng

  3. Lập trình năng động

    Xem xét tất cả các cách phân vùng vấn đề thành các vấn đề nhỏ hơn và chọn cách tốt nhất. Sơ đồ (cho hai phần),

    .f(A)=best{g(f(A[1..c]),f(A[c+1..n]))|1cn1}

    Ví dụ: Chỉnh sửa khoảng cách, Vấn đề thay đổi

    Lưu ý bên quan trọng: lập trình động không phải là vũ phu ! Việc áp dụng trong từng bước làm giảm đáng kể không gian tìm kiếm.best

Theo một nghĩa nào đó, bạn biết ngày càng ít đi tĩnh từ trên xuống dưới và phải đưa ra quyết định ngày càng nhiều hơn một cách linh hoạt.

Bài học từ việc học về lập trình động là bạn có thể thử tất cả các phân vùng có thể (tốt, nó cần cho tính chính xác) bởi vì nó vẫn có thể hiệu quả bằng cách sử dụng ghi nhớ.


"Lập trình động được cắt tỉa" (khi áp dụng) chứng tỏ rằng việc thử tất cả các khả năng là KHÔNG cần thiết cho tính chính xác.
Ben Voigt

@BenVoigt Tất nhiên rồi. Tôi vẫn cố tình mơ hồ về "tất cả các cách để phân vùng" nghĩa là gì; bạn muốn loại trừ càng nhiều càng tốt, tất nhiên! (Tuy nhiên, ngay cả khi bạn thử tất cả các cách phân vùng bạn không nhận được sức mạnh vũ phu vì bạn chỉ bao giờ điều tra kết hợp tối ưu các giải pháp cho bài toán, trong khi brute-force sẽ điều tra tất cả các kết hợp của tất cả các giải pháp.)
Raphael


5

Lập trình động cho phép bạn trao đổi bộ nhớ trong thời gian tính toán. Hãy xem xét ví dụ cổ điển, Fibonacci.

Fib(n)=Fib(n1)+Fib(n2)O(2n)Fib()n

Fib(2)Fib(3)Fib(4)O(n)

mm


1
Bạn chỉ nói về phần ghi nhớ, mà bỏ lỡ điểm của câu hỏi.
Raphael

1
"Lập trình động cho phép bạn trao đổi bộ nhớ trong thời gian tính toán" không phải là điều tôi nghe được khi học đại học và đó là một cách tuyệt vời để xem xét chủ đề này. Đây là một câu trả lời trực quan với một ví dụ ngắn gọn.
trueshot

@trueshot: Ngoại trừ việc đôi khi lập trình động (và đặc biệt, "Lập trình động được cắt tỉa") có thể giảm cả yêu cầu về thời gian và không gian.
Ben Voigt

@Ben Tôi không nói đó là giao dịch một-một. Bạn có thể cắt tỉa một cây tái phát là tốt. Tôi khẳng định rằng tôi đã trả lời câu hỏi, đó là "DP lấy gì cho chúng tôi?" Nó cho chúng ta các thuật toán nhanh hơn bằng cách giao dịch không gian cho thời gian. Tôi đồng ý rằng câu trả lời được chấp nhận là kỹ lưỡng hơn, nhưng điều này cũng hợp lệ.
Kittsil

2

Đây là một cách khác để diễn đạt những gì lập trình động mang lại cho bạn. Lập trình động thu gọn một số lượng lớn các giải pháp ứng viên thành một số đa thức các lớp tương đương, sao cho các giải pháp ứng viên trong mỗi lớp không thể phân biệt theo một nghĩa nào đó.

kAn2nO(n2)f(i,)i

f(i,)=j<i such thatA[j]<A[i]f(j,1)
f(i,1)=1 for all i=1n

O(n2k).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.