基本概念
动态规划(Dynamic Programming,DP)是一种解决优化问题的算法思想,通常用于处理具有重叠子问题和最优子结构性质的问题。动态规划算法将大问题分解成多个小问题,通过求解小问题的最优解来推导出大问题的最优解。
动态规划通常采用自底向上的方式进行计算,从小问题开始逐步推导到大问题。在求解过程中,动态规划算法会将子问题的解存储在一个表格中,以便在后续的计算中直接使用。这种存储技术使得动态规划算法具有高效的时间和空间复杂度。
动态规划算法的应用非常广泛,包括最短路径搜索、背包问题、序列比对、图像处理、语音识别等领域,具有重要的理论和实践价值。
如何实现
动态规划是一种通过将问题分解成子问题并利用已解决的子问题的解来解决复杂问题的算法。以下是实现动态规划的步骤:
- 定义子问题:将原问题分解成更小的子问题。子问题应该是原问题的一个子集,并且必须是可重复的。
- 确定状态:定义子问题的状态,也就是子问题的解。状态应该是原问题的一个子集,并且必须是可重复的。
- 设计状态转移方程:确定如何根据子问题的解来计算原问题的解。状态转移方程应该是递推的,并且必须是可重复的。
- 计算最优解:通过计算每个子问题的解来计算原问题的最优解。最优解是原问题的一个子集,并且必须是可重复的。
- 输出最优解:输出原问题的最优解。
可以通过以下步骤实现动态规划算法:
- 定义一个二维数组来存储子问题的解。
- 初始化数组的第一行和第一列。
- 通过状态转移方程计算数组中的其他元素。
- 计算最优解。
- 输出最优解。
需要注意的是,动态规划算法在计算子问题的解时,应该尽可能地利用已经计算过的子问题的解,以避免重复计算。
常见应用场景
动态规划的应用非常广泛,包括最长公共子序列、背包问题、编辑距离、最大子序和等。以下是一些常见的动态规划应用场景:
- 最长公共子序列:给定两个字符串S和T,求它们的最长公共子序列(LCS)。LCS是指在两个字符串中都出现的最长子序列,可以通过动态规划来求解。
- 背包问题:给定一些物品和一个背包,每个物品有一个重量和一个价值,背包有一个容量限制。要求选择一些物品装入背包,使得装入的物品重量不超过背包容量,同时价值最大。
- 编辑距离:给定两个字符串S和T,求它们的编辑距离。编辑距离是指将一个字符串转化成另一个字符串所需的最少操作次数,操作包括插入、删除和替换。
- 最大子序和:给定一个数组,求它的一个子序列,使得这个子序列的和最大。这个问题可以通过动态规划来求解。
- 最小路径和:给定一个二维数组,求从[1, 1] 到 [n, n] 的最小路径和。
代码示例(php实现)
<?php
/**
* 动态规划的背包问题
*
* @param array $values 物品的价值数组,下标为物品编号,值为物品的价值
* @param array $weights 物品的重量数组,下标为物品编号,值为物品的重量
* @param int $capacity 背包的容量
* @return int 最大价值
*/
function knapsack(array $values, array $weights, int $capacity): int
{
$n = count($values); // 物品的数量
// 初始化状态矩阵
$dp = array_fill(0, $n + 1, array_fill(0, $capacity + 1, 0));
// 填充状态矩阵
for ($i = 1; $i <= $n; ++$i) {
for ($j = 1; $j <= $capacity; ++$j) {
if ($weights[$i - 1] > $j) {
// 当前物品的重量大于背包的容量,无法放入背包
$dp[$i][$j] = $dp[$i - 1][$j];
} else {
// 可以放入背包,考虑取或不取当前物品
$dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i - 1][$j - $weights[$i - 1]] + $values[$i - 1]);
}
}
}
// 返回最大价值
return $dp[$n][$capacity];
}
// 示例
$values = [6, 3, 5, 4, 6];
$weights = [2, 2, 6, 5, 4];
$capacity = 10;
echo knapsack($values, $weights, $capacity); // 输出:15
<?php
function lcs($str1, $str2)
{
$m = strlen($str1);
$n = strlen($str2);
$l = array();
for ($i = 0; $i <= $m; $i++) {
for ($j = 0; $j <= $n; $j++) {
if ($i == 0 || $j == 0) {
$l[$i][$j] = 0;
} else if ($str1[$i - 1] == $str2[$j - 1]) {
$l[$i][$j] = $l[$i - 1][$j - 1] + 1;
} else {
$l[$i][$j] = max($l[$i - 1][$j], $l[$i][$j - 1]);
}
}
}
$index = $l[$m][$n];
$lcs = "";
$i = $m;
$j = $n;
while ($i > 0 && $j > 0) {
if ($str1[$i - 1] == $str2[$j - 1]) {
$lcs[$index - 1] = $str1[$i - 1];
$i--;
$j--;
$index--;
} else if ($l[$i - 1][$j] > $l[$i][$j - 1]) {
$i--;
} else {
$j--;
}
}
return $lcs;
}
$str1 = "ABCDGH";
$str2 = "AEDFHR";
echo "String 1: ".$str1."\n";
echo "String 2: ".$str2."\n";
echo "Longest Common Subsequence: ".lcs($str1, $str2)."\n";
输出结果:
String 1: ABCDGH
String 2: AEDFHR
Longest Common Subsequence: ADH
在示例代码中,我们首先定义了一个lcs函数,该函数接受两个字符串作为参数。在函数内部,我们首先获取这两个字符串的长度,并创建一个二维数组l来存储最长公共子序列的长度。接下来,我们使用两个for循环来遍历这个二维数组,并根据动态规划的思路来计算最长公共子序列的长度。最后,我们使用一个while循环来构建最长公共子序列,并返回结果。
在上面的示例代码中,我们使用了两个字符串ABCDGH和AEDFHR作为输入,并输出它们的最长公共子序列ADH。
/**
* 动态规划求最小路径和
*
* @param array $values 二维数组
* @return int 求最小路径和
*/
function minPathSum(array $values): int
{
// 初始化状态矩阵
$row = count($values);
$column = count($values[0]);
$dp = array_fill(0, $row, array_fill(0, $column, 0));
for ($i = 0; $i < $column; $i++) {
if ($i == 0) {
$dp[0][$i] = $values[0][0];
} else {
$dp[0][$i] = $dp[0][$i - 1] + $values[0][$i];
}
}
for ($i = 0; $i < $row; $i++) {
if ($i == 0) {
$dp[$i][0] = $values[$i][0];
} else {
$dp[$i][0] = $dp[$i - 1][0] + $values[$i][0];
}
}
for ($i = 1; $i < $row; $i++) {
for ($j = 1; $j < $column; $j++) {
$dp[$i][$j] = min($dp[$i - 1][$j], $dp[$i][$j - 1]) + $values[$i][$j];
}
}
return $dp[$row - 1][$column - 1];
}
// 示例
$values = [
[6, 3, 5, 4, 6],
[1, 2, 1, 1, 2],
[3, 2, 5, 2, 3],
[1, 1, 2, 1, 1]
];
echo minPathSum($values);