Thuật toán tham lam (Greedy Algorithm) là một phương pháp giải quyết vấn đề bằng cách đưa ra các lựa chọn tối ưu nhất ở từng bước mà không cần quan tâm đến các bước tiếp theo. Đây là một kỹ thuật hữu ích cho các bài toán tối ưu hóa, nơi mà bạn muốn tìm một lời giải tốt nhất (tối đa hoặc tối thiểu) trong một tập hợp các lựa chọn.

1. Nguyên lý hoạt động của thuật toán tham lam

Nguyên lý chính của thuật toán tham lam là:

  • Chọn lựa cục bộ tối ưu tại mỗi bước: Ở mỗi bước, bạn chọn lựa giải pháp tối ưu cục bộ mà không cần quan tâm đến tương lai.
  • Tính chất của bài toán: Không phải tất cả các bài toán đều có thể giải quyết bằng thuật toán tham lam. Bài toán cần thỏa mãn tính chất “con suốt tối ưu” (Optimal Substructure) và “lựa chọn tham lam” (Greedy Choice Property).

Ví dụ điển hình: Bài toán chọn đồng xu có mệnh giá nhỏ nhất để trả tiền thối.

2. Các bước giải bài toán bằng thuật toán tham lam

  1. Bước khởi tạo: Tìm ra lựa chọn tối ưu ở bước đầu tiên.
  2. Lặp lại: Ở mỗi bước tiếp theo, chọn lựa một giải pháp tốt nhất trong số các lựa chọn hiện có.
  3. Kết thúc: Khi đạt được kết quả mong muốn hoặc không còn giải pháp nào nữa.

3. Ví dụ cơ bản: Bài toán trả tiền bằng số lượng đồng xu ít nhất

Giả sử bạn có các đồng xu mệnh giá 1, 2, 5 và 10, và bạn cần trả số tiền là 28. Nhiệm vụ của bạn là sử dụng số lượng đồng xu ít nhất.

Giải pháp bằng thuật toán tham lam:

Ở mỗi bước, bạn sẽ chọn đồng xu có mệnh giá lớn nhất mà vẫn nhỏ hơn hoặc bằng số tiền còn lại.

Mã PHP:

function minCoins($coins, $amount) {
    // Mảng chứa kết quả là các đồng xu được sử dụng
    $result = [];

    // Sắp xếp các đồng xu theo thứ tự giảm dần
    rsort($coins);

    foreach ($coins as $coin) {
        // Số lượng đồng xu hiện tại có thể sử dụng
        while ($amount >= $coin) {
            $amount -= $coin;
            $result[] = $coin;
        }
    }

    return $result;
}

// Mảng các đồng xu
$coins = [1, 2, 5, 10];
$amount = 28;

$minCoins = minCoins($coins, $amount);

// In kết quả
echo "Số đồng xu ít nhất để trả $amount: ";
print_r($minCoins);

Giải thích:

  • Chương trình bắt đầu bằng cách sắp xếp các đồng xu theo thứ tự giảm dần để luôn chọn mệnh giá lớn nhất trước tiên.
  • Sau đó, vòng lặp while sẽ trừ đi số tiền tương ứng với đồng xu đang xét cho đến khi không thể trừ thêm được nữa, sau đó chuyển sang đồng xu tiếp theo.
  • Kết quả là một mảng chứa các đồng xu đã được sử dụng.

Kết quả:

Số đồng xu ít nhất để trả 28: Array
(
    [0] => 10
    [1] => 10
    [2] => 5
    [3] => 2
    [4] => 1
)

4. Ví dụ mở rộng: Bài toán lập lịch công việc với thời gian kết thúc sớm nhất

Giả sử bạn có nhiều công việc với thời gian bắt đầu và kết thúc khác nhau. Nhiệm vụ của bạn là tìm số lượng công việc lớn nhất có thể thực hiện mà không trùng lặp thời gian.

Đầu vào:

  • Mảng các công việc, mỗi công việc có thời gian bắt đầu và kết thúc.

Giải pháp:

Thuật toán tham lam sẽ chọn công việc có thời gian kết thúc sớm nhất, sau đó lặp lại quá trình này cho các công việc còn lại không trùng lặp với công việc đã chọn.

Mã PHP:

// Hàm lập lịch công việc với thời gian kết thúc sớm nhất
function scheduleJobs($jobs) {
    // Sắp xếp công việc theo thời gian kết thúc tăng dần
    usort($jobs, function ($a, $b) {
        return $a['end'] - $b['end'];
    });

    $selectedJobs = [];
    $lastEndTime = 0;

    foreach ($jobs as $job) {
        // Nếu thời gian bắt đầu của công việc lớn hơn hoặc bằng thời gian kết thúc của công việc trước đó
        if ($job['start'] >= $lastEndTime) {
            $selectedJobs[] = $job;
            $lastEndTime = $job['end'];
        }
    }

    return $selectedJobs;
}

// Danh sách các công việc với thời gian bắt đầu và kết thúc
$jobs = [
    ['start' => 1, 'end' => 3],
    ['start' => 2, 'end' => 5],
    ['start' => 4, 'end' => 6],
    ['start' => 6, 'end' => 8],
    ['start' => 5, 'end' => 7]
];

$selectedJobs = scheduleJobs($jobs);

// In ra các công việc đã chọn
echo "Danh sách công việc có thể thực hiện: ";
print_r($selectedJobs);

Giải thích:

  • Công việc được sắp xếp theo thời gian kết thúc.
  • Tại mỗi bước, công việc có thời gian kết thúc sớm nhất và không trùng thời gian với công việc trước đó sẽ được chọn.

Kết quả:

Danh sách công việc có thể thực hiện: Array
(
    [0] => Array
        (
            [start] => 1
            [end] => 3
        )

    [1] => Array
        (
            [start] => 4
            [end] => 6
        )

    [2] => Array
        (
            [start] => 6
            [end] => 8
        )
)

5. Lưu ý khi sử dụng thuật toán tham lam

Mặc dù thuật toán tham lam rất nhanh và hiệu quả trong việc đưa ra các giải pháp, nhưng không phải lúc nào nó cũng tìm được lời giải tối ưu cho tất cả các loại bài toán. Để áp dụng thuật toán tham lam thành công, bạn cần đảm bảo rằng bài toán của mình thỏa mãn các tính chất sau:

  • Lựa chọn tham lam: Bài toán có thể được giải quyết bằng cách đưa ra lựa chọn tốt nhất ở từng bước.
  • Con suốt tối ưu: Lời giải tối ưu của bài toán lớn có thể được tạo ra từ lời giải tối ưu của các bài toán con.

Ví dụ, với bài toán tìm đường đi ngắn nhất trong đồ thị (Shortest Path), thuật toán tham lam như Dijkstra sẽ hoạt động tốt, nhưng với bài toán “balo” (Knapsack Problem), thuật toán tham lam không luôn đảm bảo tìm ra lời giải tối ưu.

6. Kết luận

Thuật toán tham lam là một kỹ thuật hữu ích và hiệu quả để giải quyết nhiều bài toán trong lập trình. Bằng cách đưa ra các quyết định tối ưu tại mỗi bước, bạn có thể giảm thiểu thời gian và tài nguyên cần thiết để tìm ra giải pháp. Tuy nhiên, cần phải hiểu rõ bản chất của bài toán để đảm bảo rằng việc áp dụng thuật toán tham lam sẽ đưa ra kết quả tốt nhất.

Việc áp dụng thuật toán tham lam trong PHP, như đã minh họa qua các ví dụ ở trên, cho thấy rằng chúng ta có thể sử dụng ngôn ngữ này để triển khai các giải pháp tối ưu một cách dễ dàng và linh hoạt.

7. Ví dụ nâng cao về thuật toán tham lam

Dưới đây là một ví dụ nâng cao về thuật toán tham lam, được minh họa qua bài toán “Bài toán cái túi (Knapsack Problem) dạng phân số” (Fractional Knapsack Problem). Đây là một trong những bài toán cổ điển và phổ biến nhất trong lý thuyết thuật toán tham lam.

Bài toán cái túi dạng phân số

Mô tả bài toán:

Giả sử bạn có một cái túi với sức chứa giới hạn W. Có n đồ vật, mỗi đồ vật có trọng lượng w[i] và giá trị v[i]. Nhiệm vụ của bạn là chọn các đồ vật để bỏ vào túi sao cho tổng giá trị là lớn nhất, nhưng trọng lượng của các đồ vật không vượt quá giới hạn của túi. Điều đặc biệt ở dạng bài toán này là bạn có thể chia đồ vật ra, nghĩa là bạn có thể chọn một phần của một đồ vật.

Chiến lược giải bài toán:

  • Chọn các đồ vật có giá trị trên một đơn vị trọng lượng (v[i] / w[i]) cao nhất trước.
  • Lặp lại cho đến khi túi không thể chứa thêm được nữa.

Thuật toán tham lam:

  1. Tính tỷ lệ value/weight cho mỗi đồ vật.
  2. Sắp xếp các đồ vật theo thứ tự giảm dần của tỷ lệ này.
  3. Bắt đầu chọn các đồ vật từ cao đến thấp cho đến khi không thể chứa thêm nữa.
  4. Nếu không thể chứa cả một đồ vật, chọn một phần của nó cho đến khi túi đầy.

Mã PHP:

// Hàm để giải bài toán cái túi dạng phân số
function fractionalKnapsack($capacity, $items) {
    // Tính giá trị trên một đơn vị trọng lượng và sắp xếp theo thứ tự giảm dần
    usort($items, function($a, $b) {
        return ($b['value'] / $b['weight']) <=> ($a['value'] / $a['weight']);
    });

    $totalValue = 0.0;
    $result = [];

    foreach ($items as $item) {
        // Nếu trọng lượng của đồ vật nhỏ hơn hoặc bằng dung lượng còn lại của túi
        if ($item['weight'] <= $capacity) {
            // Chọn toàn bộ đồ vật
            $capacity -= $item['weight'];
            $totalValue += $item['value'];
            $result[] = [
                'item' => $item,
                'fraction' => 1.0
            ];
        } else {
            // Chọn một phần đồ vật để lấp đầy túi
            $fraction = $capacity / $item['weight'];
            $totalValue += $item['value'] * $fraction;
            $result[] = [
                'item' => $item,
                'fraction' => $fraction
            ];
            break; // Đã đầy túi, dừng lại
        }
    }

    return [
        'total_value' => $totalValue,
        'selected_items' => $result
    ];
}

// Danh sách các đồ vật với trọng lượng và giá trị
$items = [
    ['weight' => 10, 'value' => 60],
    ['weight' => 20, 'value' => 100],
    ['weight' => 30, 'value' => 120]
];

$capacity = 50; // Sức chứa của túi

$result = fractionalKnapsack($capacity, $items);

// In ra tổng giá trị tối đa có thể lấy
echo "Giá trị lớn nhất có thể lấy được: " . $result['total_value'] . "\n";

// In ra các đồ vật đã được chọn và phần trăm của chúng
echo "Danh sách đồ vật được chọn:\n";
foreach ($result['selected_items'] as $selected) {
    $item = $selected['item'];
    $fraction = $selected['fraction'] * 100;
    echo "Đồ vật có trọng lượng {$item['weight']} và giá trị {$item['value']} được chọn $fraction%.\n";
}

Giải thích mã:

  • Danh sách đồ vật: Mỗi đồ vật được biểu diễn dưới dạng một mảng chứa trọng lượng và giá trị.
  • Sắp xếp: Hàm usort() sắp xếp các đồ vật theo tỷ lệ value/weight giảm dần.
  • Lựa chọn đồ vật: Mỗi lần kiểm tra xem túi còn sức chứa không, nếu có thể chọn toàn bộ một đồ vật, nó sẽ được thêm vào túi. Nếu không, chỉ một phần của đồ vật sẽ được chọn để lấp đầy túi.

Kết quả:

Giá trị lớn nhất có thể lấy được: 240
Danh sách đồ vật được chọn:
Đồ vật có trọng lượng 10 và giá trị 60 được chọn 100%.
Đồ vật có trọng lượng 20 và giá trị 100 được chọn 100%.
Đồ vật có trọng lượng 30 và giá trị 120 được chọn 33.333333333333%.

Phân tích:

Trong ví dụ này:

  • Túi có sức chứa là 50.
  • Thuật toán chọn toàn bộ hai đồ vật đầu tiên (10kg và 20kg) vì túi vẫn còn đủ chỗ.
  • Sau đó, chỉ chọn một phần của đồ vật thứ ba (33.33% của đồ vật nặng 30kg) để lấp đầy túi.

Ứng dụng thực tế:

Bài toán “Cái túi dạng phân số” thường được sử dụng trong các tình huống thực tế như:

  • Vấn đề lưu trữ dữ liệu: Tối ưu hóa không gian lưu trữ bằng cách chọn các tệp tin có giá trị cao nhất trước.
  • Vấn đề đầu tư: Đầu tư vào các dự án hoặc cổ phiếu có lợi nhuận cao nhất trước khi hết ngân sách.
  • Vấn đề vận tải: Tối ưu hóa lượng hàng hóa vận chuyển trong các xe tải có trọng tải giới hạn.

Thuật toán tham lam rất mạnh mẽ khi giải quyết các bài toán tối ưu hóa, đặc biệt là khi các bài toán có thể chia nhỏ thành các phần con độc lập. Bài toán “Cái túi dạng phân số” là một ví dụ điển hình về cách thuật toán tham lam có thể giúp đưa ra giải pháp tối ưu bằng cách đưa ra quyết định tốt nhất tại mỗi bước.