算法笔记-回溯法
(1)0-1背包问题
说明:有4个商品,重量分别为2, 5, 4, 2;价值分别为6, 3, 5, 4,背包只能装10重量的物品,怎么装可以获取最大价值的物品?
思路:构造一个二叉树,每个商品都有两种状态,要或者不要。如果要就在这个节点的左枝挂子节点,如果不要就在右节点挂子节点。如果全部商品都分配完状态之后就回溯,回溯到一个还有其他选择的节点,接着往这个选择发展节点,然后再回溯,然后再往下。。。。 直到无路可走,就结束了。
假如限制重量是10,总共有四个商品,重量分别是2, 5, 4, 2 价格分别是6, 3, 5, 4。第一轮的路程如5-11图,第1个商品要,第2个商品要,第3个商品发现装不下了,所以到第3个节点只能走右节点,并且第3个节点的左节点成为死节点,没有发展下去的可能了。第4个商品要,此时已经给所有的商品赋予状态了(要或者不要),记录下此时所有商品的价值和,记为最优价格。接着就开始回溯,如5-13,从节点5先是回溯到节点4(此时购物车有1,2,3选择不要,4肯定是要的,所以没必要再发展4节点的右节点),再到节点3(节点三的左节点是死节点),再到节点2,节点2的右节点是可选的,然后接着按照刚开始的逻辑接着往下走就可以了,等继续走完这一轮,计算最优值,更新下最优值,然后再回溯。。。
剪枝:如果按照上面的逻辑,其实几乎相当于遍历了所有的可能性。如果有4个商品,就会有2的4次方种可能,有些不可能是最优结果的分支直接就剪掉就可以了,比如,如果按照上面的逻辑是会有:1不要2不要3不要4不要这个分支。所以如果发现背包可以把剩下的商品都装入的情况,就直接给剩余的商品赋值为要就可以了。当1不要2不要的时候,3和4可以都装入背包,直接都要就可以了。没必要再走3不要的分支(直接设置成死节点)。或者也可以判断就算把剩余的都加进包里,总价值也小于当前最优值,当前这条路也没必要走了。
代码:这个模板几乎可以解决所有这个类型的问题,buildTree用于构建树,这个微调就可以;thenData用于返回当前选择(这是个抽象概念,该题中表示这个商品要或者不要,部落护卫队问题表示这个人要不要,地图标色问题表示当前国家涂什么颜色)所对应的下一层数据;isContinue用于判断当前路径是否要继续(比如当前路径发展为第一个商品选择装入,第二个商品选择装入,第三个商品选择装入,isContinue将返回false,因为第三个已经装不下了。即使可以装下,有时候我们也可以从其他角度判断返回false,越早的堵死,越少走弯路);travel根据tree返回所有路径,这个方法几乎不用动,根据路径的长度就可以判断是否为完整路径(此题应该与货物数量相等);
1 <?php 2 $w = [2, 5, 4, 2]; 3 $v = [6, 3, 5, 4]; 4 $limit = 10; 5 6 $tree = buildTree($w); 7 print_r(resolve($tree)); 8 9 function resolve($obj) { 10 global $w, $v; 11 $paths = travel($obj); 12 $ret = array(); 13 foreach ($paths as $path) { 14 $valCount = 0; 15 foreach ($path as $k => $isChoose) { 16 $valCount += $v[$k] * $isChoose; 17 } 18 $ret[$valCount][] = $path; 19 } 20 return $ret; 21 22 } 23 function travel($obj) { 24 $paths = array(); 25 if (!$obj->nodes) { 26 return [$paths]; 27 } 28 29 foreach ($obj->nodes as $isChoose => $node) { 30 $tmpRes = travel($node); 31 foreach ($tmpRes as & $path) { 32 $path[$obj->val] = $isChoose; 33 } 34 $paths = array_merge($paths, $tmpRes); 35 } 36 return $paths; 37 } 38 39 function buildTree($data, $path = []) { 40 if (empty($data)) { 41 return nodeFactory(null); 42 } 43 $key = key($data); 44 $obj = nodeFactory($key); 45 $nextData = thenData($data); 46 47 foreach ($nextData as $isChoose => $ndata) { 48 $path[$key] = $isChoose; 49 if (isContinue($path)) { 50 unset($ndata[$key]); 51 $obj->nodes[$isChoose] = buildTree($ndata, $path); 52 } 53 } 54 return $obj; 55 } 56 57 function thenData($data) { 58 return [ 59 1 => $data, 60 0 => $data, 61 ]; 62 } 63 64 function isContinue($path) { 65 global $limit, $w, $v; 66 static $maxVal = 0; 67 $weightCount = 0; 68 $valueCount = 0; 69 foreach ($path as $k => $value) { 70 $weightCount += $w[$k] * $value; 71 $valueCount += $v[$k] * $value; 72 } 73 //重量限制 74 if ($weightCount > $limit) { 75 return false; 76 } 77 78 //就算剩下的货物全要, 价值也比不上最大价值 79 //就没有继续往下的必要了 80 $curreny = max(array_keys($path)); 81 $remainSumVal = array_sum(array_slice($v, $curreny + 1)); 82 if ($valueCount + $remainSumVal < $maxVal) { 83 return false; 84 } 85 86 //更新下最优值 87 $maxVal = max($maxVal, $valueCount); 88 return true; 89 } 90 91 function nodeFactory($val) { 92 $obj = new stdClass(); 93 $obj->val = $val; 94 $obj->nodes = array(); 95 return $obj; 96 }
ps:这版写的很乱,不要看了 只为记录用
1 <?php 2 $w = [2, 5, 4, 2]; 3 $v = [6, 3, 5, 4]; 4 $current = getNode(0); 5 $count = count($w); 6 list($limit, $best, $cw, $cp, $bestMap, $map) = [10, 0, 0, 0, array(), array()]; 7 $noBack = true; 8 9 while (1) { 10 $node = getNode($current->level + 1, $current); 11 if ($current->level < $count && $noBack) { 12 if ($best >= array_sum(array_slice($v, $current->level)) + $cp) { 13 $current->l = false; //剪枝 14 $current->r = false; 15 $noBack = false; 16 } elseif (is_object($current->l)|| $current->l === false) { 17 $node->dir = 0; //这种情况是回溯回来的,直接发展右节点就可以了 18 $current->r = & $node; 19 } elseif ($cw + $w[$current->level] <= $limit) { 20 $cw += $w[$current->level]; $cp += $v[$current->level]; 21 $node->dir = 1; //1代表左枝,0代表右枝 22 $current->l = & $node; //这种情况代表背包可以装下,所以挂在左节点 23 $map[$current->level] = 1; 24 } else { 25 $node->dir = 0; 26 $current->r = & $node; 27 $current->l = false; //这种情况代表装不下,左节点是死节点,发展右节点 28 } 29 $current = & $node; 30 } else { //走完一轮,开始回溯 31 if ($cp > $best) { //记录最优值 32 $best = $cp; $bestMap = $map; 33 } 34 while (1) { //开始回溯 35 $deal = isset($current->dir) ? $current->dir : 0; 36 $current = & $current->p; 37 if ($current === null) { 38 break 2; //到头了,结束 39 } 40 if (isset($map[$current->level])) { 41 unset($map[$current->level]); 42 $cw -= $w[$current->level] * $deal; //怎么加的,怎么减回去 43 $cp -= $v[$current->level] * $deal; 44 } 45 if ($current->l === null || $current->r === null) { //存在活结点 46 $noBack = true; 47 break; 48 } 49 } 50 } 51 unset($node); 52 } 53 54 function getNode($level, & $p = null) { 55 $node = new stdClass(); 56 $node->level = $level; $node->p = & $p; 57 $node->l = null; $node->r = null; 58 return $node; 59 } 60 61 print_r(['map' => $bestMap, 'val' => $best]);
(2)最大团问题
说明
思路:和上一个问题几乎是一样的, 根据场景不同, 我们需要修改isContinue,thenData这两个函数其他的几乎没变
1 <?php 2 //每个居民的关系 3 $map = [ 4 [false, true, true, true, true], 5 [true, false, true, false, false], 6 [true, true, false, true, true], 7 [true, false, true, false, true], 8 [true, false, true, true, false], 9 ]; 10 11 //五个居民 12 $residents = [1, 2, 3, 4, 5]; 13 $tree = buildTree($residents); 14 15 print_r(resolve($tree)); 16 17 18 function resolve($obj) { 19 global $residents; 20 $count = count($residents); 21 22 //过滤掉不完成的路径 23 $paths = array_filter(travel($obj), function ($path) use ($count) { 24 return count($path) === $count; 25 }); 26 27 //从里面选择一个最大就可以了 28 $ret = array(); 29 foreach ($paths as $path) { 30 $count = array_sum($path); 31 $ret[$count][] = $path; 32 } 33 $max = max(array_keys($ret)); 34 return $ret[$max]; 35 36 } 37 function travel($obj) { 38 $paths = array(); 39 if (!$obj->nodes) { 40 return [$paths]; 41 } 42 43 foreach ($obj->nodes as $isChoose => $node) { 44 $tmpRes = travel($node); 45 foreach ($tmpRes as & $path) { 46 $path[$obj->val] = $isChoose; 47 } 48 $paths = array_merge($paths, $tmpRes); 49 } 50 return $paths; 51 } 52 53 function buildTree($residents, $path = []) { 54 if (empty($residents)) { 55 return nodeFactory(null); 56 } 57 $cresident = array_shift($residents); 58 $obj = nodeFactory($cresident); 59 $nextData = thenData($cresident, $residents); 60 61 foreach ($nextData as $isChoose => $ndata) { 62 $path[$cresident] = $isChoose; 63 if (isContinue($path)) { 64 $obj->nodes[$isChoose] = buildTree($ndata, $path); 65 } 66 } 67 return $obj; 68 } 69 70 71 function isContinue($path) { 72 global $residents; 73 static $maxVal = 0; 74 75 //无法超过最大值 76 $remaind = array_diff($residents, array_keys($path)); 77 if (array_sum($path) + count($remaind) < $maxVal) { 78 return false; 79 } 80 81 //更新下最优值 82 $maxVal = max($maxVal, array_sum($path)); 83 return true; 84 } 85 86 87 function thenData($cresident, $data) { 88 $choise = array_filter($data, function ($p) use ($cresident){ 89 return isLink($p, $cresident); 90 }); 91 92 //这个把1放在上面可以少走很多弯路1 93 return [ 94 1 => $choise, 95 0 => $data, 96 ]; 97 } 98 99 function isLink($p1, $p2) { 100 global $map; 101 return $map[$p1 - 1][$p2 - 1]; 102 } 103 104 function nodeFactory($val) { 105 $obj = new stdClass(); 106 $obj->val = $val; 107 $obj->nodes = array(); 108 return $obj; 109 }
(3)地图着色问题
说明
思路:之前的是二叉树,而这次有多个分支。但总体思路和之前一样
1 <?php 2 //每个国家是否相邻 3 $map = [ 4 [false, true, true, true, false, false, false], 5 [true, false, true, false, true, false, false], 6 [true, true, false, true, true, false, false], 7 [true, false, true, false, true, false, true], 8 [false, true, true, true, false, true, true], 9 [false, false, false, false, true, false, true], 10 [false, false, false, true, true, true, false], 11 ]; 12 13 14 //3个颜色 15 $allColors = [1, 2, 3]; 16 17 //7个国家 18 $countries = [1, 2, 3, 4, 5, 6, 7]; 19 20 $tree = buildTree($countries, $allColors); 21 print_r(resolve($tree)); 22 23 function resolve($obj) { 24 global $countries; 25 $count = count($countries); 26 27 //过滤掉不完成的路径 28 $paths = array_filter(travel($obj), function ($path) use ($count) { 29 return count($path) === $count; 30 }); 31 32 return $paths; 33 34 } 35 function travel($obj) { 36 $paths = array(); 37 if (!$obj->nodes) { 38 return [$paths]; 39 } 40 41 foreach ($obj->nodes as $isChoose => $node) { 42 $tmpRes = travel($node); 43 foreach ($tmpRes as & $path) { 44 $path[$obj->val] = $isChoose; 45 } 46 $paths = array_merge($paths, $tmpRes); 47 } 48 return $paths; 49 } 50 51 function buildTree($countries, $colors, $path = []) { 52 global $allColors; 53 if (empty($countries)) { 54 return nodeFactory(null); 55 } 56 $country = array_shift($countries); 57 $obj = nodeFactory($country); 58 59 $nextData = thenData($colors, $countries); 60 foreach ($nextData as $color => $ndata) { 61 $path[$country] = $color; 62 if (isContinue($path)) { 63 $obj->nodes[$color] = buildTree($ndata, array_diff($allColors, [$color]), $path); 64 } 65 } 66 return $obj; 67 } 68 69 //这里只要判断所有相邻国家是否有相同着色就可以了 70 function isContinue($path) { 71 foreach ($path as $country1 => $color1) { 72 foreach ($path as $country2 => $color2) { 73 if (isLink($country1, $country2) && $color1 == $color2) { 74 return false; 75 } 76 } 77 } 78 return true; 79 } 80 81 82 function thenData($colors, $data) { 83 $ret = array(); 84 $datas = array_fill(0, count($colors), $data); 85 return array_combine($colors, $datas); 86 } 87 88 function isLink($p1, $p2) { 89 global $map; 90 return $map[$p1 - 1][$p2 - 1]; 91 } 92 93 function nodeFactory($val) { 94 $obj = new stdClass(); 95 $obj->val = $val; 96 $obj->nodes = array(); 97 return $obj; 98 }
(4)8皇后问题
说明
思路:分支在于判断每个格子是否放置皇后
1 <?php 2 3 //4个皇后 4 $queens = 4; 5 6 //所有坐标 7 $coordinates = array(); 8 for($i=0; $i<$queens; $i++) { 9 for($j=0; $j<$queens; $j++) { 10 $coordinates[] = [$i, $j]; 11 } 12 } 13 14 $tree = buildTree($coordinates); 15 print_r(resolve($tree)); 16 17 function resolve($obj) { 18 global $coordinates; 19 $count = count($coordinates); 20 21 //过滤掉不完成的路径 22 $paths = array_filter(travel($obj), function ($path) use ($count) { 23 return count($path) === $count; 24 }); 25 26 return $paths; 27 } 28 function travel($obj) { 29 $paths = array(); 30 if (!$obj->nodes) { 31 return [$paths]; 32 } 33 34 foreach ($obj->nodes as $isChoose => $node) { 35 $tmpRes = travel($node); 36 foreach ($tmpRes as & $path) { 37 $valToKey = join('-', $obj->val); 38 $path[$valToKey] = $isChoose; 39 } 40 $paths = array_merge($paths, $tmpRes); 41 } 42 return $paths; 43 } 44 45 function buildTree($coordinates, $map = []) { 46 if (empty($coordinates)) { 47 return nodeFactory(null); 48 } 49 $coord = array_shift($coordinates); 50 $obj = nodeFactory($coord); 51 52 $nextData = thenData($coordinates); 53 foreach ($nextData as $isLay => $ndata) { 54 $tmpMap = $map; 55 $isLay && $tmpMap[] = $coord; 56 if (isContinue($tmpMap, $ndata)) { 57 $obj->nodes[$isLay] = buildTree($ndata, $tmpMap); 58 } 59 } 60 61 return $obj; 62 } 63 64 65 function isContinue($map, $ndata) { 66 global $queens; 67 68 //判断剩余的行数能否放下剩余的皇后 69 $remaindQ = $queens - count($map); 70 $lines = ceil(count($ndata) / $queens); 71 if ($lines < $remaindQ) { 72 return false; 73 } 74 75 //判断是否有两个皇后处于一条直线 76 foreach ($map as $k1 => $m1) { 77 foreach ($map as $k2 => $m2) { 78 if ($k1 != $k2) { 79 if ($m1[0] == $m2[0] || $m1[1] == $m2[1] || abs($m1[0] - $m2[0]) == abs($m1[1] - $m2[1])) { 80 return false; 81 } 82 } 83 } 84 } 85 return true; 86 } 87 88 89 function thenData($coordinates) { 90 return [ 91 1 => $coordinates, 92 0 => $coordinates, 93 ]; 94 } 95 96 function isLink($p1, $p2) { 97 global $map; 98 return $map[$p1 - 1][$p2 - 1]; 99 } 100 101 function nodeFactory($val) { 102 $obj = new stdClass(); 103 $obj->val = $val; 104 $obj->nodes = array(); 105 return $obj; 106 } 107
(5)零件加工问题
说明
代码:节点为第几个加工顺序(第1次,第2次。。),分支为加工哪个零件
1 <?php 2 $m1 = [5, 1, 8, 5, 3, 4]; 3 $m2 = [7, 2, 2, 4, 7, 4]; 4 5 $tree = buildTree(array_keys($m1)); 6 print_r(resolve($tree)); 7 8 function resolve($obj) { 9 global $m1; 10 $count = count($m1); 11 12 //过滤掉不完成的路径 13 $ret = array(); 14 $paths = travel($obj); 15 foreach ($paths as $path) { 16 if (count($path) !== $count) { 17 continue; 18 } 19 $time = celTime(array_reverse($path)); 20 $ret[$time][] = $path; 21 } 22 return $ret; 23 } 24 function travel($obj) { 25 $paths = array(); 26 if (!$obj->nodes) { 27 return [$paths]; 28 } 29 30 foreach ($obj->nodes as $isChoose => $node) { 31 $tmpRes = travel($node); 32 foreach ($tmpRes as & $path) { 33 $path[$obj->val] = $isChoose; 34 } 35 $paths = array_merge($paths, $tmpRes); 36 } 37 return $paths; 38 } 39 40 function buildTree($keys, $path = []) { 41 global $m1; 42 if (empty($keys)) { 43 return nodeFactory(null); 44 } 45 46 $order = count($m1) - count($keys) + 1; 47 $obj = nodeFactory($order); 48 49 $nextData = thenData($keys); 50 foreach ($nextData as $m => $ndata) { 51 $path[] = $m; 52 if (isContinue($path)) { 53 $obj->nodes[$m] = buildTree($ndata, $path); 54 } 55 array_pop($path); 56 } 57 return $obj; 58 } 59 60 61 function isContinue($path) { 62 global $m1, $m2; 63 static $bestVal = PHP_INT_MAX; 64 65 //计算一下时间 66 $time = celTime($path); 67 //更新最优值 68 count($path) == count($m1) && $bestVal = min($time, $bestVal); 69 return $time <= $bestVal; 70 } 71 72 function celTime($path) { 73 global $m1, $m2; 74 $mTime1 = $mTime2 = []; 75 foreach ($path as $v) { 76 $mTime1[$v] = (int)end($mTime1) + $m1[$v]; 77 $begin = empty($mTime2) ? $mTime1[$v] : max(end($mTime2), $mTime1[$v]); 78 $mTime2[$v] = $begin + $m2[$v]; 79 } 80 return end($mTime2); 81 } 82 83 function thenData($keys) { 84 $ret = []; 85 foreach ($keys as $v) { 86 $ret[$v] = array_diff($keys, [$v]); 87 } 88 return $ret; 89 } 90 91 function isLink($p1, $p2) { 92 global $map; 93 return $map[$p1 - 1][$p2 - 1]; 94 } 95 96 function nodeFactory($val) { 97 $obj = new stdClass(); 98 $obj->val = $val; 99 $obj->nodes = array(); 100 return $obj; 101 }