Sự khác biệt giữa từ dưới lên và từ trên xuống là gì?


177

Cách tiếp cận từ dưới lên (để lập trình động) trước tiên bao gồm việc xem xét các bài toán con "nhỏ hơn", sau đó giải các bài toán con lớn hơn bằng cách sử dụng giải pháp cho các vấn đề nhỏ hơn.

Từ trên xuống bao gồm giải quyết vấn đề theo cách "tự nhiên" và kiểm tra xem bạn đã tính toán giải pháp cho bài toán con trước chưa.

Tôi có chút bối rối. sự khác biệt giữa hai cái đó là gì?


Câu trả lời:


247

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 fibhà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

3
@ coder000001: ví dụ về python, bạn có thể tìm kiếm trên google python memoization decorator; một số ngôn ngữ sẽ cho phép bạn viết một macro hoặc mã đóng gói mẫu ghi nhớ. Mẫu ghi nhớ không có gì khác hơn là "thay vì gọi hàm, hãy tìm giá trị từ bộ đệm (nếu giá trị không có ở đó, hãy tính toán và thêm nó vào bộ đệm trước)".
ninjagecko

16
Tôi không thấy ai đề cập đến điều này nhưng tôi nghĩ một ưu điểm khác của Từ trên xuống là bạn sẽ chỉ xây dựng bảng tra cứu / bộ đệm một cách thưa thớt. (tức là bạn điền vào các giá trị mà bạn thực sự cần chúng). Vì vậy, đây có thể là ưu điểm ngoài việc mã hóa dễ dàng. Nói cách khác, từ trên xuống có thể giúp bạn tiết kiệm thời gian chạy thực tế vì bạn không tính toán mọi thứ (bạn có thể có thời gian chạy tốt hơn rất nhiều nhưng cùng thời gian chạy không có triệu chứng). Tuy nhiên, nó đòi hỏi bộ nhớ bổ sung để giữ các khung ngăn xếp bổ sung (một lần nữa, mức tiêu thụ bộ nhớ 'có thể' (chỉ có thể) tăng gấp đôi nhưng không có triệu chứng là như nhau.
InformedA

2
Tôi có ấn tượng rằng các cách tiếp cận từ trên xuống mà giải pháp bộ đệm cho các bài toán con chồng chéo là một kỹ thuật gọi là ghi nhớ . Một kỹ thuật từ dưới lên lấp đầy một bảng và cũng tránh tính toán lại các bài toán con chồng chéo được gọi là lập bảng . Những kỹ thuật này có thể được sử dụng khi sử dụng lập trình động , trong đó đề cập đến việc giải các bài toán con để giải quyết vấn đề lớn hơn nhiều. Điều này có vẻ mâu thuẫn với câu trả lời này, trong đó câu trả lời này sử dụng lập trình động thay vì lập bảng ở nhiều nơi. Ai đúng?
Sammaron

1
@Sammaron: hmm, bạn làm cho một điểm tốt. Có lẽ tôi nên kiểm tra nguồn của mình trên Wikipedia mà tôi không thể tìm thấy. Khi kiểm tra cstheory.stackexchange một chút, bây giờ tôi đồng ý "từ dưới lên" sẽ ngụ ý đáy được biết trước (lập bảng) và "từ trên xuống" là bạn giả sử giải pháp cho các bài toán con / phụ đề. Tại thời điểm tôi tìm thấy thuật ngữ mơ hồ và tôi đã giải thích các cụm từ trong chế độ xem kép ("từ dưới lên", bạn giả sử giải pháp cho các bài toán con và ghi nhớ, "từ trên xuống" bạn biết bạn đang nói về những bài toán con nào và có thể lập bảng). Tôi sẽ cố gắng giải quyết điều này trong một chỉnh sửa.
ninjagecko

1
@mgiuffrida: Không gian ngăn xếp đôi khi được xử lý khác nhau tùy thuộc vào ngôn ngữ lập trình. Ví dụ, trong python, cố gắng thực hiện một sợi đệ quy được ghi nhớ sẽ thất bại fib(513). Thuật ngữ quá tải tôi cảm thấy đang đi vào đây. 1) Bạn luôn có thể vứt bỏ các bài toán con mà bạn không còn cần nữa. 2) Bạn luôn có thể tránh tính toán các bài toán con mà bạn không cần. 3) 1 và 2 có thể khó mã hóa hơn nhiều nếu không có cấu trúc dữ liệu rõ ràng để lưu trữ các bài toán con trong, HOẶC, khó hơn nếu luồng điều khiển phải đan xen giữa các lệnh gọi hàm (bạn có thể cần trạng thái hoặc tiếp tục).
ninjagecko

76

Từ trên xuống và từ dưới lên DP là hai cách khác nhau để giải quyết cùng một vấn đề. Xem xét một giải pháp lập trình ghi nhớ (từ trên xuống) so với động (từ dưới lên) để tính toán các số của Wikipedia.

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

Cá nhân tôi thấy ghi nhớ tự nhiên hơn nhiều. Bạn có thể thực hiện một hàm đệ quy và ghi nhớ nó bằng một quy trình cơ học (câu trả lời tra cứu đầu tiên trong bộ đệm và trả lại nếu có thể, nếu không, hãy tính toán đệ quy và sau đó quay lại, bạn lưu lại phép tính trong bộ đệm để sử dụng trong tương lai), trong khi thực hiện từ dưới lên lập trình động yêu cầu bạn mã hóa một thứ tự tính toán các giải pháp, sao cho không có "vấn đề lớn" nào được tính toán trước vấn đề nhỏ hơn mà nó phụ thuộc vào.


1
À, bây giờ tôi thấy "từ trên xuống" và "từ dưới lên" nghĩa là gì; thực tế nó chỉ đề cập đến việc ghi nhớ so với DP. Và nghĩ rằng tôi là người đã chỉnh sửa câu hỏi để đề cập đến DP trong tiêu đề ...
ninjagecko

thời gian chạy của sợi đệ quy v / s ghi nhớ bình thường là gì?
Siddhartha

Tôi nghĩ rằng hàm mũ (2 ^ n) cho một cây đệ quy.
Siddhartha

1
Vâng, nó là tuyến tính! Tôi đã rút ra cây đệ quy và thấy những cuộc gọi nào có thể tránh được và nhận ra các cuộc gọi memo_fib (n - 2) sẽ bị tránh sau cuộc gọi đầu tiên đến nó, và vì vậy tất cả các nhánh bên phải của cây đệ quy sẽ bị cắt và nó Sẽ giảm xuống tuyến tính.
Siddhartha

1
Vì DP chủ yếu liên quan đến việc xây dựng bảng kết quả trong đó mỗi kết quả được tính toán nhiều nhất một lần, nên một cách đơn giản để trực quan hóa thời gian chạy của thuật toán DP là xem bảng đó lớn đến mức nào. Trong trường hợp này, nó có kích thước n (một kết quả cho mỗi giá trị đầu vào) nên O (n). Trong các trường hợp khác, nó có thể là ma trận n ^ 2, dẫn đến O (n ^ 2), v.v.
Johnson Wong

22

Một tính năng chính của lập trình động là sự hiện diện của các bài toán con chồng chéo . Đó là, vấn đề mà bạn đang cố gắng giải quyết có thể được chia thành các bài toán con và nhiều bài toán con chia sẻ các bài toán con. Nó giống như "Phân chia và chinh phục", nhưng cuối cùng bạn lại làm điều tương tự rất nhiều lần. Một ví dụ mà tôi đã sử dụng từ năm 2003 khi giảng dạy hoặc giải thích những vấn đề này: bạn có thể tính toán các số Fibonacci theo cách đệ quy.

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

Sử dụng ngôn ngữ yêu thích của bạn và thử chạy nó cho fib(50). Nó sẽ mất một thời gian rất, rất dài. Khoảng thời gian nhiều như fib(50)chính nó! Tuy nhiên, rất nhiều công việc không cần thiết đang được thực hiện. fib(50)sẽ gọi fib(49)fib(48), nhưng sau đó cả hai sẽ kết thúc cuộc gọi fib(47), mặc dù giá trị là như nhau. Trên thực tế, fib(47)sẽ được tính toán ba lần: bằng một cuộc gọi trực tiếp từ fib(49), bằng một cuộc gọi trực tiếp từ fib(48)và một cuộc gọi trực tiếp từ một cuộc gọi khác fib(48), cuộc gọi được sinh ra bởi tính toán của fib(49)... Vì vậy, bạn thấy, chúng ta có các bài toán con chồng chéo .

Tin tuyệt vời: không cần phải tính toán cùng một giá trị nhiều lần. Khi bạn tính toán một lần, hãy lưu kết quả vào bộ đệm và lần sau sử dụng giá trị được lưu trong bộ nhớ cache! Đây là bản chất của lập trình động. Bạn có thể gọi nó là "từ trên xuống", "ghi nhớ" hoặc bất cứ điều gì bạn muốn. Cách tiếp cận này rất trực quan và rất dễ thực hiện. Trước tiên, chỉ cần viết một giải pháp đệ quy, kiểm tra nó trong các bài kiểm tra nhỏ, thêm ghi nhớ (lưu vào bộ đệm các giá trị đã được tính toán) và --- bingo! --- Bạn xong việc rồi.

Thông thường bạn cũng có thể viết một chương trình lặp tương đương hoạt động từ dưới lên, mà không cần đệ quy. Trong trường hợp này, đây sẽ là cách tiếp cận tự nhiên hơn: lặp từ 1 đến 50 tính toán tất cả các số Fibonacci khi bạn đi.

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

Trong bất kỳ kịch bản thú vị nào, giải pháp từ dưới lên thường khó hiểu hơn. Tuy nhiên, một khi bạn hiểu nó, thông thường bạn sẽ có được một bức tranh lớn rõ ràng hơn nhiều về cách thức hoạt động của thuật toán. Trong thực tế, khi giải quyết các vấn đề không cần thiết, trước tiên tôi khuyên bạn nên viết cách tiếp cận từ trên xuống và thử nghiệm nó trên các ví dụ nhỏ. Sau đó viết giải pháp từ dưới lên và so sánh cả hai để chắc chắn rằng bạn đang nhận được điều tương tự. Lý tưởng nhất, so sánh hai giải pháp tự động. Viết một thói quen nhỏ sẽ tạo ra nhiều bài kiểm tra, lý tưởng nhất - tất cảcác thử nghiệm nhỏ với kích thước nhất định --- và xác nhận rằng cả hai giải pháp đều cho kết quả như nhau. Sau đó, sử dụng giải pháp từ dưới lên trong sản xuất, nhưng giữ mã trên cùng, nhận xét. Điều này sẽ giúp các nhà phát triển khác dễ hiểu hơn những gì bạn đang làm: mã từ dưới lên có thể khá khó hiểu, ngay cả khi bạn đã viết nó và ngay cả khi bạn biết chính xác những gì bạn đang làm.

Trong nhiều ứng dụng, cách tiếp cận từ dưới lên nhanh hơn một chút do các cuộc gọi đệ quy. Tràn ngăn xếp cũng có thể là một vấn đề trong một số vấn đề nhất định và lưu ý rằng điều này có thể phụ thuộc rất nhiều vào dữ liệu đầu vào. Trong một số trường hợp, bạn có thể không thể viết bài kiểm tra gây ra lỗi tràn ngăn xếp nếu bạn không hiểu rõ về lập trình động, nhưng một ngày nào đó điều này vẫn có thể xảy ra.

Bây giờ, có những vấn đề trong đó cách tiếp cận từ trên xuống là giải pháp khả thi duy nhất bởi vì không gian vấn đề quá lớn nên không thể giải quyết tất cả các bài toán con. Tuy nhiên, "bộ đệm" vẫn hoạt động trong thời gian hợp lý vì đầu vào của bạn chỉ cần một phần của các bài toán con cần giải quyết --- nhưng quá khó để xác định rõ ràng, bài toán con nào bạn cần giải, và do đó để viết một phần dưới cùng lên giải pháp. Mặt khác, có những tình huống khi bạn biết bạn sẽ cần phải giải quyết tất cả các bài toán con. Trong trường hợp này tiếp tục và sử dụng từ dưới lên.

Cá nhân tôi sẽ sử dụng từ trên xuống dưới để tối ưu hóa Đoạn văn hay còn gọi là vấn đề tối ưu hóa gói Word (tìm kiếm các thuật toán ngắt dòng Knuth-Plass; ít nhất TeX sử dụng nó và một số phần mềm của Adobe Systems sử dụng cách tiếp cận tương tự). Tôi sẽ sử dụng từ dưới lên cho Biến đổi Fourier nhanh .


Xin chào!!! Tôi muốn xác định xem các đề xuất sau đây là đúng. - Đối với thuật toán Lập trình động, việc tính toán tất cả các giá trị với từ dưới lên nhanh hơn bất thường sau đó sử dụng đệ quy và ghi nhớ. - Thời gian của thuật toán động luôn là () trong đó Ρ là số lượng các bài toán con. - Mỗi vấn đề trong NP có thể được giải quyết theo thời gian theo cấp số nhân.
Mary Star

Tôi có thể nói gì về các đề xuất trên? Bạn có một ý tưởng? @osa
Mary Star

@evinda, (1) luôn sai. Nó giống nhau hoặc chậm hơn bất thường (khi bạn không cần tất cả các bài toán con, đệ quy có thể nhanh hơn). (2) chỉ đúng nếu bạn có thể giải quyết mọi bài toán con trong O (1). (3) là loại quyền. Mỗi vấn đề trong NP có thể được giải quyết trong thời gian đa thức trên một máy không xác định (như máy tính lượng tử, có thể làm nhiều việc cùng một lúc: có bánh của nó, đồng thời ăn nó và theo dõi cả hai kết quả). Vì vậy, theo một nghĩa nào đó, mỗi vấn đề trong NP có thể được giải quyết theo thời gian theo cấp số nhân trên một máy tính thông thường. SIde lưu ý: mọi thứ trong P cũng nằm trong NP. Ví dụ: thêm hai số nguyên
osa

19

Hãy lấy sê-ri Wikipedia làm ví dụ

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

Một cách khác để đặt nó,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

Trong trường hợp năm số đầu tiên của Wikipedia

Bottom(first) number :1
Top (fifth) number: 5 

Bây giờ, hãy xem thuật toán chuỗi Fibonacci đệ quy làm ví dụ

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

Bây giờ nếu chúng ta thực hiện chương trình này với các lệnh sau

rcursive(5);

nếu chúng ta xem xét kỹ thuật toán, để tạo ra số thứ năm, nó đòi hỏi số thứ 3 và thứ 4. Vì vậy, đệ quy của tôi thực sự bắt đầu từ đầu (5) và sau đó chuyển tất cả sang số dưới / số thấp hơn. Cách tiếp cận này thực sự là cách tiếp cận từ trên xuống.

Để tránh thực hiện tính toán nhiều lần, chúng tôi sử dụng các kỹ thuật Lập trình động. Chúng tôi lưu trữ giá trị tính toán trước đó và tái sử dụng nó. Kỹ thuật này được gọi là ghi nhớ. Có nhiều hơn để lập trình động khác sau đó ghi nhớ mà không cần thiết để thảo luận về vấn đề hiện tại.

Từ trên xuống

Hãy viết lại thuật toán ban đầu của chúng tôi và thêm các kỹ thuật ghi nhớ.

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

Và chúng tôi thực hiện phương pháp này như sau

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

Giải pháp này vẫn là từ trên xuống dưới khi thuật toán bắt đầu từ giá trị hàng đầu và đi xuống dưới mỗi bước để có được giá trị hàng đầu của chúng tôi.

Từ dưới lên

Nhưng, câu hỏi là, chúng ta có thể bắt đầu từ dưới lên không, như từ số đầu tiên của Wikipedia sau đó đi bộ lên trên. Hãy viết lại bằng cách sử dụng các kỹ thuật này,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

Bây giờ nếu chúng ta xem xét thuật toán này, nó thực sự bắt đầu từ các giá trị thấp hơn, sau đó chuyển lên đầu. Nếu tôi cần số thứ 5, tôi thực sự đang tính thứ 1, rồi thứ hai rồi thứ ba cho đến số thứ 5. Kỹ thuật này thực sự được gọi là kỹ thuật từ dưới lên.

Cuối cùng, thuật toán đầy đủ các yêu cầu lập trình động. Nhưng một cái là từ trên xuống và một cái khác là từ dưới lên. Cả hai thuật toán có độ phức tạp không gian và thời gian tương tự nhau.


Chúng ta có thể nói cách tiếp cận từ dưới lên thường được thực hiện theo cách không đệ quy không?
Lewis Chan

Không, bạn có thể chuyển đổi bất kỳ logic vòng lặp nào sang đệ quy
Ashvin Sharma

3

Lập trình động thường được gọi là Ghi nhớ!

1.Memoization là kỹ thuật từ trên xuống (bắt đầu giải quyết vấn đề đã cho bằng cách phá vỡ nó) và lập trình động là một kỹ thuật từ dưới lên (bắt đầu giải quyết từ vấn đề phụ tầm thường, hướng tới vấn đề đã cho)

2.DP tìm giải pháp bằng cách bắt đầu từ (các) trường hợp cơ sở và làm việc theo cách của nó trở lên. DP giải quyết tất cả các vấn đề phụ, bởi vì nó thực hiện từ dưới lên

Không giống như Ghi nhớ, chỉ giải quyết các vấn đề phụ cần thiết

  1. DP có khả năng biến đổi các giải pháp vũ lực theo thời gian theo cấp số nhân thành các thuật toán đa thức thời gian.

  2. DP có thể hiệu quả hơn nhiều vì lặp đi lặp lại

Ngược lại, Ghi nhớ phải trả cho chi phí (thường là đáng kể) do đệ quy.

Để đơn giản hơn, Ghi nhớ sử dụng cách tiếp cận từ trên xuống để giải quyết vấn đề tức là bắt đầu bằng vấn đề cốt lõi (chính) sau đó chia nó thành các vấn đề phụ và giải quyết các vấn đề phụ tương tự. Trong phương pháp này, cùng một vấn đề phụ có thể xảy ra nhiều lần và tiêu tốn nhiều chu kỳ CPU hơn, do đó làm tăng độ phức tạp thời gian. Trong khi đó, trong lập trình động, cùng một vấn đề phụ sẽ không được giải quyết nhiều lần nhưng kết quả trước đó sẽ được sử dụng để tối ưu hóa giải pháp.


4
Điều đó không đúng, việc ghi nhớ sử dụng bộ đệm sẽ giúp bạn tiết kiệm thời gian phức tạp tương tự như DP
InformedA

3

Đơn giản chỉ cần nói cách tiếp cận từ trên xuống sử dụng đệ quy để gọi các vấn đề Sub lặp đi lặp lại
trong khi cách tiếp cận từ dưới lên sử dụng đơn mà không gọi bất kỳ ai và do đó nó hiệu quả hơn.


1

Sau đây là giải pháp dựa trên DP cho vấn đề Chỉnh sửa khoảng cách từ trên xuống. Tôi hy vọng nó cũng sẽ giúp hiểu được thế giới Lập trình động:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

Bạn có thể nghĩ về việc thực hiện đệ quy tại nhà của bạn. Nó khá tốt và đầy thách thức nếu bạn chưa giải quyết được điều gì như thế này trước đây.


1

Từ trên xuống : Theo dõi giá trị tính toán cho đến bây giờ và trả về kết quả khi điều kiện cơ sở được đáp ứng.

int n = 5;
fibTopDown(1, 1, 2, n);

private int fibTopDown(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibTopDown(j, i + j, count + 1, n);
}

Từ dưới lên : Kết quả hiện tại phụ thuộc vào kết quả của vấn đề phụ.

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
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.