动态规划
斐波那契数列
爬楼梯
题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
class Solution {
public:
int climbStairs(int n) {
if(n<3)
return n:
int fibn;
int fibMinus1=2;
int fibMinus2=1;
for(int i=3;i<=n;++i)
{
fibn=fibMinus1+fibMinus2;
fibMinus2=fibMinus1;//注意赋值顺序
fibMinus1=fibn;
}
return fibn;
}
};
强盗抢劫
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
这道题的本质相当于在一列数组中取出一个或多个不相邻数,使其和最大。那么我们对于这类求极值的问题首先考虑动态规划Dynamic Programming来解,我们维护一个一位数组dp,其中dp[i]表示到i位置时不相邻数能形成的最大和,那么状态转移方程怎么写呢,我们先拿一个简单的例子来分析一下,比如说nums为{3, 2, 1, 5},那么我们来看我们的dp数组应该是什么样的,首先dp[0]=3没啥疑问,再看dp[1]是多少呢,由于3比2大,所以我们抢第一个房子的3,当前房子的2不抢,所以dp[1]=3,那么再来看dp[2],由于不能抢相邻的,所以我们可以用再前面的一个的dp值加上当前的房间值,和当前房间的前面一个dp值比较,取较大值当做当前dp值,所以我们可以得到状态转移方程dp[i] = max(num[i] + dp[i - 2], dp[i - 1]), 由此看出我们需要初始化dp[0]和dp[1],其中dp[0]即为num[0],dp[1]此时应该为max(num[0], num[1]),代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()<2)
return nums.empty()? 0:nums[0];
vector<int> dp={nums[0],max(nums[0],nums[1])};
for(int i=2;i<nums.size();++i)
dp.push_back(max(dp[i-1],dp[i-2]+nums[i]));
return dp.back();
}
};
优化空间复杂度后的代码:
class Solution {
public:
int rob(vector<int>& nums) {
int pre1=0;
int pre2=0;
int cur=0;
for(int i=0;i<nums.size();++i)
{
cur=max(pre1,pre2+nums[i]);
pre2=pre1;
pre1=cur;
}
return cur;
}
};
强盗在环形街区抢劫
这道题是之前那道 House Robber 的拓展,现在房子排成了一个圆圈,则如果抢了第一家,就不能抢最后一家,因为首尾相连了,所以第一家和最后一家只能抢其中的一家,或者都不抢,那我们这里变通一下,如果我们把第一家和最后一家分别去掉,各算一遍能抢的最大值,然后比较两个值取其中较大的一个即为所求。那我们只需参考之前的 House Robber 中的解题方法,然后调用两边取较大值,代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()<2)
return nums.empty()? 0:nums[0];
return max(core(nums,0,nums.size()-1),core(nums,1,nums.size()));
}
int core(vector<int>& nums,int start,int end)
{
int pre1=0;
int pre2=0;
int cur;
for(int i=start;i<end;++i)
{
cur=max(pre1,pre2+nums[i]);
pre2=pre1;
pre1=cur;
}
return cur;
}
};
信件错排
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
(1)i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-2] 种错误装信方式。
(2)i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-1] 种错误装信方式。综上所述,错误装信数量方式数量为:
母牛生产
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
第 i 年成熟的牛的数量为:
矩阵路径
矩阵的最小路径和
题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int rows=grid.size();
int cols=grid[0].size();
int dp[rows][cols];
dp[0][0]=grid[0][0];
for(int i=1;i<rows;++i)
dp[i][0]=grid[i][0]+dp[i-1][0];
for(int j=1;j<cols;++j)
dp[0][j]=grid[0][j]+dp[0][j-1];
for(int i=1;i<rows;++i)
for(int j=1;j<cols;++j)
dp[i][j]=grid[i][j]+min(dp[i-1][j],dp[i][j-1]);
return dp[rows-1][cols-1];
}
};
矩阵的总路径数
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。
class Solution {
public:
int uniquePaths(int m, int n) {
int dp[m][n];
for(int i=0;i<m;++i)
dp[i][0]=1;
for(int j=0;j<n;++j)
dp[0][j]=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];
}
};
数组区间
数组区间和
303. Range Sum Query - Immutable (Easy)
求区间 i ~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 ~ i - 1 的和。
class NumArray {
public:
NumArray(vector<int> nums) {
if(!nums.empty())
dp.push_back(nums[0]);
for(int i=1;i<nums.size();++i)
dp.push_back(dp[i-1]+nums[i]);
}
int sumRange(int i, int j) {
return dp[j]-dp[i-1];
}
private:
vector<int> dp;
};
/**
* Your NumArray object will be instantiated and called as such:
* NumArray obj = new NumArray(nums);
* int param_1 = obj.sumRange(i,j);
*/
优化后:
class NumArray {
public:
NumArray(vector<int> &nums) {
accu.push_back(0);
for (int num : nums)
accu.push_back(accu.back() + num);
}
int sumRange(int i, int j) {
return accu[j + 1] - accu[i];
}
private:
vector<int> accu;
};
数组中等差递增子区间的个数
413. Arithmetic Slices (Medium)
A = [1, 2, 3, 4]
return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself.
dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。
在 A[i] - A[i - 1] == A[i - 1] - A[i - 2] 的条件下,{A[i - 2], A[i - 1], A[i]} 是一个等差递增子区间。如果 {A[i - 3], A[i - 2], A[i - 1]} 是一个等差递增子区间,那么 {A[i - 3], A[i - 2], A[i - 1], A[i]} 也是等差递增子区间,dp[i] = dp[i-1] + 1。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& A) {
int n=A.size();
vector<int> dp(n,0);
for(int i=2;i<n;++i)
{
if(A[i]-A[i-1]==A[i-1]-A[i-2])
dp[i]=(dp[i-1]+1);
}
int res=0;
for(auto num:dp)
res+=num;
return res;
}
};
进一步优化空间
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& A) {
int n=A.size();
int cur=0;
int res=0;
for(int i=2;i<n;++i)
{
if(A[i]-A[i-1]==A[i-1]-A[i-2])
{
cur+=1;
res+=cur;
}
else
cur=0;
}
return res;
}
};
分割整数
分割整数的最大乘积
题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
动态规划做法:</