递推动态规划入门
- [No1 斐波那契数](https://leetcode-cn.com/problems/fibonacci-number/)
- [No2 区域和检索-数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/)
- [No3 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/)
- [No4 使用最小花费爬楼梯](https://leetcode-cn.com/problems/min-cost-climbing-stairs/)
- [No5 按摩师](https://leetcode-cn.com/problems/the-masseuse-lcci/)
- [No6 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)
- [No7 不同路径](https://leetcode-cn.com/problems/unique-paths/)
- [No8 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/)
- [No9 三角形最小路径和](https://leetcode-cn.com/problems/triangle/)
- [No10 比特位计数](https://leetcode-cn.com/problems/counting-bits/)
最近有刷一些算法题来提高自己,想想曾经入门算法竞赛的时候看着大佬写的完全看不懂的公式,不免神往而又不得不感慨:这特么都能想的出来233(虽然到现在我任然没有达到那样的高度),好了废话不多说开始正题。
以下题目均来自leetcode
No1 斐波那契数
没错!就是巨简单无比c语言教材上的斐波那契数!哈哈
分析:
F
[
N
]
=
F
[
N
−
1
]
+
F
[
N
−
2
]
F\left[N\right]=F[N-1]+F[N-2]
F[N]=F[N−1]+F[N−2]
F
[
0
]
=
0
F[0]=0
F[0]=0
F
[
1
]
=
1
F[1]=1
F[1]=1
咳咳!我承认我编不下去了,没啥好分析的直接上代码吧
int fib(int N){
int f[31] = {0, 1}; //0 <= N <= 30
for(int i = 2; i <= N; i++)
f[i] = f[i - 1] + f[i - 2];
return f[N];
}
优化: 其实我们发现计算 F [ i ] F[i] F[i]仅仅需要 F [ i − 1 ] F[i-1] F[i−1]和 F [ i − 2 ] F[i-2] F[i−2],那么其实我们只需要3个变量就可以了
int fib(int N){
if(N < 2) return N;
int a = 0, b = 1, c;
for(int i = 2; i <= N; i++){
c = a + b;
a = b;
b = c;
}
return c;
}
附加: 第N个泰波那契数这题和斐波那契一样的
No2 区域和检索-数组不可变
分析: 可以直接暴力求解,每一次计算i到j的和
#python:用时6884ms
class NumArray:
def __init__(self, nums: List[int]):
self.nums = nums
def sumRange(self, i: int, j: int) -> int:
ans = 0
for k in range(i, j + 1):
ans += self.nums[k]
return ans
优化: sumRange会被调用多次,每一次都从i迭代到j会花费大量时间,那么有没有办法可以直接查i到j的值呢?答案是有的,那就是在初始化的时候我们就计算了每一种情况的和,这样的空间复杂度将会是
O
(
n
2
)
O(n^2)
O(n2)而时间复杂度为
O
(
1
)
O(1)
O(1)。那么有没有办法降低空间复杂度呢?想象有两根长度不等的绳子,如果叫你求差值,你只需要用长的减短的就可以了,如果我们用ans[i]来表示从下标0…i的和,那么i到j的值就很好求了
公式:
s
u
m
=
a
n
s
[
j
]
−
a
n
s
[
i
−
1
]
sum = ans[j] - ans[i-1]
sum=ans[j]−ans[i−1]
class NumArray:
def __init__(self, nums: List[int]):
if len(nums) > 0:
self.ans = [nums[0]]
for i in range(1, len(nums)):
self.ans.append(self.ans[i - 1] + nums[i])
#print(self.ans)
def sumRange(self, i: int, j: int) -> int:
if i == 0:
return self.ans[j]
return self.ans[j] - self.ans[i - 1]
No3 爬楼梯
分析: 假设现在台阶只有1阶,那么你只需要一步就可以爬到楼顶;如果有2阶,那么你可以选择直接走两步到楼顶或者从第一阶台阶走一步到楼顶;如果有3阶那么你可以选择从第一阶台阶直接跨两步到楼顶或者从第2阶台阶跨一步到楼顶。OK,那么现在思路就来了假设有n阶台阶,最后一步一定是从第n-1阶台阶走上去的或者是从第n-2阶台阶走上去的 ,那么现在又相当于回到了斐波那契数列,走上第n阶台阶的方法等于走上第n-1阶台阶和第n-2阶台阶的和
公式: F [ N ] = F [ N − 1 ] + F [ N − 2 ] ; F[N] = F[N-1] + F[N - 2]; F[N]=F[N−1]+F[N−2]; F [ 0 ] = 0 , F[0]=0, F[0]=0, F [ 1 ] = 1 , F[1]=1, F[1]=1, F [ 2 ] = 2 F[2]=2 F[2]=2
int climbStairs(int n){
int f[100] = {0, 1, 2}; //n没有给条件尽量选个大点的数
for(int i = 3; i <= n; i++)
f[i] = f[i - 1] + f[i - 2];
return f[n];
}
优化: 同样的只需要保留 F [ i − 1 ] F[i-1] F[i−1]和 F [ i − 2 ] F[i-2] F[i−2]
int climbStairs(int n){
if(n < 3) return n;
int a = 1, b = 2, c;
for(int i = 3; i <= n; i++){
c = a + b;
a = b;
b = c;
}
return c;
}
附加: 三步问题其实这一题和上一题一模一样
No4 使用最小花费爬楼梯
分析: 其实这题和上面的爬楼梯差不多,只不过加了点条件。题目有说可以选择从索引为0或索引为1的元素开始,这里就给了我们一些陷阱让我们不太容易推导,因为无法确定是从0开始好还是从1开始好。但是有一个点是不变的,那就是终点,无论从0开始还是从1开始我们的目的都是要爬上n阶楼梯,那么我们可以倒过来想:我们要从第n阶台阶走到第0或1阶台阶,要走到第n-2阶台阶可以从第n-1和n阶台阶下来,那么我们是从n上下来呢还是n-1下来好呢?当然是哪个cost小从哪个下来,那么现在思路清晰了
公式:
F
[
N
−
2
]
=
c
o
s
t
[
N
−
2
]
+
m
i
n
(
F
[
N
−
1
]
,
F
[
N
]
)
;
F[N - 2] = cost[N - 2] + min(F[N - 1],F[N]);
F[N−2]=cost[N−2]+min(F[N−1],F[N]);
F
[
N
]
=
c
o
s
t
[
N
]
,
F[N]=cost[N],
F[N]=cost[N],
F
[
N
−
1
]
=
c
o
s
t
[
N
−
1
]
F[N - 1]=cost[N - 1]
F[N−1]=cost[N−1] 最终的答案就是
m
i
n
(
F
[
0
]
,
F
[
1
]
)
min(F[0], F[1])
min(F[0],F[1])
用F[N]来表示走到第N阶台阶的花费
int minCostClimbingStairs(int* cost, int costSize){
int f[1002] = {0};
f[costSize - 1] = cost[costSize - 1];
f[costSize - 2] = cost[costSize - 2];
for(int i = costSize - 3; i >= 0; i--){
f[i] = cost[i] + (f[i + 1] < f[i + 2] ? f[i + 1] : f[i + 2]);
}
return f[0] < f[1] ? f[0] : f[1];
}
优化: 其实可以直接使用cost数组
int minCostClimbingStairs(int* cost, int costSize){
for(int i = costSize - 3; i >= 0; i--){
cost[i] += cost[i + 1] < cost[i + 2] ? cost[i + 1] : cost[i + 2];
}
return cost[0] < cost[1] ? cost[0] : cost[1];
}
No5 按摩师
分析: 每个预约有两种状态:接收或不接受。对于第i个预约,如果接受那么第i-1个预约必须是拒绝的;如果不接受那么对于第i-1个预约则没有限制,那么这个时候就必须选择对于第i-1个预约是接受好呢还是不接受好呢,当然是哪个的时间长选哪个。用sum[i][0]表示第i个预约不接受的时候的总时长,用sum[i][1]表示第i个预约接受的时候的总时长。那么现在思路就清晰了
公式: s u m [ i ] [ 0 ] = m a x ( s u m [ i − 1 ] [ 0 ] , s u m [ i − 1 ] [ 1 ] ) sum[i][0] = max(sum[i-1][0], sum[i-1][1]) sum[i][0]=max(sum[i−1][0],sum[i−1][1])
s u m [ i ] [ 1 ] = n u m s [ i ] + s u m [ i − 1 ] [ 0 ] , s u m [ 0 ] [ 0 ] = 0 , s u m [ 0 ] [ 1 ] = n u m s [ 0 ] sum[i][1] = nums[i] + sum[i-1][0], sum[0][0]=0,sum[0][1]=nums[0] sum[i][1]=nums[i]+sum[i−1][0],sum[0][0]=0,sum[0][1]=nums[0]
int massage(int* nums, int numsSize){
if(numsSize == 0)return 0;
int sum[100000][2] = {{0, nums[0]}};
for(int i = 1; i < numsSize; i++){
sum[i][0] = sum[i-1][0] > sum[i-1][1] ? sum[i-1][0] : sum[i-1][1];
sum[i][1] = nums[i] + sum[i-1][0];
}
return sum[numsSize-1][0] > sum[numsSize-1][1] ? sum[numsSize-1][0] : sum[numsSize-1][1];
}
优化: 我们发现每一次迭代用到的其实之后前一个预约的结果,那么我们就只需要保留一个预约的结果就行了,只不过需要借助一个中间变量记录一下上一个预约不接受的结果。
int massage(int* nums, int numsSize){
if(numsSize == 0)return 0;
int sum[2] = {0, nums[0]};
int temp = sum[0];
for(int i = 1; i < numsSize; i++){
sum[0] = sum[0] > sum[1] ? sum[0] : sum[1];
sum[1] = nums[i] + temp;
temp = sum[0];
}
return sum[0] > sum[1] ? sum[0] : sum[1];
}
附加: 打家劫舍这题其实和按摩师一模一样
No6 买卖股票的最佳时机
分析: 假设我们在第i天卖出股票,为了使利益最大化那么我们就需要在第i天之前价格最低的时候买入,用f[i]表示第i天卖出的结果,答案就是max(f)。那么现在思路就很清晰了,直接给出公式。
公式: f [ i ] = p r i c e s [ i ] − m i n ( p r i c e s [ 0 ] , p r i c e s [ 1 ] . . . , p r i c e s [ i − 1 ] ) , f [ 0 ] = 0 f[i] = prices[i] - min(prices[0],prices[1]...,prices[i-1]), f[0]=0 f[i]=prices[i]−min(prices[0],prices[1]...,prices[i−1]),f[0]=0
#python:用时6248ms 结果太磕碜了,但证明了我们思路没错
class Solution:
def maxProfit(self, prices: List[int]) -> int:
f = [0]
for i in range(1, len(prices)):
f.append(max(0, prices[i] - min(prices[0:i])))
return max(f)
优化: 其实我们可以用一个变量来记录第i天之前的最低价格
#python:用时64ms 速度大大提升
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if prices == []:
return 0
f = [0]
minp = prices[0]
for i in range(1, len(prices)):
f.append(prices[i] - minp)
if minp > prices[i]:
minp = prices[i]
return max(f)
其实我们还可以省略f数组,因为我们需要的只是f数组中的最大值,直接用一个变量表示
int maxProfit(int* prices, int pricesSize){
if(pricesSize == 0) return 0;
int ans = 0;
int min = prices[0];
for(int i = 1; i < pricesSize; i++){
ans = ans > prices[i] - min ? ans : prices[i] - min;
if(min > prices[i]) min = prices[i];
}
return ans;
}
No7 不同路径
分析: 其实这一题和上一题的分析过程差不多,我们倒过来想,到终点的方块只有两个,它上方的方块和它左边的方块,其实任选一个方块它的前一个方块一定是它上方或左方的方块,因为机器人只能向右走或向下走,前一题我们用
F
[
N
]
=
F
[
N
−
1
]
+
F
[
N
−
2
]
F[N] = F[N-1] + F[N - 2]
F[N]=F[N−1]+F[N−2]来表示到终点的方法,那么这一题其实也差不多,只不过是二维的。我们用
a
n
s
[
i
]
[
j
]
ans[i][j]
ans[i][j]来表示走到位置
(
i
,
j
)
(i,j)
(i,j)的方法数,那么现在思路就清晰了,
a
n
s
[
0...
m
]
[
0
]
=
a
n
s
[
0
]
[
0...
n
]
=
1
ans[0...m][0] = ans[0][0...n] = 1
ans[0...m][0]=ans[0][0...n]=1。因为机器人一直往右走和一直往下走都只有一种方法
公式: a n s [ i ] [ j ] = a n s [ i ] [ j − 1 ] + a n s [ i − 1 ] [ j ] ans[i][j]=ans[i][j-1] + ans[i-1][j] ans[i][j]=ans[i][j−1]+ans[i−1][j]
int uniquePaths(int m, int n){
int ans[101][101] = {0};
for(int i = 0; i <n; i++) ans[0][i] = 1;
for(int j = 0; j < m; j++)ans[j][0] = 1;
for(int i = 1; i <m; i++)
for(int j = 1; j < n; j++)
ans[i][j] = ans[i][j - 1] + ans[i -1][j];
return ans[m - 1][n - 1];
}
No8 最小路径和
分析: 这题其实和不同路径还是一样的。只不过加了条件而已,要选一条最小的路径,我们已经知道
(
i
,
j
)
(i,j)
(i,j)可以由
(
i
−
1
,
j
)
(i-1,j)
(i−1,j)和
(
i
,
j
−
1
)
(i, j -1)
(i,j−1)到达,那么现在我们只需要选择其中花费较小的一条即可,遍历整个网格那么遍历到
(
m
,
n
)
(m,n)
(m,n)的时候就是最优解了,gird[i][j]来表示到达
(
i
,
j
)
(i,j)
(i,j)的花费
公式: g r i d [ i ] [ j ] = g r i d [ i ] [ j ] + m i n ( g r i d [ i − 1 ] [ j ] , g r i d [ i ] [ j − 1 ] ) grid[i][j] = grid[i][j] + min(grid[i-1][j], grid[i][j-1]) grid[i][j]=grid[i][j]+min(grid[i−1][j],grid[i][j−1])
int minPathSum(int** grid, int gridSize, int* gridColSize){
int m = gridSize;
int n = *gridColSize;
for(int i = 1; i < n; i++)
grid[0][i] += grid[0][i-1];
for(int i = 1; i < m; i++)
grid[i][0] += grid[i - 1][0];
for(int i = 1; i < m; i++)
for(int j = 1; j < n; j++)
grid[i][j] += grid[i - 1][j] > grid[i][j - 1] ? grid[i][j - 1] : grid[i - 1][j];
return grid[m - 1][n - 1];
}
No9 三角形最小路径和
分析: 咳咳,不用我说还是一样的,只不过变成了三角形,观察该三角形我们发现最左边和最右边只有一条路可以走,除了两边上的节点外其他的都可以从头上的两个节点到达,那么我们只需要选择一个较小的点即可,遍历整个三角形即可得到最优解
公式: t r i a n g l e [ i ] [ j ] = t r i a n g l e [ i ] [ j ] + m i n ( t r i a n g l e [ i − 1 ] [ j − 1 ] , t r i a n g l e [ i − 1 ] [ j ] ) triangle[i][j] = triangle[i][j] + min(triangle[i-1][j-1],triangle[i-1][j]) triangle[i][j]=triangle[i][j]+min(triangle[i−1][j−1],triangle[i−1][j]) 其它条件就不说了一看代码就明白了。
int minimumTotal(int** triangle, int triangleSize, int* triangleColSize){
int m = triangleSize;
for(int i = 1; i < m; i ++){
triangle[i][0] += triangle[i - 1][0];
triangle[i][i] += triangle[i - 1][i - 1];
}
for(int i = 2; i < m; i++)
for(int j = 1; j < i; j++)
triangle[i][j] += triangle[i - 1][j] > triangle[i - 1][j - 1] ? triangle[i - 1][j - 1] : triangle[i - 1][j];
int ans = triangle[m - 1][0];
for(int i = 0; i < m; i++)
if(triangle[m - 1][i] < ans) ans = triangle[m - 1][i];
return ans;
}
优化: 也许这不算是优化,只是换了种写法。我们可以倒过来写,这样的话到顶点的时候就是答案了。
int minimumTotal(int** triangle, int triangleSize, int* triangleColSize){
int m = triangleSize;
for(int i = m - 2; i >= 0; i--)
for(int j = 0; j <= i; j++)
triangle[i][j] += triangle[i + 1][j] > triangle[i + 1][j + 1] ? triangle[i + 1][j + 1] : triangle[i + 1][j];
int ans = triangle[m - 1][0];
return triangle[0][0];
}
No10 比特位计数
分析: 这一题一开始我是直接一个个算出来的,根本没网动态规划这方面想,后来发现它是动态规划类型的题目,那么如何利用已经计算好的信息呢?无意中的一瞥我发现temp>>=1其实是已经被计算过了的,那么现在就很好理解了。我们只需要判断一下最低位是不是1,如果是1那么temp中1的个数就等于temp>>1中1的个数加一,否则就为temp>>1中1的个数。
公式: a n s [ i ] = 1 & i ? a n s [ i > > 1 ] : a n s [ i > > 1 ] + 1 ans[i] = 1 \& i ? ans[i>>1]:ans[i>>1] + 1 ans[i]=1&i?ans[i>>1]:ans[i>>1]+1
class Solution {
public:
vector<int> countBits(int num) {
vector<int> ans = {0};
int temp;
for(int i = 1; i <= num; i++){
temp = i;
ans.push_back(0);
while(temp){
if(1 & temp) ans[i]++;
temp >>= 1;
}
}
return ans;
}
};
class Solution {
public:
vector<int> countBits(int num) {
vector<int> ans = {0};
for(int i = 1; i <= num; i++)
ans.push_back(i & 1 ? ans[i >> 1] + 1 : ans[i >> 1]);
return ans;
}
};