Nếu phát triển phần mềm Agile tập trung vào việc chia nhỏ các ứng dụng lớn, phức tạp thành các microservices nhỏ, liên kết với nhau, thì lập trình động cũng áp dụng phương pháp tương tự để giải quyết các vấn đề phức tạp.
Tuy nhiên, lập trình động không phải là một khái niệm lập trình máy tính. Kể từ khi nhà toán học Richard E. Bellman phát triển nó vào những năm 1950, lập trình động đã được áp dụng để giải quyết các vấn đề phức tạp trong nhiều ngành công nghiệp.
Trong bài đăng trên blog này, chúng ta sẽ xem cách bạn có thể sử dụng khái niệm và các nguyên tắc của nó để cải thiện hiệu suất của nhóm phần mềm của mình.
Điều gì là Lập trình động?
Lập trình động là phương pháp chia một vấn đề phức tạp thành các vấn đề con đơn giản hơn theo cách đệ quy.
Nó đề xuất phương pháp phân chia và chinh phục, chia các vấn đề lớn thành các phần dễ quản lý. Bằng cách giải quyết các vấn đề nhỏ nhất và tiến dần lên, bạn có thể kết hợp các giải pháp để đi đến câu trả lời cho vấn đề phức tạp ban đầu.
Về việc đặt tên, Bellman viết rằng ông chọn từ "dynamic" vì nó đại diện cho một thứ gì đó có nhiều giai đoạn hoặc thay đổi theo thời gian. Từ này cũng có nghĩa hoàn toàn chính xác trong nghĩa vật lý cổ điển cũng như khi được sử dụng như một tính từ. Ông thích từ "programming" hơn vì thấy nó phù hợp hơn so với "planning" (lập kế hoạch), "decision-making" (ra quyết định) hoặc "thinking" (suy nghĩ).
Trong ý nghĩa đó, lập trình động vừa là một phương pháp vừa là một cấu trúc đã được kiểm chứng và áp dụng rộng rãi.
Cấu trúc của lập trình động
Để sử dụng hiệu quả các phương pháp lập trình động, bạn cần hiểu hai đặc tính khóa:
Cấu trúc con tối ưu
Cấu trúc tối ưu hoặc tính tối ưu là quá trình tái diễn của việc phân chia các vấn đề phức tạp thành các vấn đề con, sao cho các giải pháp tối ưu cho các vấn đề con này khi kết hợp lại sẽ giải quyết được vấn đề ban đầu. Tính tối ưu nhấn mạnh tầm quan trọng của cách thức bạn phân chia các vấn đề.

Phương trình Bellman
Phương trình Bellman là một công cụ quan trọng giúp xây dựng cấu trúc phụ tối ưu. Nó chia một vấn đề phức tạp thành các vấn đề phụ đơn giản hơn bằng cách thể hiện giá trị của một quyết định/hành động dựa trên hai yếu tố:
- Phần thưởng ngay lập tức của quyết định/hành động
- Giá trị giảm giá của trạng thái tiếp theo là kết quả của quyết định/hành động đó
Giả sử bạn đang quyết định tuyến đường tốt nhất để đi từ nhà đến văn phòng. Sử dụng lập trình động, bạn sẽ chia hành trình thành một vài cột mốc. Sau đó, bạn sẽ áp dụng phương trình Bellman để tính thời gian cần thiết để đạt được một cột mốc (phần thưởng ngay lập tức) và thời gian ước lượng để đạt được cột mốc tiếp theo (giá trị giảm giá).
Bằng cách áp dụng lặp đi lặp lại phương trình Bellman, bạn có thể tìm ra giá trị cao nhất cho mỗi trạng thái và giải pháp tốt nhất cho vấn đề ban đầu của mình.
Phương trình Hamilton-Jacobi
Phương trình Hamilton-Jacobi mở rộng phương trình Bellman bằng cách mô tả mối quan hệ giữa hàm giá trị và động lực học hệ thống. Phương trình này được sử dụng cho các vấn đề thời gian liên tục để trực tiếp suy ra luật điều khiển tối ưu, tức là hành động cần thực hiện ở mỗi trạng thái.
Mối quan hệ lặp lại
Mối quan hệ lặp lại định nghĩa mỗi thuật ngữ trong chuỗi dựa trên các thuật ngữ trước đó. Sử dụng điều này, bạn có thể xác định chuỗi một cách lặp lại bằng cách trước tiên chỉ định điều kiện ban đầu và sau đó là mối quan hệ của nó với từng mục tiếp theo.
Do đó, giải pháp cho từng vấn đề con càng mạnh mẽ, giải pháp cho vấn đề lớn càng hiệu quả.
Các vấn đề con trùng lặp và ghi nhớ kết quả trong lập trình động
Các vấn đề phụ chồng chéo xảy ra khi cùng một vấn đề là một phần của nhiều vấn đề phụ — được giải quyết lặp đi lặp lại — trong quá trình giải quyết vấn đề ban đầu. Lập trình động ngăn chặn sự kém hiệu quả này bằng cách lưu trữ các giải pháp trong một bảng hoặc mảng để tham khảo trong tương lai.
Memoization tối ưu hóa hơn một bước nữa. Nó lưu trữ kết quả của các hàm tốn kém và tái sử dụng chúng khi các đầu vào tương tự xuất hiện lại. Điều này ngăn chặn các tính toán dư thừa, cải thiện đáng kể hiệu quả của thuật toán.
Đánh giá lười biếng, còn được gọi là gọi theo nhu cầu, chỉ đơn giản là hoãn việc đánh giá một biểu thức cho đến khi giá trị đó thực sự cần thiết. Điều này cũng giúp tăng hiệu quả bằng cách tránh các tính toán không cần thiết và cải thiện hiệu suất.
Tóm lại, đây là cấu trúc và phương pháp tiếp cận mà bạn có thể áp dụng cho lập trình động để giải quyết các vấn đề.
- Xác định các vấn đề phụ trùng lặp: Với sự trợ giúp của các mẫu mô tả vấn đề, xác định các vấn đề phụ được giải quyết nhiều lần
- Chạy đánh giá lười biếng: Chỉ thực hiện những đánh giá mà giá trị là cần thiết
- Lưu trữ kết quả: Sử dụng cấu trúc dữ liệu (như từ điển, mảng hoặc bảng băm) để lưu trữ kết quả của các vấn đề phụ này
- Tái sử dụng kết quả: Trước khi giải quyết một vấn đề phụ, hãy kiểm tra xem kết quả của nó đã được lưu trữ chưa. Nếu có, hãy tái sử dụng kết quả đã lưu trữ. Nếu không, hãy giải quyết vấn đề phụ và lưu trữ kết quả để sử dụng trong tương lai
Bây giờ chúng ta đã biết công việc lập trình động hoạt động như thế nào trên lý thuyết, hãy xem một số thuật toán phổ biến sử dụng kỹ thuật này.
Các thuật toán lập trình động phổ biến
Thuật toán lập trình động mà bạn sử dụng phụ thuộc vào bản chất của vấn đề bạn đang giải quyết. Dưới đây là một số thuật toán được sử dụng phổ biến nhất hiện nay.
Thuật toán Floyd-Warshall
Thuật toán Floyd-Warshall được sử dụng để tìm các đường đi ngắn nhất giữa tất cả các cặp đỉnh trong một đồ thị có trọng số. Thuật toán này lặp đi lặp lại để xác định khoảng cách ngắn nhất giữa bất kỳ hai đỉnh nào, bằng cách xem mỗi đỉnh là một điểm trung gian.
Thuật toán Dijkstra
Thuật toán Dijkstra tìm đường đi ngắn nhất từ một nút nguồn duy nhất đến tất cả các nút khác trong một đồ thị có trọng số. Thuật toán này được sử dụng trong các đồ thị có trọng số cạnh không âm. Thuật toán này sử dụng phương pháp tham lam để đưa ra lựa chọn tối ưu cục bộ ở mỗi bước để tìm đường đi ngắn nhất tổng thể.
Thuật toán Bellman-Ford
Thuật toán Bellman-Ford tìm đường đi ngắn nhất từ một đỉnh nguồn duy nhất đến tất cả các đỉnh khác trong một đồ thị có trọng số, ngay cả khi đồ thị đó chứa các cạnh có trọng số âm. Thuật toán này hoạt động bằng cách cập nhật lặp đi lặp lại khoảng cách ngắn nhất đã biết đến mỗi đỉnh bằng cách xem xét từng cạnh trong đồ thị và cải thiện đường đi bằng cách tìm một đường đi ngắn hơn.
Thuật toán tìm kiếm nhị phân
Thuật toán tìm kiếm nhị phân tìm vị trí của một giá trị mục tiêu trong một mảng đã được sắp xếp. Nó bắt đầu với phạm vi tìm kiếm của toàn bộ mảng và liên tục chia khoảng tìm kiếm thành hai phần bằng nhau.
Thuật toán so sánh giá trị mục tiêu với phần tử giữa của mảng. Nếu giá trị mục tiêu bằng phần tử giữa, quá trình tìm kiếm hoàn tất. Nếu giá trị mục tiêu nhỏ hơn, quá trình tìm kiếm tiếp tục ở nửa bên trái của mảng. Nếu giá trị mục tiêu lớn hơn, quá trình tìm kiếm tiếp tục ở nửa bên phải. Quá trình này lặp lại cho đến khi bạn tìm thấy giá trị mục tiêu hoặc phạm vi tìm kiếm trống.
Hãy xem một số ví dụ và ứng dụng thực tế của lập trình động.
Ví dụ về thuật toán lập trình động
Tháp Hanoi

Ngay cả khi bạn chưa từng nghe đến tên, bạn có thể đã từng thấy Trò chơi Tháp Hanoi. Đây là một trò chơi logic yêu cầu bạn di chuyển một chồng đĩa từ một cột này sang cột khác, mỗi lần chỉ di chuyển một đĩa, đồng thời đảm bảo rằng không có đĩa lớn hơn nằm trên đĩa nhỏ hơn.
Lập trình động giải quyết vấn đề này bằng cách:
- Phân tích thành việc di chuyển n−1 đĩa đến một thanh phụ
- Di chuyển đĩa thứ n đến thanh mục tiêu
- Di chuyển n−1 đĩa từ thanh phụ sang thanh mục tiêu
Bằng cách lưu trữ số lần di chuyển cần thiết cho mỗi vấn đề phụ (tức là số lần di chuyển tối thiểu cho n−1 đĩa), lập trình động đảm bảo rằng mỗi vấn đề chỉ được giải một lần, từ đó giảm thời gian tính toán tổng thể. Nó sử dụng một bảng để lưu trữ các giá trị đã tính trước đó cho số lần di chuyển tối thiểu cho mỗi vấn đề phụ.
Ma trận nhân chuỗi
Ma trận nhân chuỗi mô tả vấn đề về cách hiệu quả nhất để nhân một chuỗi ma trận. Mục tiêu là xác định thứ tự nhân để giảm thiểu số lần nhân số thực.
Cách tiếp cận lập trình động giúp chia vấn đề thành các vấn đề nhỏ hơn, tính toán chi phí nhân các chuỗi ma trận nhỏ hơn và kết hợp kết quả của chúng. Nó giải quyết lặp đi lặp lại các chuỗi có độ dài tăng dần, thuật toán đảm bảo rằng mỗi vấn đề nhỏ chỉ được giải quyết một lần.
Vấn đề tìm chuỗi con chung dài nhất
Bài toán chuỗi con dài nhất chung (LCS) nhằm tìm chuỗi con dài nhất chung cho hai chuỗi đã cho. Lập trình động giải quyết bài toán này bằng cách xây dựng một bảng trong đó mỗi mục nhập đại diện cho độ dài của LCS.
Bằng cách lặp đi lặp lại việc điền vào bảng, lập trình động tính toán hiệu quả độ dài của LCS, với bảng cuối cùng cung cấp giải pháp cho vấn đề ban đầu.
Ứng dụng thực tế của lập trình động
Mặc dù lập trình động là một lý thuyết toán học nâng cao, nhưng nó được sử dụng rộng rãi trong kỹ thuật phần mềm cho một số ứng dụng.
Căn chỉnh trình tự DNA: Trong sinh tin học, các nhà nghiên cứu sử dụng lập trình động cho một số trường hợp sử dụng, chẳng hạn như xác định sự tương đồng di truyền, dự đoán cấu trúc protein và hiểu mối quan hệ tiến hóa.
Bằng cách chia nhỏ vấn đề sắp xếp thành các vấn đề nhỏ hơn và lưu trữ các giải pháp trong một ma trận, thuật toán sẽ tính toán sự phù hợp nhất giữa các chuỗi. Khung này giúp các công việc vốn không thể thực hiện được bằng tính toán trở nên thực tế.
Lập lịch và định tuyến của hãng hàng không: Biểu diễn các sân bay dưới dạng các nút và các chuyến bay dưới dạng các cạnh có hướng, các nhà lập kế hoạch sử dụng phương pháp Ford-Fulkerson để tìm ra lộ trình tối ưu cho hành khách qua mạng lưới.
Bằng cách lặp đi lặp lại việc tăng cường các đường dẫn với sức chứa có sẵn, các thuật toán này đảm bảo phân bổ tài nguyên hiệu quả, sử dụng hiệu quả và cân bằng giữa nhu cầu và tính sẵn có, từ đó tăng hiệu quả và giảm chi phí.
Tối ưu hóa danh mục đầu tư trong tài chính: Các nhà đầu tư ngân hàng giải quyết vấn đề phân bổ tài sản giữa các khoản đầu tư khác nhau để tối đa hóa lợi nhuận đồng thời giảm thiểu rủi ro bằng cách sử dụng lập trình động.
Bằng cách chia kỳ đầu tư thành các giai đoạn, lập trình động đánh giá phân bổ tài sản tối ưu cho từng giai đoạn, xem xét lợi nhuận và rủi ro của các tài sản khác nhau. Quá trình lặp đi lặp lại bao gồm cập nhật chiến lược phân bổ dựa trên thông tin mới và điều kiện thị trường, liên tục tinh chỉnh danh mục đầu tư.
Cách tiếp cận này đảm bảo rằng chiến lược đầu tư sẽ thích ứng theo thời gian, dẫn đến danh mục đầu tư cân bằng và tối ưu hóa, phù hợp với khả năng chấp nhận rủi ro và mục tiêu tài chính của nhà đầu tư.
Lập kế hoạch mạng lưới giao thông đô thị: Để tìm ra các tuyến đường ngắn nhất trong mạng lưới giao thông đô thị, các nhà quy hoạch sử dụng lý thuyết đồ thị và đường đi, trong đó sử dụng lập trình động.
Ví dụ, trong hệ thống giao thông công cộng của một thành phố, các trạm được biểu diễn dưới dạng các nút và các tuyến đường được biểu diễn dưới dạng các cạnh với trọng số tương ứng với thời gian di chuyển hoặc khoảng cách.
Thuật toán Floyd-Warshall tối ưu hóa các tuyến đường di chuyển bằng cách cập nhật lặp đi lặp lại các tuyến đường ngắn nhất bằng cách sử dụng mối quan hệ giữa các tuyến đường trực tiếp và gián tiếp, giảm thời gian di chuyển tổng thể và nâng cao hiệu quả của hệ thống giao thông.
Mặc dù có nhiều ứng dụng, lập trình động không phải không có thách thức.
Thách thức trong lập trình động
Không giống như phương pháp tìm kiếm Brute force, trong đó bạn thử mọi giải pháp khả thi cho đến khi tìm ra giải pháp chính xác, lập trình động cung cấp giải pháp tối ưu nhất cho một vấn đề lớn. Trong quá trình thực hiện, đây là một số yếu tố khóa cần lưu ý.
Quản lý nhiều vấn đề con
Thách thức: Lập trình động yêu cầu quản lý nhiều vấn đề con để tìm ra giải pháp cho vấn đề lớn hơn. Điều này có nghĩa là bạn phải:
- Cân nhắc kỹ lưỡng việc tổ chức các kết quả trung gian để tránh tính toán dư thừa
- Xác định, giải quyết và lưu trữ từng vấn đề phụ trong một định dạng có cấu trúc như bảng hoặc mảng ghi nhớ
- Quản lý bộ nhớ hiệu quả khi quy mô của các vấn đề con tăng lên
- Tính toán chính xác và truy xuất từng bài toán con
Giải pháp: Để thực hiện tất cả những việc này và hơn thế nữa, bạn cần một phần mềm quản lý dự án mạnh mẽ như ClickUp. Nhiệm vụ ClickUp cho phép bạn tạo các nhiệm vụ phụ không giới hạn để quản lý các chuỗi lập trình động. Bạn cũng có thể đặt trạng thái tùy chỉnh, thêm trường tùy chỉnh và hệ thống quản lý chương trình phù hợp với nhu cầu của mình.

Định nghĩa vấn đề
Thách thức: Các vấn đề phức tạp có thể là một thách thức lớn đối với các nhóm trong việc hiểu, phân định và chia nhỏ thành các vấn đề phụ có ý nghĩa.
Giải pháp: Tập hợp nhóm lại và cùng nhau thảo luận các khả năng. Bảng trắng ClickUp là một khung ảo tuyệt vời để lên ý tưởng và thảo luận về vấn đề cũng như các kỹ thuật lập trình động mà bạn sử dụng. Bạn cũng có thể sử dụng phần mềm giải quyết vấn đề để hỗ trợ.

Kiểm tra và gỡ lỗi
Thách thức: Việc gỡ lỗi và kiểm tra các giải pháp lập trình động có thể phức tạp do sự phụ thuộc lẫn nhau của các vấn đề phụ. Lỗi trong một vấn đề phụ có thể ảnh hưởng đến toàn bộ giải pháp.
Ví dụ, một mối quan hệ lặp lại không chính xác trong bài toán khoảng cách chỉnh sửa có thể dẫn đến kết quả tổng thể không chính xác, khiến cho việc xác định chính xác nguồn gốc của lỗi trở nên khó khăn.
Giải pháp
- Tiến hành đánh giá mã
- Thực hiện lập trình cặp để các thành viên khác trong nhóm xem xét mã hoặc cùng nhau thực hiện, phát hiện lỗi và đưa ra các quan điểm khác nhau
- Sử dụng các công cụ phân tích nguyên nhân gốc rễ để xác định nguồn gốc của các lỗi nhằm tránh chúng tái diễn
Quản lý khối lượng công việc kém
Thách thức: Khi các thành viên trong nhóm chịu trách nhiệm về các phần khác nhau của thuật toán, có thể xảy ra sự không nhất quán trong việc hiểu các trường hợp cơ bản, định nghĩa các vấn đề phụ và quản lý khối lượng công việc không đồng đều, tất cả đều dẫn đến kết quả không chính xác.
Giải pháp: Vượt qua thách thức này bằng cách triển khai lập lịch tài nguyên hiệu quả với chế độ xem Khối lượng công việc của ClickUp.

Phối hợp và hợp tác
Thách thức: Các vấn đề phức tạp đòi hỏi sự hiểu biết sâu sắc và thực hiện chính xác. Đảm bảo tất cả các thành viên trong nhóm có cùng quan điểm về việc xây dựng vấn đề, mối quan hệ lặp lại và chiến lược tổng thể là một công việc rất lớn.
Giải pháp: Thiết lập một nền tảng cộng tác thống nhất như ClickUp. Chế độ xem trò chuyện của ClickUp hợp nhất tất cả các tin nhắn, cho phép bạn quản lý tất cả các cuộc hội thoại ở một nơi. Bạn có thể gắn thẻ các thành viên trong nhóm và thêm nhận xét mà không cần chuyển sang các công cụ khác.

Tối ưu hóa hiệu suất
Thách thức: Tối ưu hóa hiệu suất của một giải pháp lập trình động đòi hỏi phải xem xét cẩn thận cả độ phức tạp về thời gian và không gian. Thông thường, trong khi một phần của nhóm tối ưu hóa độ phức tạp về thời gian, một phần khác lại vô tình làm tăng độ phức tạp về không gian, dẫn đến hiệu suất tổng thể không đạt mức tối ưu.
Giải pháp: Bảng điều khiển ClickUp sẽ giúp bạn giải quyết vấn đề này. Bảng điều khiển này cung cấp thông tin chi tiết về hiệu suất của toàn bộ dự án theo thời gian thực, giúp bạn đo lường, điều chỉnh và tối ưu hóa các nhiệm vụ của chương trình động để đạt được hiệu quả cao hơn.

Tài liệu và chuyển giao kiến thức
Thách thức: Các nhóm Agile ưu tiên phần mềm hoạt động hơn tài liệu. Điều này có thể gây ra một thách thức đặc biệt. Ví dụ: nếu các mối quan hệ lặp lại không được ghi chép rõ ràng, các thành viên mới của nhóm có thể gặp khó khăn trong việc hiểu và phát triển giải pháp hiện có.
Giải pháp: Tạo chiến lược hoạt động cân bằng giữa tài liệu và mã công việc. Sử dụng ClickUp Docs để tạo, chỉnh sửa và quản lý tài liệu về lý do và cách thức thiết kế các quyết định nhất định.

Giải quyết các vấn đề phức tạp với lập trình động trên ClickUp
Các vấn đề hiện đại, theo định nghĩa của nó, là rất phức tạp. Đặc biệt là với độ sâu và sự tinh vi của phần mềm ngày nay, các vấn đề mà các nhóm kỹ sư phải đối mặt là rất lớn.
Lập trình động cung cấp một phương pháp giải quyết vấn đề hiệu quả và hiệu quả. Nó giảm thiểu các tính toán dư thừa và sử dụng các quy trình lặp đi lặp lại để củng cố kết quả đồng thời tối ưu hóa sức chứa và hiệu suất.
Tuy nhiên, việc quản lý các sáng kiến lập trình động từ đầu đến cuối đòi hỏi phải có quản lý dự án và lập kế hoạch sức chứa hiệu quả.
ClickUp cho các nhóm phát triển phần mềm là sự lựa chọn lý tưởng. Nó cho phép bạn xử lý các nhiệm vụ liên kết với nhau, ghi chép quá trình suy nghĩ và quản lý kết quả, tất cả chỉ trong một nơi. Đừng chỉ tin lời chúng tôi.
Hãy dùng thử ClickUp miễn phí ngay hôm nay!
Câu hỏi thường gặp
1. Dynamic programming là gì?
Thuật ngữ lập trình động đề cập đến quá trình giải quyết các vấn đề phức tạp bằng thuật toán bằng cách chia chúng thành các vấn đề phụ đơn giản hơn. Phương pháp này ưu tiên giải quyết từng vấn đề phụ chỉ một lần và lưu trữ giải pháp của nó, thường là trong một bảng, để tránh tính toán dư thừa.
2. Ví dụ về thuật toán lập trình động là gì?
Bạn có thể sử dụng lập trình động để xác định chiến lược tối ưu trong mọi lĩnh vực, từ dãy số Fibonacci đến bản đồ không gian.
Một trong những ví dụ về lập trình động là bài toán Knapsack. Trong bài toán này, bạn có một tập hợp các mục, mỗi mục có trọng lượng và giá trị, và một chiếc ba lô có sức chứa tối đa. Mục tiêu là xác định giá trị tối đa mà bạn có thể mang trong ba lô mà không vượt quá sức chứa.
Lập trình động giải quyết vấn đề này bằng cách chia nhỏ thành các vấn đề phụ và lưu trữ kết quả của các vấn đề phụ này trong một bảng. Sau đó, nó sử dụng các kết quả này để xây dựng giải pháp tối ưu cho vấn đề tổng thể.
3. Ý tưởng cơ bản của lập trình động là gì?
Ý tưởng cơ bản là tiếp cận các bài toán lập trình động bằng cách chia chúng thành các bài toán con đơn giản hơn, giải từng bài toán con một lần, sau đó tổng hợp lại để tìm ra giải pháp cho bài toán lớn hơn.