目录
贪心算法与动规算法:
剑指offer14-1 剪绳子
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
长度为n是固定的,剪成几段是随意的。三步:dp数组的定义,base case ,状态转移
- dp数组:dp[i]:长度为i的绳子分成m段后的最大乘积。
- base case: dp[0],dp[1]没有意义,dp[2]设为1,即分成1*1两个段
- 状态转移:先分出一段来,长度为j,那么剩下的要么不分要么分,不分的话剩下i-j,长度为:j*(i-j);分的话就是长度为i-j的绳子的最大乘积再乘上j,即j*dp[i-j];j是不定的,由于j是1的话相当于白切,所以从2开始一直到i,找其中的最大值,即dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
class Solution {
public:
//dp[i] 表示长度为i的绳子剪成m段后的最大乘积
int cuttingRope(int n) {
vector<int> dp(n+1);
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j=2;j<i;++j)
dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));//
}
return dp[n];
}
};
剑指offer 14-II
题目与上题一致,但是n会增大到1000,这样结果可能会溢出,用动态规模就不能处理了,改用贪心策略:
贪心规则就再与尽可能的将绳子切成3大小的段,这样会使得乘积最大,并对大数取模。
class Solution {
public:
int cuttingRope(int n) {
if(n<4){
return n-1;
}
long res=1;
while(n>4){
res=res*3%1000000007;
n-=3;
}
return res*n%1000000007;
}
};
所以能用动规解还是用动规解,因为贪心规则不好解释。
剑指offer 46 把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
第一版:
class Solution {
public:
//dp[i] 以i为结尾的数组有多少种不同的翻译方法
int translateNum(int num) {
string str=to_string(num);
vector<int> dp(str.size()+1);
dp[0]=1; //第0位相当于空位,因为dp[2]如果是可以组合的,那么要使得i-2+i-1=2,所以初始化dp[0]为1
dp[1]=1; //第一位只有一个元素,故为1
for(int i=2;i<=str.size();++i){
string temp=str.substr(i-2,2);
if(temp<="25"&&temp>="10"){
dp[i]=dp[i-2]+dp[i-1];
}
else
dp[i]=dp[i-1];
}
return dp[str.size()];
}
};
- dp数组:dp[i]代表以i结尾的数组有多少种不同的翻译方法
- base case :图上
- 状态转移: 若不组合,则就是不组合的常规方式,即dp[i-1],不加1;若组合,则有dp[i-2]和dp[i-1]两种情况,将两者相加;
第二版:状态压缩
class Solution {
public:
//dp[i] 以i为结尾的数组有多少种不同的翻译方法
int translateNum(int num) {
string str=to_string(num);
vector<int> dp(str.size()+1);
// dp[0]=1; //第0位相当于空位,因为dp[2]如果是可以组合的,那么要使得i-2+i-1=2,所以初始化dp[0]为1
//dp[1]=1; //第一位只有一个元素,故为1
int pre2=1,pre1=1;
for(int i=2;i<=str.size();++i){
string temp=str.substr(i-2,2);
if(temp<="25"&&temp>="10"){
int cur=pre2+pre1;
pre2=pre1;
pre1=cur;
}
else{
int cur=pre1;
pre2=pre1;
pre1=cur;
}
}
return pre1;
}
};
剑指offer47-礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
基础的动规题,思路:因为是二维网格,所以用一个二维dp数组,
- dp数组:dp[i][j]表示到i,j位置可以得到的最大价值
- base case:dp[0][0]=grid[0][0]
- 状态转移:dp[i][j]只跟它上面和它左边的有关,选取两者较大的一个加上自己的权重即可
class Solution {
public:
//dp[i][j]: 走到(i,j)所拿到的最大价值
int maxValue(vector<vector<int>>& grid) {
int n=grid.size(),m=grid[0].size();
vector<vector<int>> dp(n,vector<int>(m,0));
dp[0][0]=grid[0][0];
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
if(i==0&&j==0){continue;}
if(i==0){
dp[i][j]=dp[i][j-1]+grid[i][j];
}
else if(j==0){
dp[i][j]=dp[i-1][j]+grid[i][j];
}
else{dp[i][j]=max(dp[i-1][j],dp[i][j-1])+grid[i][j];}
}
}
return dp[n-1][m-1];
}
};
注意处理边界情况,当上侧或者左侧没有值,说明只跟另一个有关。
但还可以优化,让dp数组多申请一行和一列,作为边界行列,初始值为0,这样可以减少逻辑判断。返回值由dp[n-1][m-1]变为dp[n][m]
class Solution {
public:
//dp[i][j]: 走到(i,j)所拿到的最大价值
int maxValue(vector<vector<int>>& grid) {
int n=grid.size(),m=grid[0].size();
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];
}
}
return dp[n][m];
}
};
剑指49-丑数
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
这是一道非典型的动态规划,其实说是动规不如说借用动规的归纳思想。因为一个丑数必然从一个比它更小的丑数*2 *3 *5而来,所以和前序的丑数有关。动规三步如下:
- dp数组:dp[i]表示第i+1个丑数
- base case: dp[0]=1,1是最小的丑数
- 状态转移: 既然是从一个更小的丑数*2 *3 *5而来,那么可以设定a,b,c都等于0,分别代表该位*2 *3 *5并选择其中最小的,然后移动该位。这里需要注意三个if,而不是if else ,因为有可能当前有多个值是相等的,那么dp中只保留一次,所以要将结果相等的都移动当前指针(比如dp[a]*2,dp[b]*3都等于6,那么a b都应该移动)
class Solution {
public:
//dp[i]:第i+1个丑数
int nthUglyNumber(int n) {
vector<int> dp(n);
dp[0]=1;
int a=0,b=0,c=0;
for(int i=1;i<n;++i){
dp[i]=min(min(dp[a]*2,dp[b]*3),dp[c]*5);
//cout<<dp[i]<<endl;
if(dp[i]==dp[a]*2){a++;}
if(dp[i]==dp[b]*3){b++;}
if(dp[i]==dp[c]*5){c++;}
}
return dp[n-1];
}
};
剑指offer-60 n个骰子的点数
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
示例 1:
输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
要得出点数第i个小的概率,可以转变为第i个小的数出现的次数,最后除以出现的总次数即可
状态有n个色子,以及出现的次数,所以是二维数组
- dp数组:dp[i][j]表示置个数i个色子时出现次数为j
- base case: dp[1][j]=1,1个色子6个数字出现次数都为1
- 状态转移: dp[i][j]表示置个数i个色子时出现次数为j,那么新投一个色子时还会投出1~6的点数,就是 投i-1个色子的时候的出现的点数加上投这一次时出现的点数刚好等于j时的次数,例如dp[2][3] 就是投1个色子时出现1~6,加上1~6可以等于3的次数,就是1+2,2+1两种,所以 dp[i][j]+=dp[i-1][j-s];
- 最后输出投n次时出现的次数再除以总次数即可
class Solution {
public:
//dp[i][j]:置个数i个色子时出现次数为j
vector<double> dicesProbability(int n) {
vector< vector<int>> dp(n+1,vector<int>(n*6+1,0));
for(int j=1;j<=6;++j){
dp[1][j]=1;
}
for(int i=2;i<=n;++i){
for(int j=i;j<=n*6+1;++j){
for(int s=1;s<=6;s++) {
if(j-s<0){
break;
}
dp[i][j]+=dp[i-1][j-s];
}
}
}
vector<double> res;
for(int i=1;i<=n*6+1;++i){
if(dp[n][i]!=0){
res.emplace_back(dp[n][i]*1.0/pow(n,6));
}
}
return res;
}
};
剑指63 股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
思路:状态是每天的股价,选择是当天买进还是不买进,如果dp表示的是第i天买进的利润,那么到最后就无法得到最大股票差值,所以用dp[i]表示i天之前的股票最大差值。
- dp数组:dp[i]表示i天之前的股票最大差值,
- base case: dp[0]=0
- 状态转移:第i天时,买进和不买进主要决定于前一天股票差值和当天差值的最大值,所以需要维持一个变量维持前序的最小值,由于只跟dp[i-1]有关,所以状态压缩,直接用两个变量pre,cur表示dp[i-1]和dp[i]即可。
class Solution {
public:
//dp[i]:i天之前最大利润
int maxProfit(vector<int>& prices) {
if(prices.empty())return 0;
int n=prices.size()-1;
int minN=INT_MAX;
int pre=0;
int cur;
for(int i=0;i<=n;++i){
minN=min(minN,prices[i]);
cur=max(pre,prices[i]-minN);
pre=cur;
}
return cur;
}
};
HOT100
32.最长的有效括号
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
有效括号,要满足匹配规则;
- dp数组:dp[i]表示i结尾的有效字串长度,这样需要一个max记录最大的
- base case: dp[0]=0 单个括号算无效
- 状态转移:当s[i]为‘(’时,无法跟前面的匹配,所以dp[i]=0;当s[i]为‘)’时,如果s[i-1]是'(',那么可以匹配,dp[i]=dp[i-2]+2; 否则,有可能是((...))这样,需要向前找看看有没有和s[i]匹配的,若dp[i-1]匹配的长度是x,那么可能匹配的地方就是i-x-1,所以如果这个位置是'(',那么就可以匹配,dp[i]=dp[i-1]+2;然后原来i-x-1这个位置是单独的'(',可能不匹配,但是加了i位的’)‘,就匹配了,所以还要加上i-x-1位置前一位是不是有匹配的,连在一起dp[i]=dp[i-dp[i-1]-2]+dp[i-1]+2; 最后注意边界条件即可
class Solution {
public:
//dp[i]: i结尾的最长有效字串长度
int longestValidParentheses(string s) {
if(s.empty()){
return 0;
}
vector<int> dp(s.size());
dp[0]=0;
int maxX=0;
for(int i=1;i<s.size();++i){
if(s[i]=='('){
dp[i]=0;
}
else{
if(i==1){
dp[1]= (s[0]=='('?2:0);
}
else{
if(s[i-1]=='('){
dp[i]=dp[i-2]+2;
}
else if(i-dp[i-1]-1>=0&&s[i-dp[i-1]-1]=='('){
if(i-dp[i-1]-2>=0)
dp[i]=dp[i-dp[i-1]-2]+dp[i-1]+2;
else{
dp[i]=dp[i-1]+2;
}
}
}
}
maxX=max(maxX,dp[i]);
}
return maxX;
}
};