一个人思虑太多,就会失去做人的乐趣。
概述
本课程中我们将考虑大规模问题的高效解法,即考虑规模性。本课程的结构如下:
- 算法思想
- 排序/树
- 哈希
- 数论:加密解密算法
- 图
- 最短路径
- 动态规划:图像压缩
- 高级话题
峰值算法
一维峰值算法
问题
考虑下面的一维数组,其中$a-i$为数字:
极值点的定义为:
当然,如果在边界上,那么只需要大于一侧即可。我们的问题是:如果峰值点存在,找到峰值点。
解法
最直接的解法就是遍历每一个元素,然后根据定义看一下是否为峰值,其复杂度为$O(n)$,现在我们来看看一个更高效的解法:分治法。
1 | if a[n/2] < a[n/2-1] |
时间复杂度分析
假设我们的算法是正确的,那么其时间复杂度为:
故:
二维峰值算法
问题
考虑下面的二维数组:
假定$a$为峰值点,那么$a\ge b, a\ge c, a\ge d,a\ge e$。
解法
解法1:贪心
我们使用贪心算法解决这个问题,假设我们的矩阵为:
我们的策略是,总是沿着最大的路径走,例如从12开始,我们向13搜索,依次走过14->15->16…直到20为止。该算法的时间复杂度为$\Theta(nm)$。
解法2:分治法
考虑这样的解法:我们先选取中间列,令$j=m/2$,找到一个位于$(i,j)$的一维峰值点,然后以$(i,j)$为基准,再找到这一行的一维峰值点,这样能找到二维的峰值点吗?
答案是不行,这个解法是错误的,以我们上面的图为例,假设12是该列的一个峰值点,而当我们搜索12所在的行时,最终找到的峰值点是14,但是14并不是一个二维峰值点,所以该算法是错的。我们给出正确的贪心规则:
1 | 选取中间列 j=m/2 |
在上面的伪代码中,else部分可以直接返回极值点,是因为$(i,j)$为该列的最大值,所以比$(i-1,j),(i+1,j)$都要大,而该点又比$(i,j-1),(i,j+1)$大,故符合峰值定义,可以直接返回。当只有一列时,最大值即峰值,这个是基例。
时间复杂度分析
上面的算法时间复杂度计算如下:
题目及题解
题目
一:将下列函数根据时间复杂度进行排序
第一组
解
由于$n\rightarrow \infin$时$\log n$与$n^{0.0000001}$趋近于无穷大,所以对上式使用洛必达法则:
故$\Theta(f_1)\le\Theta(f_2)$。同理易得$\Theta(f_2)\le\Theta(f_3)$,$\Theta(f_2)\le\Theta(f_4)$,而$\Theta(f_4)\le\Theta(f_3)$,故上述公式时间复杂度排序为:$\Theta(f_1)\le\Theta(f_2)\le\Theta(f_4)\le\Theta(f3)$
第二组
解
常数时间复杂度为$\Theta(1)$为最小,$f_2$时间复杂度最高,为$\Theta(2^n)$,现比较$f_3$和$f_4$,$f_3=\frac{n(n-1)}{2}$,故
所以排序结果为$\Theta(f_1)\le\Theta(f_4)\le\Theta(f_3)\le\Theta(f2)$.
第三组
这个算极限比较麻烦,采用matlab求两两之间的极限,代码如下:
1 | syms n |
最终结果为:$\Theta(f_4)\le\Theta(f_1)\le\Theta(f_3)\le\Theta(f_2)$
二:递归复杂度求解
第一题
已知:
求$T(n,n)$的时间复杂度
解:
故$T(n,n)=\Theta(n)$。
第二题
已知:
解:
故$T(n,n)=\Theta(n\log n)$。
第三题
解
故$T(n,n)=\Theta(n)$
代码
向量峰值搜索代码
1 | class Solution { |
矩阵峰值搜索代码
方法一:分治法
根据上面的分治法,我们可以写出如下代码:
1 | def algorithm1(problem, trace = None): |
证明
为了证明算法总是能找到峰值,我们需要证明如下两条陈述:
如果矩阵非空,那么算法1总能返回一个位置
如果算法1返回了一个位置,那么这个位置一定是峰值点
证明第一点:
假设矩阵为$n\times n$,那么递归子问题的规模为$m \times\left \lfloor n/2 \right \rfloor$,所以递归子问题中列数随着递归过程是严格递减的。所以算法最终会返回一个点,或者最后进入一个列为0的子问题而不返回任何一个位置,故如果算法1存在不返回位置的情况,那么一定有子问题列数为0,下面我们证明这种情况不可能发生。如果算法确实处理了一个空的子问题,那么该问题的上一步子问题规模一定为$m\times 1$或者$m\times2$,下面分类讨论
(1) 如果为$m\times1$,那么计算中心列的最大值即为计算整个子数组的最大值,而子数组的最大值是一定存在的,所以一定会返回一个峰值点。
(2) 如果为$m\times2$,那么又存在两种情况,第一,中心列的最大值即为峰值点,那么会返回峰值的位置;第二,中心列的最大值不是峰值点,那么算法又会进一步递归,处理一个$m\times1$的子问题,一定会返回一个峰值点。
综上,算法一定会返回一个点。
证明第二点:
假设我们返回的点为$(r_1,c_1)$,那么该点一定是$c_1$列上最大的,如果该点不是峰值点,那么算法会以$(r_1,c_1)$为起点进入下一个递归中,同时,根据算法的规则,存在与$c_1$相邻的$c_2$($|c1-c2|= 1$),满足$val(r_1,c_1) < val(r_1,c_2)$,令$(r_2,c_2)$为$c_2$列上的最大值,有$val(r_1,c_2)\le val(r_2,c_2)$。因为算法执行到了包含$(r_1,c_1)$的一侧,所以一定有$val(r_2,c_2)<val(r_2,c_1)$,所以
但是$(r_1,c_1)$又是该列最大的,所以产生了矛盾,所以$(r_1,c_1)$一定是峰值点。
复杂度分析
根据前面的结果,分治法的时间复杂度为$\Theta(n\log n)$
方法二:贪心上升法
1 | def algorithm2(problem, location = (0, 0), trace = None): |
方法二的逻辑很简单,一直找比当前节点更大的相邻节点,直到找不到为止。
证明
在贪心算法中,我们的搜索过程是单调递增的,由于数组的大小是有限的,那么数组中的数存在上界,我们一定能够找到一个位置,且该位置一定是一个峰值点。
复杂度分析
设二维矩阵大小为$n\times n$,假设$a(i,j)$为当前搜索位置,那么下一个搜索位置$a_{next}$的完整递归定义为:
从递归中可知,最坏的情况下,需要遍历所有的网格,所以时间复杂度为$\Theta(n^2)$。
方法三:改进的分治法(错误)
在分治法中,我们将矩阵分为了左右两个部分,寻找极值点,那么我们能不能进一步分割呢?比如不再是二分割,而是将矩阵四分割,横纵坐标各切一刀,将矩阵分为四个部分,依次在四个部分中寻找极值点?代码如下:
1 | def algorithm3(problem, bestSeen = None, trace = None): |
代码逻辑如下,每次在midRow
和midCol
中找到最大值,然后找到最大值的四邻域中的最大值,如果上下左右四格都比找到的最大值小,那么最大值即为峰值;如果存在临近的位置比找到的最大值大,那么就到该位置所在的子问题中寻找峰值点。通过将问题分为四个子问题,我们似乎能够递归地找到峰值点结果。但是很遗憾,这个代码是错误的,一个反例如下:
1 | matrix = [[0,0,9,8,0,0,0], |
在上面的例子中,我们先在中间行和列找到了最大值8,然后8的邻居中最大的是9,选择左上子矩阵作为子问题,中心十字最大值为1,而在这个子问题中,1是子矩阵的极值点,但并不是全局极值点。这个解法的根本问题在于,在解决子问题的过程中,子问题边界值的比较是不充分的,在上面的例子中,1是子问题的边界值,但是这个边界值并没有和2进行比较,所以比较不充分,找到了错误的解。
方法四:另一个改进的分治法
1 | def algorithm4(problem, bestSeen = None, rowSplit = True, trace = None): |
方法四在方法一的基础上添加了一个bestSeen变量,用于记录上一步操作中最大的值,下面我们对这个方法进行证明。证明过程参考方法一,也是分为两步,第一步证明如果矩阵不为空,那么返回值存在,第二步证明返回值一定是峰值。这里直接证明第二步。
假设我们的返回值为$(r_1,c_1)$,且该点不是峰值点,那么存在$(r_3,c_3)$与$(r_1,c_1)$相邻,且$val(r_3,c_3)>val(r_1,c_1)$,又因为$(r_1,c_1)$为返回值,故存在$(r_2,c_2)\rightarrow (r_1,c_1)$的转换,且$val(r_2,c_2) < val(r_1,c_1)$。但是我们的返回值是$(r_1,c_1)$,所以递归到$(r_3,c_3)$后,又会经过某个递归链从$(r_3,c_3)$到达$(r_2,c_2)$,所以$val(r_3,c_3)<val(r_2,c_2)<val(r_1,c_1)<val(r_3,c_3)$,产生矛盾,故$(r_1,c_1)$一定是峰值点。