Lập trình động là gì? [đóng cửa]


276

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

Làm thế nào nó khác với đệ quy, ghi nhớ, vv?

Tôi đã đọc bài viết trên wikipedia về nó, nhưng tôi vẫn không thực sự hiểu nó.


1
Đây là một hướng dẫn của Michael A. Trick từ CMU mà tôi thấy đặc biệt hữu ích: mat.gsia.cmu.edu/groupes/dynamic/dynamic.html Đây chắc chắn là ngoài tất cả các tài nguyên mà những người khác đã khuyến nghị (tất cả các tài nguyên khác, đặc biệt là CLR và Kleinberg, Tardos rất tốt!). Lý do tại sao tôi thích hướng dẫn này là vì nó giới thiệu các khái niệm nâng cao khá dần. Nó là một tài liệu hơi cũ nhưng nó là một bổ sung tốt vào danh sách các tài nguyên được trình bày ở đây. Ngoài ra hãy xem trang của Steven Skiena và các bài giảng về Lập trình động: cs.sunysb.edu/~alactic/video-lectures http:
Edmon

11
Tôi luôn thấy "Lập trình động" là một thuật ngữ khó hiểu - "Động" gợi ý không tĩnh, nhưng "Lập trình tĩnh" là gì? Và "... Lập trình" mang đến tâm trí "Lập trình hướng đối tượng" và "Lập trình chức năng", cho thấy DP là một mô hình lập trình. Tôi thực sự không có một cái tên nào tốt hơn (có lẽ là "Thuật toán động"?) Nhưng thật tệ khi chúng ta bị mắc kẹt với cái này.
dimo414

3
@ dimo414 "Lập trình" ở đây liên quan nhiều hơn đến "lập trình tuyến tính" nằm trong một lớp các phương pháp tối ưu hóa toán học. Xem bài viết Tối ưu hóa toán học để biết danh sách các phương pháp lập trình toán học khác.
syockit

1
@ dimo414 "Lập trình" trong ngữ cảnh này đề cập đến một phương thức dạng bảng, không phải để viết mã máy tính. - Coreman
dùng2618142

Vấn đề giảm thiểu chi phí vé xe buýt được mô tả trong cs.stackexchange.com/questions/59797/NH được giải quyết tốt nhất trong lập trình động.
Truthadjustr

Câu trả lời:


210

Lập trình động là khi bạn sử dụng kiến ​​thức trong quá khứ để giúp giải quyết vấn đề trong tương lai dễ dàng hơn.

Một ví dụ điển hình là giải quyết chuỗi Fibonacci cho n = 1.000.002.

Đây sẽ là một quá trình rất dài, nhưng nếu tôi cho bạn kết quả với n = 1.000.000 và n = 1.000.001 thì sao? Đột nhiên vấn đề trở nên dễ quản lý hơn.

Lập trình động được sử dụng rất nhiều trong các vấn đề về chuỗi, chẳng hạn như vấn đề chỉnh sửa chuỗi. Bạn giải quyết một tập hợp con của vấn đề và sau đó sử dụng thông tin đó để giải quyết vấn đề ban đầu khó khăn hơn.

Với lập trình động, bạn thường lưu trữ kết quả của mình trong một số loại bảng. Khi bạn cần câu trả lời cho một vấn đề, bạn tham khảo bảng và xem bạn đã biết nó là gì chưa. Nếu không, bạn sử dụng dữ liệu trong bảng của mình để tạo cho mình một bước đệm hướng tới câu trả lời.

Cuốn sách Thuật toán Cormen có một chương tuyệt vời về lập trình động. VÀ nó miễn phí trên Google Sách! Kiểm tra nó ở đây.


50
Không phải bạn chỉ mô tả ghi nhớ mặc dù?
dreadwail

31
Tôi muốn nói rằng ghi nhớ là một dạng lập trình động, khi hàm / phương thức ghi nhớ là một dạng đệ quy.
Daniel Huckstep

6
Câu trả lời hay, sẽ chỉ thêm một đề cập về cấu trúc phụ tối ưu (ví dụ: mọi tập hợp con của bất kỳ đường dẫn nào dọc theo đường đi ngắn nhất từ ​​A đến B đều là đường đi ngắn nhất giữa 2 điểm cuối giả định một thước đo khoảng cách quan sát bất đẳng thức tam giác).
Shea

5
Tôi sẽ không nói "dễ dàng hơn", nhưng nhanh hơn. Một sự hiểu lầm phổ biến là dp giải quyết các vấn đề mà thuật toán ngây thơ không thể và đó không phải là trường hợp. Không phải là vấn đề chức năng mà là hiệu suất.
andandandand

6
Sử dụng ghi nhớ, các vấn đề lập trình động có thể được giải quyết theo cách từ trên xuống. tức là gọi hàm để tính giá trị cuối cùng và lần lượt hàm đó gọi nó là đệ quy để giải các bài toán con. Không có nó, các vấn đề lập trình động chỉ có thể được giải quyết theo cách từ dưới lên.
Pranav

175

Lập trình động là một kỹ thuật được sử dụng để tránh tính toán nhiều lần cùng một bài toán con trong thuật toán đệ quy.

Chúng ta hãy lấy ví dụ đơn giản về các số Fibonacci: tìm số Fibonacci thứ n được xác định bởi

F n = F n-1 + F n-2 và F 0 = 0, F 1 = 1

Đệ quy

Cách rõ ràng để làm điều này là đệ quy:

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1

    return fibonacci(n - 1) + fibonacci(n - 2)

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

  • Từ trên xuống - Ghi nhớ

Đệ quy thực hiện rất nhiều phép tính không cần thiết bởi vì một số Fibonacci đã cho sẽ được tính nhiều lần. Một cách dễ dàng để cải thiện điều này là lưu trữ kết quả:

cache = {}

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    if n in cache:
        return cache[n]

    cache[n] = fibonacci(n - 1) + fibonacci(n - 2)

    return cache[n]
  • Từ dưới lên

Một cách tốt hơn để làm điều này là loại bỏ đệ quy tất cả cùng nhau bằng cách đánh giá kết quả theo đúng thứ tự:

cache = {}

def fibonacci(n):
    cache[0] = 0
    cache[1] = 1

    for i in range(2, n + 1):
        cache[i] = cache[i - 1] +  cache[i - 2]

    return cache[n]

Chúng tôi thậm chí có thể sử dụng không gian không đổi và chỉ lưu trữ các kết quả một phần cần thiết trên đường đi:

def fibonacci(n):
  fi_minus_2 = 0
  fi_minus_1 = 1

  for i in range(2, n + 1):
      fi = fi_minus_1 + fi_minus_2
      fi_minus_1, fi_minus_2 = fi, fi_minus_1

  return fi
  • Áp dụng lập trình động như thế nào?

    1. Tìm đệ quy trong bài toán.
    2. Từ trên xuống: lưu trữ câu trả lời cho từng bài toán con trong một bảng để tránh phải tính toán lại chúng.
    3. Từ dưới lên: Tìm thứ tự đúng để đánh giá kết quả sao cho có sẵn kết quả một phần khi cần.

Lập trình động thường hoạt động cho các vấn đề có thứ tự từ trái sang phải vốn có như chuỗi, cây hoặc chuỗi số nguyên. Nếu thuật toán đệ quy ngây thơ không tính toán cùng một bài toán con nhiều lần, lập trình động sẽ không có ích.

Tôi đã thực hiện một tập hợp các vấn đề để giúp hiểu logic: https://github.com/tristanguigue/dynamic-programing


3
Đây là một câu trả lời tuyệt vời và bộ sưu tập vấn đề trên Github cũng rất hữu ích. Cảm ơn!
p4sh4

Chỉ tò mò để làm rõ mọi thứ - theo ý kiến ​​của bạn, một triển khai đệ quy sử dụng mối quan hệ lặp lại và ghi nhớ là lập trình động?
Codor

Cảm ơn đã giải thích. Có một điều kiện bị thiếu từ dưới lên: if n in cachenhư với ví dụ từ trên xuống hoặc tôi đang thiếu một cái gì đó.
DavidC

Tôi có hiểu chính xác rằng bất kỳ vòng lặp nào trong đó các giá trị được tính toán ở mỗi lần lặp được sử dụng trong các lần lặp tiếp theo là một ví dụ về lập trình động không?
Alexey

Bạn có thể đưa ra bất kỳ tài liệu tham khảo nào cho việc giải thích bạn đã đưa ra, bao gồm các trường hợp đặc biệt từ trên xuống và từ dưới lên không?
Alexey

37

Ghi nhớ là khi bạn lưu trữ các kết quả trước đó của một lệnh gọi hàm (một hàm thực luôn trả về cùng một thứ, với cùng các đầu vào). Nó không tạo ra sự khác biệt cho độ phức tạp thuật toán trước khi kết quả được lưu trữ.

Đệ quy là phương thức của một hàm gọi chính nó, thường là với một tập dữ liệu nhỏ hơn. Vì hầu hết các hàm đệ quy có thể được chuyển đổi thành các hàm lặp tương tự, điều này cũng không tạo ra sự khác biệt cho độ phức tạp thuật toán.

Lập trình động là quá trình giải quyết các vấn đề phụ dễ giải quyết hơn và xây dựng câu trả lời từ đó. Hầu hết các thuật toán DP sẽ có trong thời gian chạy giữa thuật toán Greedy (nếu tồn tại) và thuật toán hàm mũ (liệt kê tất cả các khả năng và tìm ra thuật toán tốt nhất).

  • Các thuật toán DP có thể được thực hiện với đệ quy, nhưng chúng không phải như vậy.
  • Các thuật toán DP không thể được tăng tốc bằng cách ghi nhớ, vì mỗi vấn đề phụ chỉ được giải quyết (hoặc hàm "giải quyết" được gọi) một lần.

Rất rõ ràng đặt. Tôi muốn các giảng viên thuật toán có thể giải thích điều này tốt.
Kelly S. Pháp

21

Đó là một tối ưu hóa thuật toán của bạn để giảm thời gian chạy.

Mặc dù Thuật toán Tham lam thường được gọi là ngây thơ , bởi vì nó có thể chạy nhiều lần trên cùng một bộ dữ liệu, Lập trình động tránh được cạm bẫy này thông qua sự hiểu biết sâu sắc hơn về kết quả một phần phải được lưu trữ để giúp xây dựng giải pháp cuối cùng.

Một ví dụ đơn giản là duyệt qua cây hoặc đồ thị chỉ qua các nút sẽ đóng góp cho giải pháp hoặc đưa vào bảng các giải pháp mà bạn đã tìm thấy cho đến nay để bạn có thể tránh đi qua các nút giống nhau.

Đây là một ví dụ về một vấn đề phù hợp với lập trình động, từ thẩm phán trực tuyến của UVA: Chỉnh sửa bậc thang.

Tôi sẽ tóm tắt nhanh phần quan trọng của phân tích vấn đề này, lấy từ cuốn sách Những thách thức lập trình, tôi khuyên bạn nên kiểm tra nó.

Hãy xem xét kỹ vấn đề đó, nếu chúng ta xác định hàm chi phí cho chúng ta biết hai chuỗi khởi động được bao xa, chúng ta có hai xem xét ba loại thay đổi tự nhiên:

Thay thế - thay đổi một ký tự từ mẫu "s" sang một ký tự khác trong văn bản "t", chẳng hạn như thay đổi "bắn" thành "phát hiện".

Chèn - chèn một ký tự đơn vào mẫu "s" để giúp nó khớp văn bản "t", chẳng hạn như thay đổi "trước" thành "agog".

Xóa - xóa một ký tự đơn từ mẫu "s" để giúp nó khớp văn bản "t", chẳng hạn như thay đổi "giờ" thành "của chúng tôi".

Khi chúng tôi đặt mỗi thao tác này thành một bước, chúng tôi sẽ xác định khoảng cách chỉnh sửa giữa hai chuỗi. Vậy làm thế nào để chúng ta tính toán nó?

Chúng ta có thể định nghĩa một thuật toán đệ quy bằng cách sử dụng quan sát rằng ký tự cuối cùng trong chuỗi phải được khớp, thay thế, chèn hoặc xóa. Cắt bỏ các ký tự trong thao tác chỉnh sửa cuối cùng để lại một thao tác cặp để lại một cặp chuỗi nhỏ hơn. Đặt i và j là ký tự cuối cùng của tiền tố liên quan và t, tương ứng. có ba cặp chuỗi ngắn hơn sau thao tác cuối cùng, tương ứng với chuỗi sau khi khớp / thay thế, chèn hoặc xóa. Nếu chúng ta biết chi phí chỉnh sửa ba cặp chuỗi nhỏ hơn, chúng ta có thể quyết định tùy chọn nào dẫn đến giải pháp tốt nhất và chọn tùy chọn đó cho phù hợp. Chúng ta có thể tìm hiểu chi phí này, thông qua điều tuyệt vời đó là đệ quy:

#define MATCH 0 /* enumerated type symbol for match */
#define INSERT 1 /* enumerated type symbol for insert */
#define DELETE 2 /* enumerated type symbol for delete */


int string_compare(char *s, char *t, int i, int j)

{

    int k; /* counter */
    int opt[3]; /* cost of the three options */
    int lowest_cost; /* lowest cost */
    if (i == 0) return(j * indel(’ ’));
    if (j == 0) return(i * indel(’ ’));
    opt[MATCH] = string_compare(s,t,i-1,j-1) +
      match(s[i],t[j]);
    opt[INSERT] = string_compare(s,t,i,j-1) +
      indel(t[j]);
    opt[DELETE] = string_compare(s,t,i-1,j) +
      indel(s[i]);
    lowest_cost = opt[MATCH];
    for (k=INSERT; k<=DELETE; k++)
    if (opt[k] < lowest_cost) lowest_cost = opt[k];
    return( lowest_cost );

}

Thuật toán này là chính xác, nhưng cũng rất chậm.

Chạy trên máy tính của chúng tôi, phải mất vài giây để so sánh hai chuỗi 11 ký tự và tính toán biến mất thành không bao giờ không bao giờ rơi vào bất cứ điều gì lâu hơn.

Tại sao thuật toán quá chậm? Phải mất thời gian theo cấp số nhân vì nó tính toán lại các giá trị hết lần này đến lần khác. Tại mọi vị trí trong chuỗi, đệ quy phân nhánh theo ba cách, có nghĩa là nó tăng trưởng với tốc độ ít nhất là 3 ^ n - thực sự, thậm chí còn nhanh hơn vì hầu hết các cuộc gọi chỉ giảm một trong hai chỉ số, không phải cả hai.

Vậy làm thế nào chúng ta có thể làm cho thuật toán thực tế? Quan sát quan trọng là hầu hết các cuộc gọi đệ quy này là tính toán những thứ đã được tính toán trước đó. Làm sao mà chúng ta biết được? Chà, chỉ có thể có | s | · | T | các cuộc gọi đệ quy duy nhất có thể có, vì chỉ có nhiều cặp (i, j) riêng biệt được dùng làm tham số của các cuộc gọi đệ quy.

Bằng cách lưu trữ các giá trị cho từng cặp (i, j) này trong một bảng, chúng ta có thể tránh tính toán lại chúng và chỉ cần tra cứu chúng khi cần thiết.

Bảng là một ma trận hai chiều m trong đó mỗi | s | · | t | các ô chứa chi phí cho giải pháp tối ưu của bài toán con này, cũng như một con trỏ cha giải thích cách chúng tôi đến vị trí này:

typedef struct {
int cost; /* cost of reaching this cell */
int parent; /* parent cell */
} cell;

cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */

Phiên bản lập trình động có ba điểm khác biệt so với phiên bản đệ quy.

Đầu tiên, nó nhận các giá trị trung gian bằng cách sử dụng tra cứu bảng thay vì các cuộc gọi đệ quy.

** Thứ hai, ** nó cập nhật trường cha của mỗi ô, điều này sẽ cho phép chúng ta xây dựng lại trình tự chỉnh sửa sau này.

** Thứ ba, ** Thứ ba, nó được sử dụng một cell()hàm mục tiêu tổng quát hơn thay vì chỉ trả về m [| s |] [| t |] .cost. Điều này sẽ cho phép chúng tôi áp dụng thói quen này cho một lớp vấn đề rộng hơn.

Ở đây, một phân tích rất đặc biệt về những gì cần thiết để thu thập kết quả một phần tối ưu nhất, là điều làm cho giải pháp trở nên "năng động".

Đây là một giải pháp thay thế, đầy đủ cho cùng một vấn đề. Nó cũng là một "động" mặc dù cách thực hiện của nó là khác nhau. Tôi khuyên bạn nên kiểm tra hiệu quả của giải pháp bằng cách gửi cho thẩm phán trực tuyến của UVA. Tôi thấy đáng kinh ngạc khi một vấn đề nặng nề như vậy đã được giải quyết một cách hiệu quả như vậy.


Là lưu trữ thực sự cần thiết để được lập trình năng động? Sẽ không có bất kỳ khối lượng công việc bỏ qua đủ điều kiện một thuật toán là động?
Nthalk

Bạn phải thu thập các kết quả từng bước tối ưu để làm cho thuật toán "động". Lập trình động bắt nguồn từ công việc của Bellman trong OR, nếu bạn nói "bỏ qua bất kỳ lượng từ nào là lập trình động" thì bạn đang đánh giá thấp thuật ngữ này, vì bất kỳ tìm kiếm heuristic nào cũng sẽ là lập trình động. vi.wikipedia.org/wiki/Docate_programming
andandandand

12

Các bit chính của lập trình động là "các vấn đề con chồng chéo" và "cấu trúc con tối ưu". Các tính chất của một vấn đề có nghĩa là một giải pháp tối ưu bao gồm các giải pháp tối ưu cho các vấn đề phụ của nó. Ví dụ, các vấn đề đường dẫn ngắn nhất thể hiện cấu trúc tối ưu. Con đường ngắn nhất từ ​​A đến C là con đường ngắn nhất từ ​​A đến một số nút B theo sau là con đường ngắn nhất từ ​​nút B đến C.

Chi tiết hơn, để giải quyết vấn đề con đường ngắn nhất, bạn sẽ:

  • tìm khoảng cách từ nút bắt đầu đến mọi nút chạm vào nó (giả sử từ A đến B và C)
  • tìm khoảng cách từ các nút đó đến các nút chạm vào chúng (từ B đến D và E, và từ C đến E và F)
  • bây giờ chúng ta biết đường đi ngắn nhất từ ​​A đến E: đó là tổng ngắn nhất của Ax và xE cho một số nút x mà chúng ta đã truy cập (B hoặc C)
  • lặp lại quá trình này cho đến khi chúng ta đạt đến nút đích cuối cùng

Bởi vì chúng tôi đang làm việc từ dưới lên, chúng tôi đã có giải pháp cho các vấn đề phụ khi đến lúc sử dụng chúng, bằng cách ghi nhớ chúng.

Hãy nhớ rằng, các vấn đề lập trình động phải có cả các vấn đề phụ chồng chéo và cấu trúc tối ưu. Tạo chuỗi Fibonacci không phải là một vấn đề lập trình động; nó sử dụng ghi nhớ vì nó có các vấn đề phụ chồng chéo, nhưng nó không có cấu trúc tối ưu (vì không có vấn đề tối ưu hóa nào liên quan).


1
IMHO, đây là câu trả lời duy nhất có ý nghĩa về mặt lập trình động. Tôi tò mò từ khi mọi người bắt đầu giải thích DP bằng cách sử dụng các số Fibonacci (hầu như không liên quan).
Terry Li

@TerryLi, Nó có thể có nghĩa là "có nghĩa", nhưng nó không dễ hiểu. Vấn đề số Fibonacci được biết và dễ hiểu.
Ajay

5

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

Định nghĩa

Lập trình động (DP) là một kỹ thuật thiết kế thuật toán chung để giải quyết các vấn đề với các vấn đề phụ chồng chéo. Kỹ thuật này được phát minh bởi nhà toán học người Mỹ Richard Richardmanman vào những năm 1950.

Ý chính

Ý tưởng chính là lưu các câu trả lời chồng chéo các vấn đề nhỏ hơn để tránh tính toán lại.

Thuộc tính lập trình động

  • Một trường hợp được giải quyết bằng cách sử dụng các giải pháp cho các trường hợp nhỏ hơn.
  • Các giải pháp cho một trường hợp nhỏ hơn có thể cần nhiều lần, vì vậy hãy lưu trữ kết quả của chúng trong một bảng.
  • Do đó, mỗi trường hợp nhỏ hơn chỉ được giải quyết một lần.
  • Không gian bổ sung được sử dụng để tiết kiệm thời gian.

4

Tôi cũng rất mới đối với Lập trình động (một thuật toán mạnh mẽ cho các loại vấn đề cụ thể)

Nói cách đơn giản nhất, chỉ cần nghĩ lập trình động như một cách tiếp cận đệ quy với việc sử dụng kiến thức trước đó

Kiến thức trước đây là điều quan trọng nhất ở đây, Theo dõi giải pháp cho các vấn đề phụ bạn đã có.

Hãy xem xét điều này, ví dụ cơ bản nhất cho dp từ Wikipedia

Tìm chuỗi

function fib(n)   // naive implementation
    if n <=1 return n
    return fib(n − 1) + fib(n − 2)

Cho phép ngắt chức năng gọi với hàm n = 5

fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

Cụ thể, sợi (2) được tính ba lần từ đầu. Trong các ví dụ lớn hơn, nhiều giá trị khác của sợi, hoặc các vấn đề phụ, được tính toán lại, dẫn đến thuật toán thời gian theo cấp số nhân.

Bây giờ, hãy thử bằng cách lưu trữ giá trị mà chúng tôi đã tìm thấy trong cấu trúc dữ liệu nói là Bản đồ

var m := map(0 → 0, 1 → 1)
function fib(n)
    if key n is not in map m 
        m[n] := fib(n − 1) + fib(n − 2)
    return m[n]

Ở đây chúng tôi đang lưu giải pháp cho các vấn đề phụ trong bản đồ, nếu chúng tôi chưa có nó. Kỹ thuật lưu các giá trị mà chúng tôi đã tính toán này được gọi là Ghi nhớ.

Cuối cùng, đối với một vấn đề, trước tiên hãy cố gắng tìm các trạng thái (các vấn đề phụ có thể xảy ra và cố gắng nghĩ ra cách tiếp cận đệ quy tốt hơn để bạn có thể sử dụng giải pháp của vấn đề phụ trước đó cho các vấn đề khác).


Rip-off thẳng từ Wikipedia. Bị hạ bệ !!
solidak

3

Lập trình động là một kỹ thuật để giải quyết các vấn đề với các vấn đề phụ chồng chéo. Một thuật toán lập trình động giải quyết mọi vấn đề phụ chỉ một lần và sau đó Lưu câu trả lời của nó trong một bảng (mảng). Tránh công việc tính toán lại câu trả lời mỗi khi gặp phải vấn đề phụ. Ý tưởng cơ bản của lập trình động là: Tránh tính toán cùng một thứ hai lần, thường bằng cách giữ một bảng kết quả đã biết của các vấn đề phụ.

Bảy bước trong quá trình phát triển thuật toán lập trình động như sau:

  1. Thiết lập một thuộc tính đệ quy đưa ra giải pháp cho một trường hợp của vấn đề.
  2. Phát triển thuật toán đệ quy theo thuộc tính đệ quy
  3. Xem nếu cùng một vấn đề đang được giải quyết một lần nữa trong các cuộc gọi đệ quy
  4. Phát triển một thuật toán đệ quy ghi nhớ
  5. Xem mẫu lưu trữ dữ liệu trong bộ nhớ
  6. Chuyển đổi thuật toán đệ quy ghi nhớ thành thuật toán lặp
  7. Tối ưu hóa thuật toán lặp bằng cách sử dụng lưu trữ theo yêu cầu (tối ưu hóa lưu trữ)

6. Convert the memoized recursive algorithm into iterative algorithmmột bước bắt buộc? Điều này có nghĩa là hình thức cuối cùng của nó là không đệ quy?
Truthadjustr

không bắt buộc, không bắt buộc
Adnan Qureshi

Mục tiêu là thay thế thuật toán đệ quy được sử dụng để lưu trữ dữ liệu trong bộ nhớ bằng phép lặp trên các giá trị được lưu trữ bởi vì một giải pháp lặp giúp tiết kiệm việc tạo ngăn xếp hàm cho mỗi lệnh gọi đệ quy được thực hiện.
David C. Rankin

1

trong ngắn hạn, sự khác biệt giữa ghi nhớ đệ quy và lập trình động

Lập trình động như tên đề xuất đang sử dụng giá trị được tính toán trước đó để tự động xây dựng giải pháp mới tiếp theo

Áp dụng lập trình động ở đâu: Nếu giải pháp của bạn dựa trên cấu trúc tối ưu và vấn đề phụ chồng chéo thì trong trường hợp đó, sử dụng giá trị được tính toán trước đó sẽ hữu ích để bạn không phải tính toán lại. Đó là cách tiếp cận từ dưới lên. Giả sử bạn cần tính toán sợi (n) trong trường hợp đó, tất cả những gì bạn cần làm là thêm giá trị tính toán trước đó của sợi (n-1) và sợi (n-2)

Đệ quy: Về cơ bản chia nhỏ vấn đề của bạn thành phần nhỏ hơn để giải quyết vấn đề một cách dễ dàng nhưng hãy nhớ rằng nó không tránh tính toán lại nếu chúng ta có cùng giá trị được tính toán trong cuộc gọi đệ quy khác.

Ghi nhớ: Về cơ bản lưu trữ giá trị đệ quy được tính toán cũ trong bảng được gọi là ghi nhớ sẽ tránh tính toán lại nếu nó đã được tính toán bởi một số cuộc gọi trước đó vì vậy mọi giá trị sẽ được tính một lần. Vì vậy, trước khi tính toán, chúng tôi kiểm tra xem giá trị này đã được tính hay chưa nếu đã được tính rồi, chúng tôi trả lại tương tự từ bảng thay vì tính toán lại. Nó cũng là cách tiếp cận từ trên xuống


-2

Dưới đây là một ví dụ đơn giản mã python của Recursive, Top-down, Bottom-upcách tiếp cận để Fibonacci loạt:

Đệ quy: O (2 n )

def fib_recursive(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fib_recursive(n-1) + fib_recursive(n-2)


print(fib_recursive(40))

Từ trên xuống: O (n) Hiệu quả cho đầu vào lớn hơn

def fib_memoize_or_top_down(n, mem):
    if mem[n] is not 0:
        return mem[n]
    else:
        mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
        return mem[n]


n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))

Từ dưới lên: O (n) Để đơn giản và kích thước đầu vào nhỏ

def fib_bottom_up(n):
    mem = [0] * (n+1)
    mem[1] = 1
    mem[2] = 1
    if n == 1 or n == 2:
        return 1

    for i in range(3, n+1):
        mem[i] = mem[i-1] + mem[i-2]

    return mem[n]


print(fib_bottom_up(40))

Trường hợp đầu tiên KHÔNG có thời gian chạy là n ^ 2, độ phức tạp thời gian của nó là O (2 ^ n): stackoverflow.com/questions/360748/ Lỗi
Sam

cập nhật cảm ơn. @Sam
0xAliHn
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.