动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。
状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
**因为一些情况是递推公式决定了dp数组要如何初始化!**写动规题目,代码出问题很正常!找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!一些同学对于dp的学习是黑盒的状态,就是不清楚dp数组的含义,不懂为什么这么初始化,递推公式背下来了,遍历顺序靠习惯就是这么写的,然后一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:F(0) = 0,F(1) = 1; F(n) = F(n - 1) + F(n - 2),其中 n > 1
class Solution {
public:
int fib(int n) {
if(n<2){
return n;
}
int res=fib(n-1)+fib(n-2);
return res;
}
};
时间复杂度:O(2^n);空间复杂度:O(n),算上了编程语言中实现递归的系统栈所占空间
动态规划:这里我们要用一个一维 dp 数组来保存递归的结果;确定dp数组以及下标的含义,dp[i]的定义为:第i个数的斐波那契数值是dp[i]
class Solution {
public:
int fib(int n) {
if(n<2){
return n;
}
vector<int> dp(n+1,0);
dp[1]=1;
for(int i=2;i<dp.size();i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[dp.size()-1];
}
};
时间复杂度:O(n); 空间复杂度:O(n)
if(n<2){
return n;
}
int dp[2]={0,1};
for(int i=2;i<n+1;i++){
dp[i%2]=dp[0]+dp[1];
}
return dp[(n)%2];
时间复杂度:O(n);空间复杂度:O(1)
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。此时大家应该发现了,这不就是斐波那契数列么!
class Solution {
public:
int climbStairs(int n) {
if(n<3){
return n;
}
int dp[2]={1,2};
for(int i=2;i<n;i++){
dp[i%2]=dp[0]+dp[1];
}
return dp[(n-1)%2];
}
};
后面将讲解的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,但我个人认为面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化。
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
if(cost.size()<3){
return min(cost[0],cost[1]);
}
vector<int> dp(cost.size()+1,0);
for(int i=2;i<dp.size();i++){
dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[cost.size()];
}
};
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
这道题目,刚一看最直观的想法就是用图论里的深搜,来枚举出来有多少种路径。注意题目中说机器人每次只能向下或者向右移动一步,那么其实机器人走过的路径可以抽象为一棵二叉树,而叶子节点就是终点!
class Solution {
public:
int track(int i,int j,int m,int n){
if(i>m||j>n){
return 0;
}
if(i==m && j==n){
return 1;
}
return track(i+1,j,m,n)+track(i,j+1,m,n);
}
int uniquePaths(int m, int n) {
return track(0,0,m-1,n-1);
}
};//超时
那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已);深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的。
动态规划:机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m,vector<int>(n,1));
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
时间复杂度:O(m × n);空间复杂度:O(m × n)
一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。可以转化为,给你m + n - 2个不同的数,随便取m - 1个数,有几种取法。那么这就是一个组合问题了。求组合的时候,要防止两个int相乘溢出! 所以不能把算式的分子,分母都算出来再做除法。需要在计算分子的时候,不断除以分母,代码如下:
class Solution {
public:
int uniquePaths(int m, int n) {
long long son=1;
int mom=m-1;
int count=m-1;
int t=m+n-2;
while(count--){
son *= t--;
while(mom!=0 && son%mom==0){
son /= mom;
mom--;
}
}
return son;
}
};
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用 1
和 0
来表示。
数论解决,完全不行,不止一块障碍物
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int flag =0;
int temp_m=0,temp_n=0;
for(int i=0;i<obstacleGrid.size();i++){
for(int j=0;j<obstacleGrid[0].size();j++){
if(obstacleGrid[i][j]){
temp_m=i;
temp_n=j;
flag=1;
break;
}
}
}
int count_sum=obstacleGrid.size()-1;
long long son_sum=1;
int mom_sum=obstacleGrid.size()-1;
int mn=obstacleGrid.size()+obstacleGrid[0].size()-2;
while(count_sum--){
son_sum *= mn--;
while(mom_sum!=0 && son_sum%mom_sum==0){
son_sum /= mom_sum;
mom_sum--;
}
}
if(flag==0){
return son_sum;
}
long long son1=1;
int mom1 = temp_m;
int count =temp_m;
int mn1 = temp_m+temp_n;
while(count--){
son1 *= mn1--;
while(mom1!=0 && son1%mom1==0){
son1 /= mom1;
mom1--;
}
}
long long son2=1;
int mom2 = obstacleGrid.size()-temp_m-1;
int count2 = obstacleGrid.size()-temp_m-1;
int mn2 = obstacleGrid.size()-temp_m+obstacleGrid[0].size()-temp_n-2;
while(count2--){
son2 *= mn2--;
while(mom2!=0 && son2%mom2==0){
son2 /= mom2;
mom2--;
}
}
return son_sum-son1*son2;
}
};//
动态规划:确定dp数组(dp table)以及下标的含义,dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
确定递推公式:dp[i][j] = dp[i - 1] [j] + dp[i] [j - 1]。因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。
dp数组如何初始化:从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i] [0]一定为1,dp[0] [j]也同理。但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i] [ 0]应该还是初始值0。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m=obstacleGrid.size();
int n=obstacleGrid[0].size();
if(obstacleGrid[m-1][n-1]==1 || obstacleGrid[0][0]==1){
return 0;
}
vector<vector<int>> dp(m,vector<int>(n,0));
for(int i=0;i<m&&obstacleGrid[i][0]==0;i++){
dp[i][0]=1;
}
for(int i=0;i<n&&obstacleGrid[0][i]==0;i++){
dp[0][i]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacleGrid[i][j]==1){
continue;
}
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度;空间复杂度:O(n × m)
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。返回 你可以获得的最大乘积 。
确定dp数组(dp table)以及下标的含义:dp[i]:分拆数字 i,可以得到的最大乘积为dp[i]。dp[i]的定义将贯彻整个解题过程,下面哪一步想不懂了,就想想dp[i]究竟表示的是啥!
确定递推公式:其实可以从1遍历j,然后有两种渠道得到dp[i].一个是j * (i - j) 直接相乘。一个是j * dp[i - j],相当于是拆分(i - j),对这个拆分不理解的话,可以回想dp数组的定义。j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j 取最大的。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));也可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。所以递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});
dp的初始化:这里我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+1);
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j=1;j<=i/2;j++){
dp[i]=max(dp[i],max((i-j)*j,dp[i-j]*j));
}
}
return dp[n];
}
};
时间复杂度:O(n^2); 空间复杂度:O(n)
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
n为1的时候有一棵树,n为2有两棵树,来看看n为3的时候
确定dp数组(dp table)以及下标的含义:dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]。也可以理解是i个不同元素节点组成的二叉搜索树的个数为dp[i] ,都是一样的。
确定递推公式:dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量];j相当于是头结点的元素,从1遍历到i为止。所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
dp数组如何初始化:从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1);
dp[0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
dp[i] += dp[j-1]*dp[i-j];
}
}
return dp[n];
}
};
时间复杂度: O ( n 2 ) O(n^2) O(n2);空间复杂度: O ( n ) O(n) O(n)