算法设计技术:一维模式识别
最近重温经典《编程珠玑》,在第8章算法设计技术中一维模式识别实例,书中举出了5种不同的解法,解法不断优化,不断的变得高效,不断得变得更优雅,看完感触良深。已经三年没有和算法有过直接沟通了,也淡忘了,从记忆中唤起,再次遇到,发现有些思想已经深入到骨子里了。
回正题。
【问题描述】
输入n个数的序列,输出这n个数的任意连续子序列的最大和。在这里我们假设序列的数都为整数(包括正和负)
如序列: 31 -41 59 26 -53 58 97 -93 -23 84
从[2..6]的总和187为最大
【解法一:穷举法】
最简单的方法,穷举法:即对所有满足0 <= i <= j < n 的(i, j)整数对进行迭代。对于每个整数对,程序都要计算x[i..j]的总和,并检验该总和是否大于迄今为止的最大总和。
其PHP实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <?php /** * 一维模式识别算法一,穷举法 * @author phppan.p#gmail.com http://www.phppan.com * 哥学社成员(http://www.blog-brother.com/) * @package test */ function maxsofar($seq) { $maxsofar = 0; $len = count($seq); for ($i = 0; $i < $len; $i++) { for ($j = $i; $j < $len; $j++) { $sum = 0; for ($k = $i; $k <= $j; $k++) { $sum += $seq[$k]; } if($sum > $maxsofar) { $maxsofar = $sum; } } } return $maxsofar; } $seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84); echo maxsofar($seq); die(); |
算法一优点是非常清晰明了,一眼就看出其思路,并且实现简单。
但是其时间复杂度为O(n的立方),如果数据量稍微大一点,整个程序的效率将会巨慢。
【解法二:】
x[i..j]的总和与前面已计算的总和x[i..j-1]密切相关,即x[i..j] = x[i..j-1] + x[j]
于是我们有了解法二。
如下所示PHP代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <?php /** * 一维模式识别算法二,穷举法的优化算法,将时间复杂度变为n的平方 * @author phppan.p#gmail.com http://www.phppan.com * 哥学社成员(http://www.blog-brother.com/) * @package test */ function maxsofar($seq) { $maxsofar = 0; $len = count($seq); for ($i = 0; $i < $len; $i++) { $sum = 0; for ($j = $i; $j < $len; $j++) { $sum += $seq[$j]; if($sum > $maxsofar) { $maxsofar = $sum; } } } return $maxsofar; } $seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84); echo maxsofar($seq); die(); |
【解法三:预处理】
同样是根据x[i..j]的总和与前面已计算的总和x[i..j-1]密切相关,即x[i..j] = x[i..j-1] + x[j]
不过我们使用另外一种表示:预处理数据,计算cumarr[i] = x[0..i],则x[i..j] = cumarr[j] – cumarr[i - 1]
然后再如解法二一样遍历比较,算出最大的值。
如下所示代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <?php /** * 一维模式识别算法三,穷举法的优化算法,将时间复杂度变为n的平方 * @author phppan.p#gmail.com http://www.phppan.com * 哥学社成员(http://www.blog-brother.com/) * @package test */ function maxsofar($seq) { $maxsofar = 0; $len = count($seq); /* 预处理数据 */ $cumarr[-1] = 0; for ($i = 0; $i < $len; $i++) { $cumarr[$i] = $cumarr[$i - 1] + $seq[$i]; } for ($i = 0; $i < $len; $i++) { for ($j = $i; $j < $len; $j++) { $sum = $cumarr[$j] - $cumarr[$i - 1]; if($sum > $maxsofar) { $maxsofar = $sum; } } } return $maxsofar; } $seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84); echo maxsofar($seq); die(); |
【解法四:分治算法】
分治原理:要解决规模为n的问题,可递归地解决两个规模近似为n/2的子问题,然后对它们的答案进行合并以得到整个问题的答案。
首先创建两个子序列a和b,然后递归找出a,b中元素总和最大的子向量,分别称为ma和mb,也许我们可能找到最后的解了,可是也有可能答案所在的子序列一部分在ma,一部分在mb,对于这种跨越边界的序列我们将其称之为mc。
我们的分治算法将递归地计算ma和mb,并通过其它方法计算mc,然后返回3个总各和中的最大者。
其PHP实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <?php /** * 一维模式识别算法四,分治算法 * @author phppan.p#gmail.com http://www.phppan.com * 哥学社成员(http://www.blog-brother.com/) * @package test */ function maxsofar($seq, $left, $right) { if($left > $right) { return 0; // 已没有元素 递归返回 } if($left == $right) { return max(0, $seq[$left]); // 只有一个元素,返回这个元素与0之间的较大值 } $middle = floor(($left + $right) / 2); /* 左边从中间开始的最大子序列和 */ $lmax = $sum = 0; for($i = $middle; $i >= $left; $i--) { $sum += $seq[$i]; $lmax = max($lmax, $sum); } /* 右边从中间开始的最大子序列和 */ $rmax = $sum = 0; for($i = $middle + 1; $i <= $right; $i++) { $sum += $seq[$i]; $rmax = max($rmax, $sum); } return max($lmax + $rmax, maxsofar($seq, $left, $middle), maxsofar($seq, $middle + 1, $right)); } $seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84); echo maxsofar($seq, 0, count($seq) - 1); die(); |
对于mc的计算,通过观察发现,mc在a中包含右边界的最大子序列,而mc在了中包含左边界的最大子序列
此解法的时间复杂度为O(nlogn)
【解法五:扫描算法】
最大总和的初始值设为0,假设我们已解决了x[0..i-1]的问题,那么最大总各子序列要么在前i-1个元素中,要么其结束位置为i
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php /** * 一维模式识别算法五,扫描算法 * @author phppan.p#gmail.com http://www.phppan.com * 哥学社成员(http://www.blog-brother.com/) * @package test */ function maxsofar($seq) { $maxsofar = 0; $maxendinghere = 0; $len = count($seq); for($i = 0; $i < $len; $i++) { $maxendinghere = max($maxendinghere + $seq[$i], 0); $maxsofar = max($maxsofar, $maxendinghere); } return $maxsofar; } $seq = array(31, -41, 59, 26, -53, 58, 97, -93, -23, 84); echo maxsofar($seq); die(); |
理解这个程序的关键在于变量$maxendinghere。
在循环中的第一个赋值语句之前,$maxendinghere是结束位置为i-1的最大子序列的和;赋值语句将其改为结束位置为i的最大子序列的和。
其时间复杂度为O(n),对于这种时间复杂度为O(n)的算法,一般称其为线性算法。
【后记】
从立方算法到线性算法,这是一个质的飞跃。
虽然现在的工作中可能用不到算法,但是在写程序的过程中能够不自觉的优化自己的代码,这也是一种好的习惯。
《编程珠玑》值得多看几次。
1.最后一种算法中,为什么最大总和的初始值设为0?
2.最后一种算法中,echo maxsofar($seq, 0, count($seq) – 1);
写错了
@anakin 非常感谢,已经修改
另外,这里少给了一个定义,当小于0时,以0为准