文章目录
一、前言
二、递推
首先让我们来看一下,零基础学习动态规划前必须要看的一道题。
1、斐波那契数列
1)题目描述
2)算法分析
3)源码详解
int fib(int n) {
int i; // (1)
int f[31] = {0, 1}; // (2)
for(i = 2; i <= n; ++i) { // (3)
f[i] = f[i-1] + f[i-2]; // (4)
}
return f[n]; // (5)
}
- ( 1 ) (1) (1) 首先定义一个循环变量;
- ( 2 ) (2) (2) 再定义一个数组记录斐波那契数列的第 n n n 项,并且初始化第 0 0 0 项 和 第 1 1 1 项。
- ( 3 ) (3) (3) 然后一个 for 循环,从第 2 项开始;
- ( 4 ) (4) (4) 利用递推公式逐步计算每一项的值;
- ( 5 ) (5) (5) 最后返回第 n n n 项即可。
4)简单复盘
2、爬楼梯
1)题目描述
2)算法分析
3)源码详解
int climbStairs(int n){
int i; // (1)
int f[46] = {1, 1}; // (2)
for(i = 2; i <= n; ++i) { // (3)
f[i] = f[i-1] + f[i-2]; // (4)
}
return f[n]; // (5)
}
- ( 1 ) (1) (1) 首先定义一个循环变量;
- ( 2 ) (2) (2) 再定义一个数组 f [ i ] f[i] f[i] 代表从第 0 0 0 阶爬到第 i i i 阶的方案数;
- ( 3 ) (3) (3) 然后一个 for 循环,从第 2 项开始;
- ( 4 ) (4) (4) 利用递推公式逐步计算每一项的值;
- ( 5 ) (5) (5) 最后返回第 n n n 项即可。
4)简单复盘
三、线性DP
递推也是某种意义上的线性DP,线性DP的最大特征就是状态是用一个一维数组表示的,一般状态转移的时间复杂度为
O
(
1
)
O(1)
O(1) 或者
O
(
n
)
O(n)
O(n)。
让我们来看一个线性DP的经典例子来加深理解。
1、使用最小花费爬楼梯
1)题目描述
2)算法分析
3)源码详解
int min(int a, int b) {
return a < b ? a : b; // (1)
}
int minCostClimbingStairs(int* cost, int n){
int i; // (2)
int f[1001] = {0, 0}; // (3)
for(i = 2; i <= n; ++i) { // (4)
f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2]);
}
return f[n]; // (5)
}
- ( 1 ) (1) (1) 为了方便求最小值,我们实现一个最小值函数 m i n min min,直接利用 C语言 的 条件运算符 就可以了;
- ( 2 ) (2) (2) 然后开始动态规划的求解,首先定义一个循环变量;
- ( 3 ) (3) (3) 再定义一个数组 f [ i ] f[i] f[i] 代表从第 0 0 0 阶爬到第 i i i 阶的最小花费,并且初始化第 0 0 0 项 和 第 1 1 1 项;
-
(
4
)
(4)
(4) 然后一个
for
循环,从第 2 2 2 项开始,直接套上状态转移方程就能计算每一项的值了; - ( 5 ) (5) (5) 最后返回第 n n n 项即可;
4)简单复盘
2、打家劫舍
1)题目描述
2)算法分析
3)源码详解
int max(int a, int b) {
return a > b ? a : b; // (1)
}
int rob(int* nums, int n){
int i; // (2)
int dp[110];
dp[0] = nums[0]; // (3)
for(i = 1; i < n; ++i) { // (4)
if(i == 1) { // (5)
dp[1] = max(nums[0], nums[1]);
}else { // (6)
dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
}
}
return dp[n-1]; // (7)
}
- ( 1 ) (1) (1) 首先要求的是最大值,所以我们用条件运算符简单实现一个求最大值的函数;
- ( 2 ) (2) (2) 然后开始动态规划的求解,首先定义一个循环变量;
- ( 3 ) (3) (3) 定义一个状态数组 d p [ i ] dp[i] dp[i],初始状态 d p [ 0 ] dp[0] dp[0] 为 n u m s [ 0 ] nums[0] nums[0];
- ( 4 ) (4) (4) 然后一层循环执行状态转移;
-
(
5
)
(5)
(5) 为了防止调用
d
p
[
i
−
2
]
dp[i-2]
dp[i−2] 时的数组下标越界,当
i == 1
的情况需要特殊处理,也比较简单啦,就是要么取第 0 个要么取第 1 个; -
(
6
)
(6)
(6) 当
i > 1
时直接套用刚才研究出来的状态转移方程就可以啦; - ( 7 ) (7) (7) 最后返回 d p [ n − 1 ] dp[n-1] dp[n−1] 就是我们要求的答案了;
4)简单复盘
3、删除并获得点数
1)题目描述
2)算法分析
3)源码详解
int max(int a, int b) {
return a > b ? a : b;
}
int rob(int* nums, int n){
int i;
int dp[10010];
dp[0] = nums[0];
for(i = 1; i < n; ++i) {
if(i == 1) {
dp[1] = max(nums[0], nums[1]);
}else {
dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
}
}
return dp[n-1];
}
int deleteAndEarn(int* nums, int n){
int i; // (1)
int sum[10010], val[10010]; // (2)
memset(sum, 0, sizeof(sum)); // (3)
for(i = 0; i < n; ++i) {
++sum[ nums[i] ]; // (4)
}
for(i = 0; i <= 10000; ++i) {
val[i] = i * sum[i]; // (5)
}
return rob(val, 10001); // (6)
}
- ( 1 ) (1) (1) 首先定义一个循环变量;
- ( 2 ) (2) (2) 然后定义两个辅助数组 s u m [ i ] sum[i] sum[i] 和 v a l [ i ] val[i] val[i], 后面我会解释它们的作用。
- ( 3 ) (3) (3) 将 s u m sum sum 数组利用 m e m s e t memset memset 归零;
- ( 4 ) (4) (4) 一层循环将所有数字映射到 s u m sum sum 数组中, s u m [ i ] sum[i] sum[i] 的值代表的是 i i i 在数组中的个数;
- ( 5 ) (5) (5) 然后填充 v a l [ i ] val[i] val[i], v a l [ i ] val[i] val[i] 的值代表选取 i i i 这个数以后能够获得的点数,当然就是它本身的值乘上它的个数,即 i × s u m [ i ] i \times sum[i] i×sum[i];
- ( 6 ) (6) (6) 然后直接把 打家劫舍的代码拷过来,修改一下数组范围,直接调用即可;
4)简单复盘
4、最大子数组和
1)题目描述
2)算法分析
3)源码详解
int max(int a, int b) {
return a > b ? a : b; // (1)
}
int maxSubArray(int* nums, int n){
int i; // (2)
int dp[100001]; // (3)
int maxValue = nums[0]; // (4)
dp[0] = nums[0]; // (5)
for(i = 1; i < n; ++i) { // (6)
dp[i] = max(dp[i-1] + nums[i], nums[i]);
maxValue = max(maxValue, dp[i]);// (7)
}
return maxValue; // (8)
}
- ( 1 ) (1) (1) 定义一个求最大值的函数;
- ( 2 ) (2) (2) 定义一个循环变量;
- ( 3 ) (3) (3) 定义一个状态数组 d p [ i ] dp[i] dp[i] ;
- ( 4 ) (4) (4) 定义一个我们要返回的最大值,初始化为第一个整数的值;
- ( 5 ) (5) (5) 计算初始状态 d p [ 0 ] dp[0] dp[0];
- ( 6 ) (6) (6) 利用一层循环,执行状态转移;
- ( 7 ) (7) (7) 然后在所有以第 i i i 个整数结尾的子数组中取一个最大值;
- ( 8 ) (8) (8) 最后返回这个最大值就是我们要求的答案了;
4)简单复盘
这道题显然和前面的题难度有所增加,可以多看几遍,多想想,利用所有的碎片时间来进行学习,迟早会想出来的,那么好好想想吧,祝你好运!
四、总结复盘
1、状态
如果你学过编译原理,那么你应该会知道 DFA (有限状态自动机),没错,这里的状态就可以理解成状态机中的状态,即 DFA 上的某个结点。
2、状态转移
状态转移则对应了 DFA 上的边,即从一个状态到另一个状态,边上也有可能有条件,也就对应了状态转移的条件。
3、时间复杂度
动态规划的时间复杂度分为两部分:状态计算的时间复杂度,每个状态的状态转移时间复杂度。
所有状态计算的时间复杂度为
O
(
a
)
O(a)
O(a),单个状态的状态转移时间复杂度为
O
(
b
)
O(b)
O(b),则整个动态规划的求解过程的时间复杂度就是
O
(
a
b
)
O(ab)
O(ab)。
线性DP 的状态数就是
O
(
n
)
O(n)
O(n),状态转移的时间复杂度一般为
O
(
1
)
O(1)
O(1) 或者
O
(
n
)
O(n)
O(n),也有
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n) 的,可能利用二分枚举进行状态转移,比如最长单调子序列。
4、线性DP拓展
常见的线性DP有最长单调子序列、前缀最值、前缀和、背包问题等等。
5、刷题路线
可以通过公众号 「 夜深人静写算法 」 回复「 题集 」 获取。