Quy hoạch động (Dynamic Programming – DP) là một kỹ thuật tối ưu hóa được sử dụng để giải các bài toán có tính chất lặp lại, bằng cách chia nhỏ bài toán lớn thành các bài toán con nhỏ hơn. Quy hoạch động tận dụng kết quả của các bài toán con đã được giải trước đó để tránh tính toán lại, từ đó giảm thiểu thời gian thực thi.

Quy hoạch động thường được sử dụng trong các bài toán tối ưu hóa và các bài toán có cấu trúc con tối ưu (optimal substructure) và tính chất lặp lại (overlapping subproblems). Kỹ thuật này thường được áp dụng khi giải các bài toán như dãy con tăng dài nhất, bài toán xếp ba lô (knapsack problem), bài toán đường đi ngắn nhất, v.v.

1. Nguyên Lý Của Quy Hoạch Động

Quy hoạch động giải quyết các bài toán thông qua ba bước chính:

Bước 1: Xác Định Cấu Trúc Con Tối Ưu (Optimal Substructure)

Nếu bài toán có thể được chia thành các bài toán con độc lập mà giải pháp cho bài toán lớn có thể được xây dựng từ lời giải của các bài toán con, ta có cấu trúc con tối ưu.

Bước 2: Xác Định Tính Chất Lặp Lại (Overlapping Subproblems)

Nếu các bài toán con lặp lại và có thể được giải quyết nhiều lần, thay vì tính toán lại mỗi khi cần, ta sẽ lưu trữ kết quả của các bài toán con để sử dụng lại sau này. Điều này giúp tiết kiệm thời gian và công sức.

Bước 3: Tối Ưu Bài Toán (Memoization hoặc Tabulation)

Có hai cách tiếp cận trong quy hoạch động:

  • Memoization (Ghi nhớ): Lưu kết quả của các bài toán con trong quá trình thực thi và sử dụng lại khi cần.
  • Tabulation: Xây dựng bảng lưu trữ (bảng tra cứu) các kết quả từ bài toán nhỏ nhất đến bài toán lớn nhất.

2. Các Bước Cơ Bản Để Giải Bài Toán Bằng Quy Hoạch Động

  1. Xác định trạng thái bài toán: Xác định các biến trạng thái phản ánh tình trạng của bài toán tại một thời điểm.
  2. Thiết lập công thức chuyển trạng thái (recurrence relation): Xác định cách tính kết quả của một trạng thái dựa trên các trạng thái trước đó.
  3. Khởi tạo trạng thái ban đầu: Đặt giá trị ban đầu cho các trạng thái cơ bản (base cases).
  4. Tối ưu hóa và tìm lời giải cuối cùng: Sử dụng các trạng thái đã tính trước đó để tính toán lời giải của bài toán lớn.

3. Ví Dụ Minh Họa: Bài Toán Dãy Con Tăng Dài Nhất (Longest Increasing Subsequence – LIS)

Bài toán dãy con tăng dài nhất (LIS) yêu cầu tìm độ dài của dãy con dài nhất trong một dãy số, sao cho các phần tử của dãy con đó là tăng dần theo thứ tự.

Ví dụ:

Cho một dãy số: [10, 22, 9, 33, 21, 50, 41, 60, 80], độ dài của dãy con tăng dài nhất là 6 với dãy con: [10, 22, 33, 50, 60, 80].

Công Thức Quy Hoạch Động

Gọi dp[i] là độ dài của dãy con tăng dài nhất kết thúc tại vị trí i. Khi đó:

dp[i] = max(dp[j]) + 1 với mọi j < i và arr[j] < arr[i]

Công thức này có nghĩa là nếu phần tử ở vị trí i lớn hơn phần tử ở vị trí jdp[j] là độ dài của dãy con tăng dài nhất tại j, thì ta có thể mở rộng dãy con đó bằng phần tử i.

Mã PHP Cho Bài Toán LIS

function longestIncreasingSubsequence($arr) {
    $n = count($arr);
    
    // Mảng dp dùng để lưu trữ độ dài của LIS kết thúc tại mỗi vị trí
    $dp = array_fill(0, $n, 1); 

    // Duyệt qua từng phần tử trong mảng
    for ($i = 1; $i < $n; $i++) {
        for ($j = 0; $j < $i; $j++) {
            if ($arr[$i] > $arr[$j] && $dp[$i] < $dp[$j] + 1) {
                $dp[$i] = $dp[$j] + 1;
            }
        }
    }

    // Tìm giá trị lớn nhất trong mảng dp, chính là độ dài của LIS
    $maxLIS = 0;
    for ($i = 0; $i < $n; $i++) {
        if ($maxLIS < $dp[$i]) {
            $maxLIS = $dp[$i];
        }
    }

    return $maxLIS;
}

$arr = [10, 22, 9, 33, 21, 50, 41, 60, 80];
echo "Độ dài của dãy con tăng dài nhất là: " . longestIncreasingSubsequence($arr);

Giải Thích Mã PHP:

  • Khởi tạo mảng dp: Ban đầu, mảng dp được khởi tạo với giá trị 1 cho mọi phần tử, vì mỗi phần tử riêng lẻ luôn tạo thành một dãy con tăng dài nhất có độ dài ít nhất là 1.
  • Duyệt qua mảng: Với mỗi phần tử i, kiểm tra tất cả các phần tử trước nó (j). Nếu arr[i] > arr[j] và dãy con tăng tại j có thể mở rộng bằng i, thì cập nhật dp[i] = dp[j] + 1.
  • Tìm giá trị lớn nhất trong dp: Kết quả cuối cùng là giá trị lớn nhất trong mảng dp, tương ứng với độ dài của dãy con tăng dài nhất.

Kết Quả:

Độ dài của dãy con tăng dài nhất là: 6

Độ Phức Tạp:

  • Thời gian: O(n^2), vì có hai vòng lặp lồng nhau.
  • Không gian: O(n), do sử dụng mảng dp để lưu trữ kết quả.

4. Tối Ưu Hóa với Thuật Toán Tìm Kiếm Nhị Phân

Bài toán LIS có thể được tối ưu hóa xuống O(n log n) bằng cách sử dụng một cấu trúc dữ liệu tìm kiếm nhị phân để duy trì dãy con tăng tạm thời. Tuy nhiên, cách tiếp cận này phức tạp hơn và sẽ không được trình bày trong bài viết này.

5. Một Số Bài Toán Khác Sử Dụng Quy Hoạch Động

  • Bài toán xếp ba lô (Knapsack Problem): Tối ưu hóa việc chọn đồ vật sao cho tổng giá trị là lớn nhất mà không vượt quá giới hạn khối lượng.
  • Bài toán đồng xu (Coin Change Problem): Tìm số lượng tối thiểu các đồng xu cần sử dụng để tạo thành một giá trị nhất định.
  • Bài toán đường đi ngắn nhất (Shortest Path Problem): Tìm đường đi ngắn nhất từ một đỉnh đến tất cả các đỉnh còn lại trong đồ thị.

6. Kết Luận

Quy hoạch động là một kỹ thuật quan trọng và mạnh mẽ trong giải thuật, đặc biệt hữu ích khi giải quyết các bài toán có cấu trúc con tối ưu và tính chất lặp lại. Bằng cách lưu trữ và tái sử dụng kết quả của các bài toán con, quy hoạch động giúp tối ưu hóa hiệu suất và giảm thiểu thời gian thực thi.

Trong bài viết này, chúng ta đã tìm hiểu về quy hoạch động thông qua các bước giải bài toán và ví dụ cụ thể bằng PHP. Với khả năng áp dụng trong nhiều bài toán tối ưu hóa, việc hiểu và thành thạo kỹ thuật quy hoạch động sẽ giúp bạn giải quyết nhiều vấn đề phức tạp một cách hiệu quả.