[TOC]
一、斐波那契数列
剑指 Offer 10- I. 斐波那契数列(简单难度)
509. 斐波那契数(简单难度)
class Solution {
public:
/* 本题是动态规划的经典题目,和509的区别在于答案需要% 1000000007,这是测试数据n最大可取100,防止溢出
F(n)需要由F(n-1)和F(n-2)确定,已知第0和1个,求解第n个
动态规划的步骤:
状态定义:F[n] 表示的含义为斐波那契数列中第 n 个数字
转移方程:F(n)=F(n-1)+F(n−2),求 n元素时,只需要知道第 n-1 和 n-2个元素即可,故而运算过程中不需要保存全部的dp数组
初始状态: F[0] = 0, F[1] = 1
*/
int fib(int n) {
if(n<=1) return n;
// 动态规划数组
vector<int> dp(2); //dp[i]表示第i个数的斐波那契数值
// 初始状态
dp[0]=0;// 第n-2个数
dp[1]=1;// 第n-1个数
// 遍历[2,n+1)计算第n个数的值
for(int i=2;i<n+1;i++){
int temp = dp[0]+dp[1];//第n个数=第n-2个数+第n-1个数
temp=temp% 1000000007;//取模,防止数字移除
dp[0] = dp[1];//更新第n-2个数
dp[1] = temp;// 更新第n-1个数
}
return dp[1];
}
};
二、爬楼梯问题
剑指 Offer 10- II. 青蛙跳台阶问题(简单难度)
70. 爬楼梯(简单难度)
class Solution {
public:
/* 本题是动态规划的经典题目,和70的区别在于答案需要% 1000000007,这是测试数据n最大可取100,防止溢出
本题和斐波那契数列数列几乎一样,唯一的区别在于初始状态不一样。
第n个台阶只能从第n-1或者n-2个上来。到第n-1个台阶的走法 + 第n-2个台阶的走法 = 到第n个台阶的走法
动态规划的步骤:
状态定义:F[n] 表示的含义为上一个n级台阶的跳法数目
转移方程:F(n)=F(n-1)+F(n−2),求 n+1 元素时,只需要知道第 n 和 n-1 个元素即可,故而运算过程中不需要保存全部的dp数组
初始状态: F[0] = 1, F[1] = 1
*/
int numWays(int n) {
if(n==0) return 1;
if(n==1) return 1;
vector<int> dp={0,0};//爬到第i层楼梯,有dp[i]种方法
dp[0] = 1;
dp[1] = 1;
// 第n个台阶只能从第n-1或者n-2个上来。到第n-1个台阶的走法 + 第n-2个台阶的走法 = 到第n个台阶的走法
for(int i=2;i<n+1;i++){
int temp=dp[0]+dp[1];
temp=temp%1000000007;// 防止溢出
dp[0] = dp[1];
dp[1] = temp;
}
return dp[1];
}
};
剑指 Offer 46. 把数字翻译成字符串(中等难度)
class Solution {
public:
/* 解析:这道题定义0-25的数都可以翻译成对应的英文字母,给定一个多位数字,问有多少种方案
存在多种翻译方案的原因在于,每次翻译可以只翻译1个数位,也可以一次性翻译2个数位,例如:
16可以看做1和6分别进行翻译,也可以看做16一次性翻译
这道题可以看做是抽象的爬楼梯问题:给定一个n位数,就是给了n个台阶
每此只能翻译一个数或两个数就是,n个台阶里每此只能上一级或两级
唯一的区别是爬两级需要做一个是否小于26的判断。
【状态定义】:dp[i] 表示的含义为翻译前i个数字的方案数目
【转移方程】:翻译前i位数只能是
(1)第i位数字 无法和前面的数组合,比如 1245, 5 只能单独翻译,那么方法数和 124 是一样的
dp[i]=dp[i-1],[i-2,i-1]组成的数<10||>25
(2)第i位数字 可以和前面的数组合,比如 1215, 5 可以选择 组合 和 不组合,最终结果为两种情况相加
a. 选择组合,15看成是整体,那么 dp[i] = dp[i - 2]
b. 不选择组合,5单独翻译,那么 dp[i] = dp[i - 1]
最终结果:
dp[i]=dp[i-1]+dp[i−2],10<=[i-2,i-1]组成的数<=25
*/
int translateNum(int num) {
string numStr=to_string(num);
vector<int> dp={0,0};
dp[0]=1;
dp[1]=1;
// 循环[2,len],因为dp[i]是定义前i位数字,最后求前len位数字
for(int i=2;i<=numStr.length();i++){
int temp=0;
int digit=(numStr[i-2]-'0')*10+numStr[i-1]-'0';
if(digit>=10&&digit<=25) temp=dp[0]+dp[1];
else temp=dp[1];
dp[0] = dp[1];
dp[1] = temp;
}
return dp[1];
}
};
三、网格路径
剑指 Offer 47. 礼物的最大价值(中等难度)
class Solution {
public:
/* 解析:本类型题是网格路径问题,可以看做是二维版本的爬楼梯问题
【状态定义】:dp[i][j] 表示的含义为从[0,0]到[i-1,j-1]所能获得的最大累积奖励
【转移方程】:[i,j]只能是从上一格[i,j-1]或者左一格[i-1,j]达到
所以dp[i][j]=max(dp[i][j-1],dp[i-1][j])+grid(i-1,j-1);
*/
int maxValue(vector<vector<int>>& grid) {
int row = grid.size();
int column = grid[0].size();
//dp[i][j]表示从grid[0][0]到grid[i-1][j- 1]时的最大价值
vector<vector<int>> dp(row + 1,vector<int>(column + 1,0)) ;
// 遍历[1,1]到[row,column]
for (int i = 1; i <= row; i++) {
for (int j = 1; j <= column; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i-1][j-1];
}
}
return dp[row][column];
}
};
四、序列问题
剑指 Offer 42. 连续子数组的最大和(简单难度)
53. 最大子数组和(简单难度)
class Solution {
public:
/* 本题是动态规划的经典序列题目,数组中的每个元素可能是正数、0或负数,所以不考虑滑动窗口
动态规划的步骤:
【状态定义】:F[i] 表示的含义为以nums[i]结尾的连续子数组最大和(实现时使用动态规划数组dp),0<=i<=n-1
请注意,F[i]并不是nums数组的连续子数组的最大和,其值会随i变化,但是其最大值就是nums数组的连续子数组的最大和
【转移方程】:
假设F(i-1)已知,根据定义新的F(i)=F(i-1)+nums[i]或者F(i)=nums[i];
F(i-1)>0时,F(n)=nums[i]+F(n-1)。F(n-1)产生正向增益,予以于保留
F(i-1)<=0时,F(n)=nums[i]。F(n-1)产生负向增益,不予以于保留,因为非正数+某个数<=某个数
【初始状态】: F[0] = nums[0]
*/
int maxSubArray(vector<int>& nums) {
// 初始化动规数组
vector<int> dp(nums.size(),0);
dp[0]=nums[0];
int res = dp[0];
// 计算[1,n)的最大和
for(int i=1;i<nums.size();i++){
// if(dp[i-1] > 0) dp[i] = nums[i]+dp[i-1];
// else dp[i] = nums[i];
dp[i]=max(nums[i]+dp[i-1],nums[i]);//以nums[i]结尾的连续子数组的最大和
res= max(res,dp[i]);//整个nums数组可以取得的连续子数组的最大和
}
return res;
}
};
五、丑数问题
剑指 Offer 49. 丑数(中等难度)
264. 丑数 II(中等难度)
class Solution {
public:
/* 解析:本类型题是动态规划问题,属于特殊情景类,根据题意设计算法
【状态定义】:dp[i]表示的含义为从小到大排序的第i个丑数
【转移方程】:
理论原理:
最小的丑数是1,即dp[1]=1,那么如何得到其他的丑数呢?
任意一个丑数都是由小于它的某一个丑数*2,*3或者*5得到的,假设存在3个有序数组分别是每个丑数*2,*3或者*5
那么所有丑数的排列,必定就是上面ABC3个有序数组的合并结果然后去重得到的
假设有3个有序数组A={1*2},B={1*3},C={1*5},定义三个指针,p2,p3,p5分别指向其中第一个元素。
dp[2]=min(A[p2],B[p3],C[p5])=2;
A={1*2,2*2},B={1*3,2*3},C={1*5,2*5}...
可以发现,ABC三个数组实际上就是dp[]*2,dp[]*3和dp[]*5的结果,
dp[i]=min(A[p2],B[p3],C[p5]),谁的最小就移动谁的指针
合并有序数组的一个比较好的方法,就是每个数组都对应一个指针,
然后比较这些指针所指的数中哪个最小,就将这个数放到结果数组中,然后该指针向后挪一位。
*/
int nthUglyNumber(int n) {
vector<int> dp(n+1,0);//动规数组
dp[1]=1;//初始状态
int p2=1,p3=1,p5=1;//三个指针中的初始值
// 遍历[2,n]
for(int i=2;i<=n;i++){
int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
dp[i] = min(min(num2, num3), num5);//取三个有序数组中的最小值
// 谁的最小谁的指针就移动(合并3个有序数组的操作)
if (dp[i] == num2) p2++;
if (dp[i] == num3) p3++;
if (dp[i] == num5) p5++;
}
return dp[n];
}
};
六、约瑟夫环问题
剑指 Offer 62. 圆圈中最后剩下的数字(简单难度)
class Solution {
public:
/* 本题是根据经典的约瑟夫环的数学问题设计而来的,
N个人围成一圈,第一个人从1开始报数,报M的将被杀掉,下一个人接着从1开始报。如此反复,最后剩下一个,求最后的胜利者。
解决方案:
模拟法:普通的模拟方式就是使用循环链表,按照题意进行模拟,其时间复杂度高达O(nm)
动态规划法:
【状态定义】:dp[i] 表示的含义为有i个数,每次删除第m个数后的剩余数字索引,i<n
【转移方程】:假设有8个数,每次删除第3个数:
索引:0 1 2 3 4 5 6 7
1 2 [3] 4 5 6 7 8
4 5 [6] 7 8 1 2
7 8 [1] 2 4 5
2 4 [5] 7 8
7 8 [2] 4
4 7 [8]
[4]7
7 最终的胜利者
通过观察上述过程,可知,每删除一个数字,相当于将胜利者的索引前移了m位
已知i-1个人时,胜利者的下标位置dp[i-1]
i个人时,胜利者的索引相当于往后移动M位,由于移动后数组可能越界,
如果超出,则超出的部分接到头部,索引则需要对数字个数i取模
dp[i]=(dp[i-1]+m)%i,
【初始状态】: dp[1] = 0
*/
int lastRemaining(int n, int m) {
vector<int> dp(n+1,0);
dp[1]=0;
for (int i = 2; i <=n; i++) {
dp[i]=(dp[i-1]+m)%i;
}
return dp[n];
}
};
七、股票问题
剑指 Offer 63. 股票的最大利润(中等难度)
121. 买卖股票的最佳时机(简单难度)
2016. 增量元素之间的最大差值(简单难度)
class Solution {
public:
/*解析:
股票问题是动态规划的经典应用场景,本题是股票类中最简单的一个问题
买卖一次股票的最大利润意味着必须在股票价格最小值买入,最小值之后的股票价格最大值卖出。
本题只要求出最小值,和最小值之后的最大值就能通过。
但是我们重点放在动态规划的思路上:
【状态定义】:dp[i] 表示的含义为价格数组中0-i的子数组的最大利润,i<n
【转移方程】:假设
前n天的最大利润 = max(前n-1天的最大利润,第n天的价格 - 前n天的最低价格)
dp[i] = max(dp[i-1], min(prices[n], minCost))
【初始状态】: dp[0] = 0
*/
int maxProfit(vector<int>& prices) {
int minCost = INT_MAX;
int res=0;
for(int price:prices){
minCost = min(minCost,price);
res=max(res,price-minCost);
}
return res;
}
};
八、乘积数组
剑指 Offer 66. 构建乘积数组(中等难度)
238. 除自身以外数组的乘积(中等难度)
class Solution {
public:
/*解析:本题的题意不难理解,最简单的思路就是用2个循环暴力破解
但是注意到a的长度最多100000,而且不让用除法,所以暴力破解法大概率会超时。
*/
vector<int> constructArr(vector<int>& a) {
// 方法一:暴力破解法(超时)
// int n=a.size();
// vector<int> res(n,1);
// for(int i=0;i<n;i++){
// for(int j=0;j<n;j++){
// if(j!=i) res[i]*=a[j];
// }
// }
// return res;
// 方法二:
int n=a.size();
vector<int> res(n,1);
int left=1;
for(int i=0;i<n;i++){
res[i]=left;
left*=a[i];
}
int right=1;
for(int i=n-1;i>=0;i--){
res[i] *= right;
right *= a[i];
}
return res;
}
};