目录
剑指 Offer II 088. 爬楼梯的最少成本
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。
请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。
一次能爬两个楼梯或者一个楼梯,从-1开始能+1或者+2,但是要加上此时位置的权重
动态规划和回溯法比较,动态规划和分治法比较
- 动态规划和回溯法都可以分成若干步,每一步都面临若干种选择,但是回溯法要求列举出所有的解,动态规划是求解一个问题的最优解(通常是最值),或者求问题解的个数
- 动态规划和分治法都可以采用递归思路通过把大问题分成成小问题求解,分治法是各小问题之间不存在重叠部分,动态规划是各小问题之间存在重叠部分
本题若需要到达第 i 层,那么此时面临两个选择,第一个是是从第 i-1 层到达,第二个是是从第 i-2 层到达,可以通过回溯法列出所有的解,但是要求求最值,那么只能用动态规划
动态规划很容易采用递归的方式解决,即把大问题拆分为多个小问题。但是因为这些小问题之间存在大量重叠,所以直接处理会带来严重的效率问题,虽然可以使用缓存的方式保存各小问题的解来避免重复计算,但是同时也会带来空间效率的降低。一种比较好的解决方式就是采用迭代的形式求解动态规划问题,即先解决小问题再解决大问题的方式,这样可以优化时间和空间效率。
- 动态规划
- 公式: dp[i] = Math.min( dp[i-1] + cost(i-1), dp[i-2] + cost(i-2));
- 当前花费的最小值就是前一个的花费加之前的花费和前两个台阶的花费+之前的花费的最小值
java的代码
代码
//java的代码
class Solution {
public int minCostClimbingStairs(int[] cost) {
int size = cost.length ;
int[] res = new int[size+1];
res[0] = res[1] = 0;
for(int i = 2; i <= size; i++){
res[i] = Math.min(res[i-1] + cost[i-1], res[i-2] + cost[i-2]);
}
return res[size];
}
}
剑指 Offer II 089. 房屋偷盗
一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:nums = [2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
方法一:单状态动态规划
若设 f(i) 表示小偷从下标为 0 的房间到下标为 i 的房间为止最多能偷取到的财物最大值。当小偷在下标为 i 的房屋前,他面临两个选择。第一个选择是他进去偷东西,根据要求此时他不能进入下标为 i-1 的房屋偷东西,所以他最多可以偷到 f(i - 2) + nums[i];第二个选择是他不进去偷东西,那么他可以进入进入下标为 i-1 的房间偷东西,所以他最多可以偷到 f(i - 1)。因为要求偷的财物最大,所以状态转移方程为
以上转移方程成立的条件为 i >= 2。当 i = 0 时,此时小偷直接偷下标为 0 的房屋就能取得最大财物,当 i = 1 时,因为小偷只能偷其中一间房屋,所以选择最大的房屋偷就能取得最大的财物
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) {
return nums[0];
}
vector<int> dp(2, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); ++i) {
dp[i % 2] = max(dp[(i - 2) % 2] + nums[i], dp[(i - 1) % 2]);
}
return max(dp[0], dp[1]);
}
};
剑指 Offer II 090. 环形房屋偷盗
一个专业的小偷,计划偷窃一个环形街道上沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
动态规划
一个是求小偷从下标为 0 的房屋开始到下标为 n - 2 的房屋结束能偷得的最大财物,另一个是求小偷从下标为 1 的房屋开始到下标为 n - 1 的房屋结束能偷得的最大财物。最终的大问题的解就是这两个小问题的最大值。
剑指 Offer II 091. 粉刷房子
假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。
例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
输入: costs = [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。
最少花费: 2 + 5 + 3 = 10。
题目意思第一个数组是0号房子耗的钱,从里面找最小的,第二个数组是1号的钱,从里面找最小的,但是相邻的颜色不能相同,也就是0号房子使用的第二个,那么1号房子不能使用第二个
动态规划
dp [i] 表示 1 ~ i 个房子花费的最小值, 这样后面的状态转移方程并不好表示, 所以使用二维来表示解, dp[i][j] 表示第i个房子,使用了j种颜色并且加上了前面房子的花费。当第i个房子使用了j时,那么对i-1尝试使用(j + 1) % 3和(j + 2) % 3求其最小值
所以动态转移函数是
dp[i][j] = min(dp[i - 1][(j + 1) % 3], dp[i - 1][(j + 2) % 3]) + costs
代码
class Solution
{
public:
int minCost(vector<vector<int>>& costs)
{
int n = costs.size();
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
for (int i = 1; i <= n; ++i)
for (int j = 0; j < 3; ++j)
dp[i][j] = min(dp[i - 1][(j + 1) % 3], dp[i - 1][(j + 2) % 3]) + costs[i - 1][j];
int res = min(dp[n][0], dp[n][1]);
res = min(res, dp[n][2]);
return res;
}
};
剑指 Offer II 092. 翻转字符
如果一个由 ‘0’ 和 ‘1’ 组成的字符串,是以一些 ‘0’(可能没有 ‘0’)后面跟着一些 ‘1’(也可能没有 ‘1’)的形式组成的,那么该字符串是 单调递增 的。
我们给出一个由字符 ‘0’ 和 ‘1’ 组成的字符串 s,我们可以将任何 ‘0’ 翻转为 ‘1’ 或者将 ‘1’ 翻转为 ‘0’。
返回使 s 单调递增 的最小翻转次数。
输入:s = “010110”
输出:2
解释:我们翻转得到 011111,或者是 000111。
可以将0变成1,也可以将1变成0,但是需要将字符串变成递增的,需要找到最小翻转次数
方法1
确定数组
dp[i][0] 和 dp[i][1]分别表示下标 ii 处的字符为 0 和 1 的情况下使得 s[0…i] 单调递增的最小翻转次数。
确定递推公式
dp[i][0]说明前面的都是0 ,所以如果当前为1那么将其反转过来,如果是0不变
dp[i][1] 前面有可能是0也有可能是1这时候取前面最小值,如果当前为0将其翻转为1如果是1不变
dp[i][1]这样取会不会乱序呢?并不会
确定初始状态
代码
class Solution {
public:
int minFlipsMonoIncr(string s) {
//dp[i][0]表示前i个元素,最后一个元素为0的最小翻转次数;
//dp[i][1]表示前i个元素,最后一个元素为1的最小翻转次数
//初始化dp都为0
vector<vector<int>>dp(s.size(), vector<int>(2, 0));
//第一个元素如果是0,dp[0][1]是表示将其反转为1 这时候要加1
//如果是1,则令 dp[0][0] 将其反转为1,这时候加1
s[0] == '0' ? dp[0][1] = 1 : dp[0][0] = 1;
for (int i = 1; i < s.size(); ++i) {
//前 i 个字符翻转后得到的递增字符串分别以 '0' 结尾时共所需的最小翻转次数
int prev0 = dp[(i - 1)][0];
//前 i 个字符翻转后得到的递增字符串分别以 '1' 结尾时共所需的最小翻转次数
int prev1 = dp[(i - 1)][1];
dp[i][0] = prev0 + (s[i] != '0');//维护最后一个元素为0的数组 如果当前不是0 则+1
dp[i][1] = min(prev0, prev1) + (s[i] != '1');//维护最后一个元素为1的数组 如果当前不是1 则+1
}
int last = (s.size() - 1);//获得最后一个
return min(dp[last][0], dp[last][1]);//返回最后一个为0或者为1的最小值
}
};
由于dp只用到两个元素,当前和前面的信息,那么不用那么大空间,只用两个,维护这两个就可以
class Solution {
public:
int minFlipsMonoIncr(string s) {
vector<vector<int>>dp(2, vector<int>(2, 0));
s[0] == '0' ? dp[0][1] = 1 : dp[0][0] = 1;
for (int i = 1; i < s.size(); ++i) {
int prev0 = dp[(i - 1) % 2][0];
int prev1 = dp[(i - 1) % 2][1];
dp[i % 2][0] = prev0 + (s[i] != '0');
dp[i % 2][1] = min(prev0, prev1) + (s[i] != '1');
}
int last = (s.size() - 1) % 2;
return min(dp[last][0], dp[last][1]);
}
};
剑指 Offer II 093. 最长斐波那契数列
输入: arr = [1,2,3,4,5,6,7,8]
输出: 5
解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
当前元素要等于前两个元素的和,但是有很多符合条件的数列,返回最长的那个
注意特点,下图1+2=3 2+3=5,如果仅有两个元素那就不是序列,因此序列最少是3个数,在1、2、3基础上再加一个符合条件的2+3才使的序列增加了1位
方法一 动态规划
这三个数以 k j i表示,两个for循环,第一层是i(0~n-1),第二层是j(0 ~ i -1),然后每次都i-j得到k的值,判断k是否存在,判断k是否存在可以用一个map存储,键是数列的值,值是其下标,每次判断k是否存在并且k的下标要小于j。
另一个问题,如何增加长度呢,上面题解在有1 2 3的基础上(此时长度为3)在添加一个5,5为i的值,j为2的值,此时k是2,而2 3保存的长度是3,所以在其基础上+1,也就f[ 5 ] [ 3 ]=f[ 3] [ 2 ]+1,也就是f[ 5 ] [ 3 ]=3+1=4,但是如果没有找到,比如1 2,前面没有k值,那么长度为2。到最后返回的时候如果为2那么不返回
代码
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
vector<vector<int>> dp(arr.size(), vector<int>(arr.size()));
unordered_map<int, int> mp;//mp的键为k的值,值为对应的下标
for (int i = 0; i < arr.size(); ++i) {
mp[arr[i]] = i;
}
int ret = 0;
//i每向前一步,都判断前面的
for (int i = 1; i < arr.size(); ++i) {
//i在前面跑 j在后面追 然后找k
for (int j = 0; j < i; ++j) {
int temp = arr[i] - arr[j];//得到k的值
// 存在 k 使得 A[i] = A[j] + A[k] (0 <= k < j < i)
if (mp.count(temp) && mp[temp] < j) {
dp[i][j] = dp[j][mp[temp]] + 1;
}
// 不存在 k 使得 A[i] = A[j] + A[k] (0 <= k < j < i)
else {
dp[i][j] = 2;
}
ret = max(ret, dp[i][j]);
}
}
return ret > 2 ? ret : 0;
}
};
动态规划在查找时用上二分查找
此题的数列的特点是递增的,那么需要利用上,在查找k的时候,可以使用二分查找
代码
class Solution {
private:
int search(vector<int>& nums, int right, int target) {
int left = 0;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
}
nums[mid] > target ? right = mid - 1 : left = mid + 1;
}
return -1;
}
public:
int lenLongestFibSubseq(vector<int>& arr) {
vector<vector<int>> dp(arr.size(), vector<int>(arr.size()));
int ret = 0;
for (int i = 1; i < arr.size(); ++i) {
for (int j = 0; j < i; ++j) {
int index = search(arr, j - 1, arr[i] - arr[j]);
dp[i][j] = (index != -1) ? dp[j][index] + 1 : 2;
ret = max(ret, dp[i][j]);
}
}
return ret > 2 ? ret : 0;
}
};
剑指 Offer II 094. 最少回文分割
给定一个字符串 s,请将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的 最少分割次数 。
输入:s = “aab”
输出:1
解释:只需一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。
输入:s = “a”
输出:0
最少分割使的每个子串都是回文
剑指 Offer II 095. 最长公共子序列
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
公共子序列可以删掉几个字符但是不能改变相对顺序,只要有就行
代码
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n1 = text1.size();
int n2 = text2.size();
//初始化数组,n1+1个 vector,这个vector有n2+1个空间,即二维数组(n1+1)*(n2+1)
vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1));
for (int i = 0; i < n1; ++i) {
for (int j = 0; j < n2; ++j) {
if (text1[i] == text2[j]) {
dp[i + 1][j + 1] = dp[i][j] + 1;
}
else {
dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);
}
}
}
return dp[n1][n2];
}
};
剑指 Offer II 096. 字符串交织
方法 动态规划
用一个二维数组用来表示
s3的前i+j已经被满足,现在判断i+j+1,这个元素可能来自两个方向,一个是s1,另一个来自s2,只要这两个有一个满足就可以,也就是上面的公式
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
if (s1.size() + s2.size() != s3.size()) {
return false;
}
//弄一个二维数组
vector<vector<bool>> dp(s1.size() + 1, vector<bool>(s2.size() + 1, false));
dp[0][0] = true;
//第一行表示判断s2和s3是否匹配
for (int j = 0; j < s2.size() && s2[j] == s3[j]; ++j) {
dp[0][j + 1] = true;
}
//第一列表示判断s1和s3是否匹配
for (int i = 0; i < s1.size() && s1[i] == s3[i]; ++i) {
dp[i + 1][0] = true;
}
for (int i = 0; i < s1.size(); ++i) {
for (int j = 0; j < s2.size(); ++j) {
char ch1 = s1[i];
char ch2 = s2[j];
char ch3 = s3[i + j + 1];
dp[i + 1][j + 1] = ((ch1 == ch3) && dp[i][j + 1]) || ((ch2 == ch3) && dp[i + 1][j]);
}
}
return dp[s1.size()][s2.size()];
}
};