分治法思想
分治法的适用条件:
- 问题的规模缩小到一定程度就可以容易地解决;
- 问题可以分解为若干个规模较小的相同问题,即问题具有最优子结构性质;
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
满足前三条即可用分治法;如果只满足前两条,则考虑用贪心或动态规划思想;如果还满足第四条,子问题之间包含公共子问题,此时用分治法的效率比较低,因为公共子问题被重复多次计算,一般使用动态规划思想比较好。
分治法解题的步骤:
- 分解,将原问题分解成一系列规模较小的子问题;
- 解决,递归地解各子问题,如果子问题足够小,则直接求解;
- 合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。
动态规划思想
对于上面分治法的适用条件,当满足条件四时使用分治法求解问题时会重复求解公共子问题,这使得分治法的效率变低了。
动态规划思想的核心就是在划分子问题并求解子问题后保存所有子问题的解,在求解其他子问题时,如果遇到前面已经求解过的子问题(即公共子问题),只需直接查找我们保存的该子问题的解即可。
动态规划思想的有效性依赖于问题本身具有的两个重要性质:最优子结构性质和子问题重叠性质。
- 最优子结构性质:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质;
- 子问题重叠性质:指有些子问题在递归求解时被多次计算,这些子问题被称为公共子问题。
动态规划算法的步骤:
- 描述最优解的结构;
- 递归定义最优解的值;
- 采用自底向上的方式计算问题的最优值;
- 由计算出的结果构造一个最优解。
分治法求斐波那契数列第n个数
采用分治法计算斐波那契数列第n个数时,递归的子问题中含有大量重复子问题,主要表现在计算Fibonacci(n - 1)和Fibonacci(n - 2)时需要计算更小的子问题,而这些子问题被计算过多次,因此效率很低。
#include <cstdio>
#include <vector>
using namespace std;
class Solution {
public:
int Fibonacci(int n) {
if (n == 1 || n == 2)
return 1;
else
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
};
int main() {
int n = 10;
Solution s;
int sum = s.Fibonacci(n);
printf("%d ", sum);
return 0;
}
动态规划法求斐波那契数列第n个数
采用动态规划的方法求斐波那契数列第n个数时,我们从第3个数开始依次向后计算,并保存前面计算过的子问题的结果,这样每个子问题只需要计算一次即可,最后从第3个数一直计算到我们所需的第n个数就是结果。
#include <cstdio>
#include <vector>
using namespace std;
class Solution {
public:
int Fibonacci(int n) {
vector<int> r(n, 0);
r[0] = r[1] = 1;
for (int i = 2; i < n; i++) {
r[i] = r[i - 1] + r[i - 2];
}
return r[n - 1];
}
};
int main() {
int n = 10;
Solution s;
int sum = s.Fibonacci(n);
printf("%d ", sum);
return 0;
}
这样我们的时间复杂度为O(n),空间复杂度也为O(n)。
动态规划法求斐波那契数列第n个数的改进
由于我们最终只需要第n个数的结果,前n-1个数的结果我们并不需要,而上面的计算方法中,我们将前n-1个数的结果都保存了下来,这对于空间是一种浪费。
我们可以采用迭代更新的方式,只用a1和a2两个变量记录每一轮for循环时第n-1和第n个数的值。
#include <cstdio>
#include <vector>
using namespace std;
class Solution {
public:
int Fibonacci(int n) {
int a1=1,a2=1;
for (int i = 3; i <= n; i++) {
a2=a1+a2;
a1=a2-a1;
}
return a2;
}
};
int main() {
int n = 10;
Solution s;
int sum = s.Fibonacci(n);
printf("%d ", sum);
return 0;
}
这样我们的时间复杂度为O(n),空间复杂度为O(1)。
矩阵乘法角度:斐波那契数列的O(logn)解法
斐波那契数列还可以写成矩阵乘法的形式:
$$
\left[ \begin{matrix}{a_{i-1}} & {a_{i-2}}\end{matrix}\right] \left[ \begin{matrix}{1} & {1} \\ {1} & {0}\end{matrix}\right]=\left[ \begin{matrix}{a_{i}} & {a_{i-1}}\end{matrix}\right] \quad(i \geq 2)
$$
于是有:
$$
\left[ \begin{matrix}{a_{1}} & {a_{0}}\end{matrix}\right] \left( \left[ \begin{matrix}{1} & {1} \\ {1} & {0}\end{matrix}\right] \right)^{n-1}=\left[ \begin{matrix}{a_{n}} & {a_{n-1}}\end{matrix}\right] \quad(n \geq 1)
$$
这样就把Fib数列问题转化成了一个求矩阵幂的运算。计算a的n次方最简单的方法就是a连乘n次,但这样的时间复杂度为O(n)。我们可以把a的n次方拆成下面的形式:
当n为偶数时
$$
a^{n}=a^{n / 2 } a^{n / 2}
$$
当n为奇数时
$$
a^{n}=a^{(n-1) / 2} a^{(n-1) / 2}
$$
要求a的n次方,我们先求a的n/2次方,再把n/2的结果平方一下。如果把求n次方的问题看成一个大问题,把求n/2看成一个较小的问题。我们可以使用分治法的思路来求a的n次方,这样就只需要logn次运算。
#include <cstdio>
#include <cassert>
using namespace std;
// 定义2x2矩阵
struct Matrix2By2 {
Matrix2By2(int m00 = 0, int m01 = 0, int m10 = 0, int m11 = 0) : m_00(m00), m_01(m01), m_10(m10), m_11(m11) {}
int m_00;
int m_01;
int m_10;
int m_11;
};
// 定义2x2矩阵乘法
Matrix2By2 MatrixMultiply(const Matrix2By2 &matrix1, const Matrix2By2 &matrix2) {
return {
matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10,
matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11,
matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10,
matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * matrix2.m_11};
}
// 分治法计算n个2x2矩阵相乘
Matrix2By2 MatrixPower(int n) {
Matrix2By2 matrix;
if (n == 1) {
matrix = Matrix2By2(1, 1, 1, 0);
} else if (n % 2 == 0) {
matrix = MatrixPower(n / 2);
matrix = MatrixMultiply(matrix, matrix);
} else if (n % 2 == 1) {
matrix = MatrixPower((n - 1) / 2);
matrix = MatrixMultiply(matrix, matrix);
matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0));
}
return matrix;
}
// 计算斐波那契数列第n个数
int Fibonacci(int n) {
int result[2] = {1, 1};
// 如果n为1或2
if (n <= 2)
return result[n - 1];
Matrix2By2 PowerNMinus2 = MatrixPower(n - 1);
return PowerNMinus2.m_00;
}
int main() {
int n = 10;
int sum = Fibonacci(n);
printf("%d ", sum);
return 0;
}