文章目录
- 分治法
- 分治策略
- 分治算法的效率分析
- 归并排序
- 具体应用问题
- ==最大子数组问题==
- 蛮力法
- 分治法
- 找跨越中间位置的最大子数组
- 时间复杂度
- 矩阵乘法
- 蛮力算法
- 分治法
- Strassen矩阵乘法
分治法
- 方法
- 分治策略
- 分治法效率分析——迭代法(递归树法)
- 分治法效率分析——主定理方法
注:这两个效率分析的方法,详见另一篇博客“数据结构与算法(一)——绪论”。
https://blog.csdn.net/weixin_44421143/article/details/132193847
-
问题
-
最大子数组问题
-
矩阵乘法的Srassen算法
-
最近点对问题
-
凸包问题
-
分治策略
- 分治法思想
将原问题分成n个规模较小而结构与原问题相似的子问题,递归地解这些子问题,然后合并其结果就得到原问题的解。
-
分而治之(divide - and - conquer)
-
分解(Divide)
原问题分为若干子问题,这些子问题是原问题的规模较小的实例。
-
解决(Conquer)
递归地求解各子问题。若子问题足够小,则直接求解。
-
合并(Combine)
将子问题的解合并成原问题的解。
关键在于合并处理。
-
分治算法的效率分析
- 用递归式分析分治算法的运行时间。
- 一个递归式是一个函数,它由一个或多个基本情况(base case),它自身,以及小参数组成。
- 递归式的解可以用来近似算法的运行时间。
T ( n ) T(n) T(n)本身表示的就是时间代价。对于函数 T ( n ) T(n) T(n)而言,它的时间代价是基于 T ( n − 1 ) T(n-1) T(n−1)的基础之上的,但是加 1 1 1。
为什么要加1,因为它是在 T ( n − 1 ) T(n-1) T(n−1)所需要花费的时间代价之外还需多执行一个乘法操作。
这就形成了一个递归关系式。
递归关系式体现的是,我的大问题,和小问题,之间的关系。
那我知道 T ( n ) = T ( n − 1 ) + 1 T(n)=T(n-1)+1 T(n)=T(n−1)+1,但是 T ( n − 1 ) T(n-1) T(n−1)我也不知道啊,我还是不知道啊?
你一直往下递推,一直递归到 T ( 1 ) T(1) T(1)。最后 T ( n ) T(n) T(n)具体的时间代价不就求出来了。
归并排序
初始序列我不断地分成两个子问题。直到分解到最后,每个子序列里只剩一个了。
然后开始合并,两两合并。
具体应用问题
最大子数组问题
问题:
- 输入:数值数组
A[1..n]
- 假设数组中存在负数
- 如果数组中全是非负数,该问题很简单。
- 输出:数组下标
i
和j
使得子数组A[i..j]
为A[1..n]
的和最大的非空连续子数组。
啥意思呢?
-
考虑下面的情景:
- 一只股票连续n天的交易价格。
- 什么时候该买入?什么时候该卖出?
-
这个问题就可以转换成最大子数组问题。
定义:A[i] = (第i天的价格) - (第i-1天的价格)
即,我今天的价格,比昨天涨了多少(正数)、跌了多少(负数)。
- 如果最大子数组是
A[i..j]
- 第i天买入
- 第j天卖出
即,我到底从哪天买入、到哪天卖出,我从而能够赚的最多。
例子1
- 一支股票连续n天的交易价格:
看右边的表格,它的最后一行,就对应于整个数组A[1..n]
。
什么是最大子数组,就是,我到底是哪天买入(i
)、哪天卖出(j
),才是最赚的情况,对应于最大子数组A[i..j]
。
进一步想一下,“最赚的”是啥意思。——就是最终卖出价格与最初买入价格之间的差最大。——或者说,是A[i]+...+A[j]
这些数求和为最大值。
- 对于这个例子,其最大子数组为
A[3..3]
。即第三天当天买入,当天卖出。
例子2
- 一支股票连续n天的交易价格:
- 最大子数组是
A[8..11]
。
咋看出来的??——若从图像上直观来看,可以理解为,图像的某一个极小值点与某一个极大值点之间的差为最大的情况(前提:极小值点的选取要从时间上位于极大值点前)。所以从上图,若直观的观察,也能观察出,第7天对应的一个很低价买入,第11天对应的很高价卖出,获利会是最大了。——为啥数组是A[8]
,这有可能是因为,我们具体的数组存放,和我们所谓的第7天,它不是完全对照的,它可能是有个对应关系。
关键就看计算机咋实现了。
我们在解决问题的时候,一般都是从蛮力法开始。我们不会一上来就想象出来一种很精妙的办法。
蛮力法
你不是有n个元素么,我不就是要找出最大差值么?
那我就从第一个元素开始:A[1..1]
是多少,A[1..2]
是多少,A[1..3]
是多少,……A[1..n]
是多少。——n次
然后从第二个元素开始:A[2..2]
,A[2..3]
,A[2..4],……,A[2..n]
。——n-1次
…………
从第n-1个元素开始:A[n-1..n-1]
,A[n-1..n]
。——2次
从第n个元素开始:A[n..n]
。——1次
蛮力法是啥,我就是把所有情况列出来么。
总共有 1 + 2 + . . . + n = n ( n + 1 ) 2 1+2+...+n=\frac{n(n+1)}{2} 1+2+...+n=2n(n+1)种可能性。
但是,对于其中的操作,我们总是要做一个求和的操作。对于“从第一个元素开始”下的情况,我们对A[1..1]
,一直求到A[1..n]
,分别为1、2、3、…n个数求和。——我们对于n个数字,前1、2、…、n个数之和的求和,要获取这n个值,共需要进行
n
2
n^2
n2次的操作。而我们又不仅仅需要算“第一个元素开始”的情况,也要算其他的情况,所以总的时间代价还要再乘上
n
n
n,为
O
(
n
3
)
O(n^3)
O(n3)。
这个求和操作,能否优化一下?——求前缀和。
实际上,你求完A[1..1]
得到的和(第一个数字),之后你在算A[1..2]
之和时(前两个数之和)就完全不必从头加起,而是由上一个的结果基础之上加一个数即可。因此,我们对于n个数字,获取前1、2、…、n个数之和,这n个值,只需要进行
n
n
n次的操作。因此,总共的算法为
O
(
n
2
)
O(n^2)
O(n2)。
我们既然蛮力法已经会求了——时间复杂度 O ( n 2 ) O(n^2) O(n2)。
那么我们再来看看,这个问题用分治法怎么求?
分治法
分治法,同样也是个,n个元素的数组。
但是,我对这个数组,我考虑的是,把它从中间分成两半。——三个位置: l o w , h i g h , m i d low,high,mid low,high,mid。
我的一个初步的想法:左半边数组,找到它的最大子数组A[i1, j1]
,右半边数组它的最大子数组A[i2,j2]
。然后再看看这两个最大子数组谁更大,最终就是整个数组的最大子数组了。
但是,稍加思考,这个思路就明显有个问题:我的最大子数组凭什么就一定是从左半边某处起、左半边某处停止;或者右半边某处起、右半边某处止?——我完全有可能是横跨mid的情况啊。
所以对这个问题的分治法处理,还是有点麻烦的了。因为它除了对左右两边分别处理之外,还有一个对于“中间”处理的过程。
该问题的分治解法总结如下:
- 子问题:找出
A
[
l
o
w
.
.
h
i
g
h
]
A[low..high]
A[low..high]的最大子数组。
- 参数初始值: l o w = 1 , h i g h = n . low=1,high=n. low=1,high=n.
- 分解。将子数组分解成两个大小基本相同的子数组。
- 找到子数组的中间位置 m i d mid mid,将子数组分成两个更小的子数组 A [ l o w . . m i d ] A[low..mid] A[low..mid]和 A [ m i d + 1.. h i g h ] A[mid+1..high] A[mid+1..high]。
- 求解。找数组 A [ l o w . . h i g h ] A[low..high] A[low..high]和 A [ m i d + 1.. h i g h ] A[mid+1..high] A[mid+1..high]的最大子数组。
- 组合。找出跨越中间位置的最大子数组。
- 三种情况取和最大的子数组(跨越中间位置的最大子数组和“求解”步骤中找到的那两个最大子数组)。
这时候会发现,问题在于,跨越中间位置的最大子数组咋找啊?
找跨越中间位置的最大子数组
- 子数组必须跨越中间位置。
- 解决思路:
- 任何一个跨越中间位置 A [ m i d ] A[mid] A[mid]的子数组 A [ i . . j ] A[i..j] A[i..j]由两个更小的子数组 A [ i . . m i d ] A[i..mid] A[i..mid]和 A [ m i d + 1.. j ] A[mid+1..j] A[mid+1..j]组成,其中 l o w ≤ i ≤ m i d ≤ j ≤ h i g h low≤i≤mid≤j≤high low≤i≤mid≤j≤high。
- 只要找到最大子数组 A [ i . . m i d ] A[i..mid] A[i..mid]和 A [ m i d + 1.. j ] A[mid+1..j] A[mid+1..j],然后把它们合并。
- 注意: m i d mid mid是固定的,左右分别扫描即可。这个问题可以用 θ ( n ) \theta(n) θ(n)的时间解决。
(可能上面的说法还是有点抽象,下面形象一点来解释)
实际上对于这一块,也是靠蛮力了。
m i d mid mid是一个已经固定好的位置。我从 m i d mid mid出发,往左边找:我往左1个、往左2个、往左3个、……一直往左到 l o w low low的位置为止。我们找啥?——我们找,这所有情况里,哪种是最大的。往右边找是同理的。
对于这两边的蛮力找法,总共操作也就是 O ( n ) O(n) O(n)的时间。
这样一来,我们通过对以 m i d mid mid为终点,左边起点所有情况的遍历,找出最大的那一情况: A [ i . . m i d ] A[i..mid] A[i..mid]。通过对以 m i d mid mid为起点,右边终点所有情况的遍历,找出最大的那一情况: A [ m i d + 1.. j ] A[mid+1..j] A[mid+1..j]。把这两个连起来,就是一个跨越 m i d mid mid的最大子数组了。
时间复杂度
时间复杂度为: T ( n ) = 2 T ( n / 2 ) + O ( n ) T(n)=2T(n/2)+O(n) T(n)=2T(n/2)+O(n)。
其中, O ( n ) O(n) O(n)是蛮力解决跨界子数组的时间。
中间的推理过程不写了,最后结果如下图所示。
矩阵乘法
具体的矩阵乘法定义就不说了,学过线性代数的都知道。
蛮力算法
Matrix operator*(const Matrix &m, const Matrix &n) {
if(m.l_size()!=n.h_size()) return Matrix(); //非法运算返回空矩阵
Matrix ans(m.h_size(), n.l_size());
for(int i=0; i!=ans.h_size(); ++i) {
for(int j=0; j!=ans.l_size(); ++j) {
for(int k=0; k!=m.l_size(); ++k) {
ans[i][j] += m[i][k]*n[k][j];
}
}
}
return ans;
}
通过看这个代码,或者根据矩阵乘法的定义来想一下,很容易能想到,蛮力法是 O ( n 3 ) O(n^3) O(n3)。
那既然蛮力法不好,那怎么写比较好呢?——还是分治呗。
分治法
- 分治法:
- 将矩阵 A A A, B B B和 C C C中每一矩阵都分块成 4 4 4个大小相等的子矩阵。由此,可将方程 C = A B C=AB C=AB重写为:
由此可得:
注意:只关心乘法的执行次数。
我们把一个矩阵,分解成8个子矩阵的相乘问题。(只关心乘法操作)至于其他的操作,如相加操作,它们可视为一个常量时间 O ( 1 ) O(1) O(1)。
由此可得: T ( n ) = 8 T ( n / 2 ) + O ( 1 ) T(n)=8T(n/2)+O(1) T(n)=8T(n/2)+O(1)
时间复杂度
具体的推导过程不写了,最后得到,这种方法得到的时间复杂度为 T ( n ) = θ ( n 3 ) T(n)=\theta(n^3) T(n)=θ(n3)。
我们会发现,它跟蛮力法时间复杂度一样了,没改进啊。
想一下,我们都用分治法了,为啥没有改进。因为乘8。——具体原因,看一看主定理分析时间复杂度那里,有个 l o g b a log_ba logba的问题。
Strassen矩阵乘法
- 为了降低时间复杂度,必须减少乘法的次数。
对于矩阵的加法,我们可以理解为一个 O ( n ) O(n) O(n)时间的操作。
他这个人研究的这个算法,为啥好?就是因为他只做了 7 7 7次矩阵乘法,其余的均为 O ( 1 ) O(1) O(1)的矩阵加法操作。
由此可得: T ( n ) = 7 T ( n / 2 ) + O ( 1 ) T(n)=7T(n/2)+O(1) T(n)=7T(n/2)+O(1)。
具体推理过程不写了,最终得到它这个算法的时间复杂度 ≈ n 2.807 ≈n^{2.807} ≈n2.807。