算法笔记-动态规划
动态规划:通过不停的缩小问题规模最终找到解。几乎都是求极限值。有以下几个特征:
①规模n问题和规模为n-1的问题,两个最优解可能相同也可能只差第n个元素,换句话说就是第n个元素是否属于最优解。如果不属于最优解那么规模为n的问题就可以换算为规模n-1的问题;如果属于最优解,那么规模为n的问题就可以换算为规模n-1的问题再补上第n个元素(可能有时候是n-k,但大部分是n-1)
②规模缩减到一定程度(一般是0或者1的时候)有明确解,或者求解的方法 ,此类问题大部分用递归
③缩减规模的方式可能有多种,再加上有时并不能直接判断第n个元素是否属于最优解,一般要用min或max取最优
(1)最长公共子序列
说明:假如有一个字符串是abcde。那么他的子序列包括a,ab,bc,ad,abc,abe等等(这个不一定非要连续,只要每个字符取自该字符串并且保持前后顺序就可以)。现在给定两个字符串cnblogs,belong,他们的公共的序列包括bl,bo等等,求其最长公共子序列
思路:其最长的子序列是blog。先理解一下书上的给的几个结论:
最后的总结很重要,逻辑就是根据这个来的。
代码:
1 <?php 2 $str1 = 'cnblogs'; 3 $str2 = 'belong'; 4 function findStr($str1, $str2) 5 { 6 if ($str1 == '' || $str2 == '') //i=0或j=0 7 return ''; 8 9 while ($str1 != '' && $str2 != '') 10 { 11 if ($str1[strlen($str1) - 1] == $str2[strlen($str2) - 1]) { //X(i) = Y(j) 12 $res = substr($str1, -1) . $res; 13 list($str1, $str2) = [substr($str1, 0, -1), substr($str2, 0, -1)]; 14 } else { //X(i) != Y(j) 15 list($r1, $r2) = [findStr(substr($str1, 0, -1), $str2), findStr($str1, substr($str2, 0, -1))]; 16 $res = strlen($r1) > strlen($r2) ? $r1 . $res : $r2 . $res; //取max 17 break; 18 } 19 } 20 return $res; 21 } 22 echo findStr($str1, $str2);
补充:忽然发现有些情况可能有多解(上面的逻辑只会返回其中一个解),比如cnb1a2ogs和beaon12g,最长公共子序列不仅有n12g还有b12g等等。补充下面的代码(主要16行解决多个解的问题,之前只是取了一个值)。
代码:
1 <?php 2 $str1 = 'cnb1a2ogs'; 3 $str2 = 'beaon12g'; 4 function findStr($str1, $str2) 5 { 6 if ($str1 == '' || $str2 == '') //i=0或j=0 7 return []; 8 9 while ($str1 != '' && $str2 != '') 10 { 11 if ($str1[strlen($str1) - 1] == $str2[strlen($str2) - 1]) { //X(i) = Y(j) 12 $res = substr($str1, -1) . $res; 13 list($str1, $str2) = [substr($str1, 0, -1), substr($str2, 0, -1)]; 14 } else { //X(i) != Y(j) 15 list($r1, $r2) = [findStr(substr($str1, 0, -1), $str2), findStr($str1, substr($str2, 0, -1))]; 16 $tmp = strlen($r1[0]) > strlen($r2[0]) ? $r1 : (strlen($r1[0]) == strlen($r2[0]) ? array_merge($r1, $r2) : $r2); //取max 17 foreach ($tmp as $v) { 18 $ret[] = $v . $res; 19 } 20 break; 21 } 22 } 23 return isset($ret) ? array_unique($ret) : array_filter([$res]); 24 } 25 print_r(findStr($str1, $str2));
(2)编辑距离
说明:如图(他那有个地方笔误了,第2种对齐是插入F,不是插入R)
思路:最短编辑距离为4。先理解一下书上的给的几个结论(画红框部分为重点,是主要逻辑)
代码:
1 <?php 2 $str1 = 'family'; 3 $str2 = 'frame'; 4 function editDistance($str1, $str2) 5 { 6 if ($str1 == $str2) return 0; //相等的话距离肯定为0 7 list($len1, $len2) = [strlen($str1), strlen($str2)]; //如果有一个字符串长度小于2,我们也可以计算出长度来 8 if ($len1 < 2 || $len2 < 2) { 9 list($long, $short) = $len1 > $len2 ? [$str1, $str2] : [$str2, $str1]; 10 return strpos($long, $short) === false ? strlen($long) : strlen($long) - strlen($short); 11 } 12 $distance = substr($str1, -1) == substr($str2, -1) ? 0 : 1; 13 return min([ //这就是那三个方式了 14 $distance + editDistance(substr($str1, 0, -1), substr($str2, 0, -1)), 15 1 + editDistance($str1, substr($str2, 0, -1)), 16 1 + editDistance(substr($str1, 0, -1), $str2), 17 ]); 18 } 19 echo editDistance($str1, $str2);
(3)游艇租赁
说明:如下图
思路:假如有5个出租站,求第1个到第5个的最少租金。可以求(1直接到5的租金),和(1-2最少租金+2-5最少租金),和(1-3最少租金+3-5最少租金),和(1-4最少租金+4-5最少租金)的最小值。然后第1个为例(1-2最少租金+2-5最少租金),1-2是可以直接求出来的(问题规模缩减到可求解),2-5的最小租金还是按照1-5的方案求。如下:
代码:
<?php $arr = [ [0, 2, 6, 9, 15, 20], [null, 0, 3, 5, 11, 18], [null, null, 0, 3, 6, 12], [null, null, null, 0, 5, 8], [null, null, null, null, 0, 6], [null, null, null, null, null, 0], ]; function minTravel($arr, $s, $e) { if ($s + 1 == $e) return $arr[$s][$e]; $res[] = $arr[$s][$e]; for ($i=$s; $i<$e - 1; $i++) { $res[] = minTravel($arr, $s, $i + 1) + minTravel($arr, $i + 1, $e); } return empty($res) ? 0 : min($res); } echo minTravel($arr, 0, 5);
补充:上面的代码只是算出最少的租金,下面的代码会算出路径和最少租金
1 <?php 2 $arr = [ 3 [0, 2, 6, 9, 15, 20], 4 [null, 0, 3, 5, 11, 18], 5 [null, null, 0, 3, 6, 12], 6 [null, null, null, 0, 5, 8], 7 [null, null, null, null, 0, 6], 8 [null, null, null, null, null, 0], 9 ]; 10 11 function minTravel($arr, $s, $e) 12 { 13 if ($s + 1 == $e) return [$s . '-' . $e => $arr[$s][$e]]; 14 $res[$s . '-' . $e] = $arr[$s][$e]; 15 for ($i=$s; $i<$e - 1; $i++) { 16 list($p1[], $p2[]) = [minTravel($arr, $s, $i + 1) , minTravel($arr, $i + 1, $e)]; 17 } 18 array_map(function($v1, $v2) use (& $res){ 19 $res[key($v1) . '-' . key($v2)] = current($v1) + current($v2); 20 }, $p1, $p2); 21 asort($res); 22 return [key($res) => current($res)]; 23 } 24 print_r(minTravel($arr, 0, 5));
(4)矩阵连乘
说明:
思路:
代码:如果想观察到合并过程可以在函数结尾的地方打印newArr(是倒叙的)
1 <?php 2 $arr = [[3, 5], [5, 10], [10, 8], [8, 2], [2, 4]]; 3 function matrix($arr) 4 { 5 if (($c = count($arr)) < 3) 6 return ($arr[0] ? array_product($arr[0]) : 0) * ($arr[1][1] ?: 1); 7 8 for ($i=0; $i<$c-1; $i++) { 9 $param = array_product($arr[$i]) * $arr[$i+1][1]; 10 $newArr = $arr; 11 array_splice($newArr, $i, 2, [[$arr[$i][0], $arr[$i+1][1]]]); 12 $res[] = $param + matrix($newArr); 13 } 14 return min($res); 15 } 16 echo matrix($arr);
(5)最优三角剖分
说明:以最短的切割路径之和,把一个凸多边形切分为多个三角形
思路:如上图,假如v0-v3是最优解的第一步,那么问题转化为1+多边形(v0,v3,v4,v5)+多边形(v0,v1,v2,v3);第一也可能是v0-v2等等,返回最小值就可以了。
代码:
1 <?php 2 $map = [ 3 [0, 2, 3, 1, 5, 6], 4 [2, 0, 3, 4, 8, 6], 5 [3, 3, 0, 10, 13, 7], 6 [1, 4, 10, 0, 12, 5], 7 [5, 8, 13, 12, 0, 3], 8 [6, 6, 7, 5, 3, 0], 9 ]; 10 $points = [0, 1, 2, 3, 4, 5]; 11 function cutting($points) { 12 global $map; 13 list($pointsCount, $lines, $res) = [count($points), array(), array()]; 14 for ($i=0; $i<$pointsCount; $i++) { 15 for ($j=0; $j<$pointsCount; $j++) { //不是相同的点,不是重复的连线:相连 lines用于记录连接过得点,避免重复计算 16 if ($i != $j && ! in_array($points[$i] . '_' . $points[$j], $lines)) { 17 $blocks = array(); //连接不相邻的两个点,将多边形分为两块 $twoPoints是这两个点 18 $lines = array_merge($lines, [$points[$i] . '_' . $points[$j], $points[$j] . '_' . $points[$i]]); 19 $twoPoints = $points[$i] < $points[$j] ? [$points[$i], $points[$j]] : [$points[$j], $points[$i]]; 20 foreach ($points as $point) { 21 if (! in_array($point, $twoPoints)) 22 $point > $twoPoints[0] && $point < $twoPoints[1] ? $blocks[0][] = $point : $blocks[1][] = $point; 23 } 24 if (count($blocks[0]) == 0 || count($blocks[1]) == 0) continue; //判定为相邻的两个点 25 list($path, $distance) = [array($points[$i] . '_' . $points[$j]), $map[$points[$i]][$points[$j]]]; 26 foreach ($blocks as $block) { //path用于记录路径,distance用于记录距离 27 if (count($block) > 1) { //等于1为三角形,没必要再分 28 $blockRes = cutting(array_merge($block, $twoPoints)); 29 $distance += current($blockRes); $path[] = key($blockRes); 30 } 31 } 32 $res[join(':', $path)] = $distance; 33 } 34 } 35 } 36 asort($res); 37 return [key($res) => current($res)]; 38 } 39 print_r(cutting($points)); //'3_5:0_3:2_0' => 9
(6)石子合并
说明:假如有6堆石子,个数分别是5, 8, 6, 9, 2, 3;将这6堆石子合并成一堆,一次合并两堆,只有相邻的两堆才能合并。比如:
第一次合并 13,6,9,2,3 5,8合并代价是13
第二次合并 13,15,2,3 6,9合并代价是15
第三次合并 13,15,5 2,5合并代价是5
第四次合并 28,5 13,15合并代价是28
第四次合并 33 28.5合并代价是33
总代价:13+15+5+28+33,求总代价最小的合并方法,这个和上面矩阵相乘的差不多。
代码:
1 <?php 2 $arr = [5, 8, 6, 9, 2, 3]; 3 function unionNum($arr) { 4 if (($numCount = count($arr)) < 3) return array_sum($arr); 5 for ($i=0; $i<count($arr) - 1; $i++) { 6 $newArr = $arr; 7 array_splice($newArr, $i, 2, $arr[$i] + $arr[$i + 1]); 8 $res[] = $arr[$i] + $arr[$i + 1] + unionNum($newArr); 9 } 10 return min($res); 11 } 12 echo unionNum($arr);
(7)0-1背包问题
说明:假如有5个物品,质量分别是2, 5, 4, 2, 3;价值分别是6, 3, 5, 4, 6。背包承载的最大重量是10。装哪些物品可以价值最大化。
代码:
1 <?php 2 $arr = [[2, 6], [5, 3], [4, 5], [2, 4], [3, 6]]; 3 function zeroOne($arr, $limit) { 4 $res = []; 5 for ($i=0; $i<count($arr); $i++) { 6 if ($limit > $arr[$i][0]) { 7 $nextArr = $arr; unset($nextArr[$i]); 8 $others = zeroOne(array_values($nextArr), $limit - $arr[$i][0]); 9 $key = key($others) ? join('-', $arr[$i]) . ':' . key ($others) : join('-', $arr[$i]); 10 $res[$key] = $arr[$i][1] + current($others); 11 } 12 } 13 arsort($res); 14 return $res ? [key($res) => current($res)] : [0]; 15 } 16 print_r(zeroOne($arr, 10)); //[3-6:4-5:2-6] => 17
(8)最优二叉搜索树
说明:有点长。。。
思路:假如最优树的根是20(第5个点,定义为s5,搜索概率为p5,左支搜索不到概率为q4,右支搜索不到概率为q5),那么问题转化为p5 * 深度 + [9, 5, 12, 15]最优树 + [24]最优树 ,后面两个分别是比20小的数组和比20大的数组,如果没有比20小的数组则替换为q4*深度;如果没有比20大的数组则替换为q5*深度;
代码:
1 <?php 2 $nums = [5, 9, 12, 15, 20, 24]; 3 $prs = [[6, 4, 8], [8, 9, 10], [10, 8, 7], [7, 2, 5], [5, 12, 5], [5, 14, 10]]; 4 5 function bestTree($nums, $prs, $level = 1) { 6 $numsCount = count($nums); 7 if ($numsCount == 1) return array_sum($prs[0]) * $level; 8 for ($i=0; $i<$numsCount; $i++) { 9 list($max, $min) = [array(), array()]; 10 for ($j=0; $j<$numsCount; $j++) { 11 if ($nums[$i] > $nums[$j]) { 12 list($min['nums'][], $min['prs'][]) = [$nums[$j], $prs[$j]]; 13 } elseif ($nums[$i] < $nums[$j]) { 14 list($max['nums'][], $max['prs'][]) = [$nums[$j], $prs[$j]]; 15 } 16 } 17 $res[] = $prs[$i][1] * $level 18 + ($max['nums'] ? bestTree($max['nums'], $max['prs'], $level + 1) : $prs[$i][2] * $level) 19 + ($min['nums'] ? bestTree($min['nums'], $min['prs'], $level + 1) : $prs[$i][0] * $level); 20 } 21 return min($res); 22 } 23 echo bestTree($nums, $prs); //252