Day9 2020.07.28&2020.07.29&2020.07.30
1.triangle
给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字
例如,给出的三角形如下:
[↵ [2],↵ [3,4],↵ [6,5,7],↵ [4,1,8,3]↵]
最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
注意:如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。
- 第一种解法:时O(n^2) 空O(n^2)
class Solution {
public:
int minimumTotal(vector<vector<int> > &triangle) {
//动态规划,从上到下计算经过最小路径
//返回最后一行的最小值
//时O(n^2) 空O(n^2)
int n=triangle.size();
vector<vector<int>> temp(n,vector<int>(n));
temp[0][0]=triangle[0][0];
for(int i=1;i<n;i++){
temp[i][0]=temp[i-1][0]+triangle[i][0];
for(int j=1;j<i;j++){
//上面一行较小的路径值加上当前节点的值
temp[i][j]=min(temp[i-1][j-1],temp[i-1][j])+triangle[i][j];
}
temp[i][i]=temp[i-1][i-1]+triangle[i][i];
}
return *min_element(temp[n-1].begin(),temp[n-1].end());
}
};
- 将第一种解法优化,空间复杂度为O(n)
class Solution {
public:
int minimumTotal(vector<vector<int> > &triangle) {
//动态规划+空间优化,使用两个一维数组就可以,上一行和当前行,之前的不需要存储
//空间复杂度为O(n)
int n=triangle.size();
vector<vector<int>> temp(2,vector<int>(n));
temp[0][0]=triangle[0][0];
for(int i=1;i<n;i++){
int cur=i%2;
int pre=1-cur;
temp[cur][0]=temp[pre][0]+triangle[i][0];
for(int j=1;j<i;j++){
上面一行较小的路径值加上当前节点的值
temp[cur][j]=min(temp[pre][j-1],temp[pre][j])+triangle[i][j];
}
temp[cur][i]=temp[pre][i-1]+triangle[i][i];
}
return *min_element(temp[(n-1)%2].begin(),temp[(n-1)%2].end());
}
};
2.minimum-path-sum
给定一个由非负整数填充的m x n的二维数组,现在要从二维数组的左上角走到右下角,请找出路径上的所有数字之和最小的路径。
注意:你每次只能向下或向右移动。
vec.end()和vec.rend()都是指向数组最后一个元素的后一个元素
class Solution {
public:
int minPathSum(vector<vector<int> >& grid) {
//空间复杂度为O(n)
if(grid.size()==0||grid[0].size()==0) return 0;
int row=grid.size();
int col=grid[0].size();
vector<int> temp(col,grid[0][0]);
//temp中第一行(0行)当前元素的值等于面前元素的累加和
for(int j=1;j<col;j++){
temp[j]=temp[j-1]+grid[0][j];
}
for(int i=1;i<row;i++){
//temp中第一列(0列)当前元素的值等于前面元素的累加和
temp[0]+=grid[i][0];
for(int j=1;j<col;j++){
temp[j]=min(temp[j-1],temp[j])+grid[i][j];
}
}
//在数组不为空的时候temp.rbegin()!=temp.end();
return *temp.rbegin();
}
};
3.unique-paths
一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。可以有多少种不同的路径从起点走到终点?
- 解法一:数学方法
C(n+m-2,m-1)=(从n+m-2开始乘,乘m-1个/(m-1)!
** 在连乘的时候一定要注意数据类型是否越界**
class Solution {
public:
int uniquePaths(int m, int n) {
long int k1=1,k2=1;
//计算(m-1)!,(n+m-2)!,(n-1)!,注意0!=1
//!!!需要注意的是,如果循环直接乘的话会超过int的范围,所以不能直接乘
//所以若m>=n只能计算从(m+n-2)开始乘(n-1)个以及(n-1)!
if(m<n){
int temp=n;
n=m;
m=temp;
}
//此时m>=n
for(int i=n+m-2,j=1;j<=n-1;i--,j++){
k1*=i;
k2*=j;
}
return k1/k2;
}
};
- 解法二:动态规划
class Solution {
public:
int uniquePaths(int m, int n) {
//使用O(n)的时间复杂度
vector<int> temp(n,1);
for(int i=1;i<m;i++){
for(int j=1;j<n;j++)
temp[j]+=temp[j-1];
}
return *temp.rbegin();
}
};
4.unique-paths-ii
继续思考题目"Unique Paths":如果在图中加入了一些障碍,有多少不同的路径?分别用0和1代表空区域和障碍
例如:
下图表示有一个障碍在3*3的图中央。
[
[0,0,0],
[0,1,0],
[0,0,0]
]
有2条不同的路径
备注:m和n不超过100.
注意特殊情况
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int> >& obstacleGrid) {
int row=obstacleGrid.size();
int col=obstacleGrid[0].size();
if(obstacleGrid[0][0]==1||obstacleGrid[row-1][col-1]==1) return 0;
//也要注意第一行和第一列,如果其中有1,则1后面的路径数都为0
//特殊情况:开始和结尾都为障碍,则只有0条路径
//此时便将上述特殊情况包含在内
//但上面的语句还是保留了,为的是可以提前结束,减少特殊情况的运行时间
vector<int> temp(col,0);
for(int j=0;j<col;j++){
if(obstacleGrid[0][j]==0) temp[j]=1;
else break;
}
for(int i=1;i<row;i++){
if(obstacleGrid[i][0]==1) temp[0]=0;
for(int j=1;j<col;j++){
if(obstacleGrid[i][j]==1) temp[j]=0;
else temp[j]+=temp[j-1];
}
}
return *temp.rbegin();
}
};
5.climbing-stairs
你在爬楼梯,需要n步才能爬到楼梯顶部
每次你只能向上爬1步或者2步。有多少种方法可以爬到楼梯顶部?
class Solution {
public:
int climbStairs(int n) {
int d[n+1];
d[0]=1;
d[1]=1;
for(int i=2;i<=n;i++){
d[i]=d[i-1]+d[i-2];
}
return d[n];
}
};
6.jump-game
给出一个非负整数数组,你最初在数组第一个元素的位置,数组中的元素代表你在这个位置可以跳跃的最大长度,判断你是否能到达数组最后一个元素的位置。
例如:
A =[2,3,1,1,4], 返回 true.
A =[3,2,1,0,4], 返回 false.
class Solution {
public:
bool canJump(int* A, int n) {
vector<int> B(n,0);
B[0]=1;
for(int i=0;i<n&&1==B[i];i++){
if(i+A[i]>=n-1) return true;
else for(int j=i+1;j<=i+A[i];j++){B[j]=1;}
}
if(0==*B.rbegin()) return false;
else return true;
}
};
7.jump-game-ii
给出一个非负整数数组,你最初在数组第一个元素的位置,数组中的元素代表你在这个位置可以跳跃的最大长度,你的目标是用最少的跳跃次数来到达数组的最后一个元素的位置。
例如:给出数组 A =[2,3,1,1,4]
最少需要两次才能跳跃到数组最后一个元素的位置。(从数组下标为0的位置跳长度1到达下标1的位置,然后跳长度3到数组最后一个元素的位置)
class Solution {
public:
int jump(int* A, int n) {
//使用贪心算法求解
int maxPos=0;//指向下一步最大可跳跃的位置
int jump=0;//记录再次起跳的位置
int step=0;//记录需要的步数
//不考虑从最后一个位置起跳的情况
for(int i=0;i<n-1;i++){
if(maxPos>=i){
maxPos=max(maxPos,i+A[i]);
if(i==jump){
jump=maxPos;
++step;
}
}
}
return step;
}
};
8.palindrome-partitioning-ii
给出一个字符串s,分割s使得分割出的每一个子串都是回文串,计算将字符串s分割成回文分割结果的最小切割数
例如:给定字符串s=“aab”,
返回1,因为回文分割结果[“aa”,“b”]是切割一次生成的。
- 第一种解法:运行超时!!!
class Solution {
public:
int minCut(string s) {
int n=s.length();
if(n<2) return 0;
vector<int> dp(n);
for(int i=0;i<n;i++){
dp.at(i)=i;
}
for(int i=0;i<n;i++){
//dp[i]表示前缀子串s[0:i]分割成若干个回文子串所需要的最小分割次数
//单个字符一定是回文数,所以dp[0]=0;
if(checkPalindrome(s,0,i)){//保证s[0:i]是回文串,才能对后面的进行分割
dp[i]=0;
continue;
}
//当j==i时,s[i]就一个字符,一定时回文
//所以枚举到i-1即可
for(int j=0;j<i;j++){
if(checkPalindrome(s,j+1,i)){
dp[i]=min(dp[i],dp[j]+1);
}
}
}
return dp[n-1];
}
bool checkPalindrome(string s,int left,int right){
while(left<right){
if(s[left]!=s[right]) return false;
left++;
right--;
}
return true;
}
};
- 优化:
class Solution {
public:
int minCut(string s) {
int n=s.length();
if(n<2) return 0;
vector<int> dp(n);
for(int i=0;i<n;i++){
dp.at(i)=i;
}
//用于记录子串s[a:b]是否为回文串,一开始全部初始化为false
//用一次遍历,记录所有子串是否为回文串
vector<vector<bool>> checkPalindrome(n,vector<bool>(n,false));
for(int right=0;right<n;right++){
for(int left=0;left<=right;left++){
if(s[left]==s[right]&&(right-left<=2||checkPalindrome[left+1][right-1])){
checkPalindrome[left][right]=true;
}
}
}
for(int i=0;i<n;i++){
//dp[i]表示前缀子串s[0:i]分割成若干个回文子串所需要的最小分割次数
//单个字符一定是回文数,所以dp[0]=0;
if(checkPalindrome[0][i]){//保证s[0:i]是回文串,才能对后面的进行分割
dp[i]=0;
continue;
}
//当j==i时,s[i]就一个字符,一定时回文
//所以枚举到i-1即可
for(int j=0;j<i;j++){
if(checkPalindrome[j+1][i]){
dp[i]=min(dp[i],dp[j]+1);
}
}
}
return dp[n-1];
}
};
9.longest-increasing-subsequence
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
- 第一种解法:时间复杂度为O(n^2)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
if(n==0) {
return 0;
}
vector<int> dp(n,1);
for(int i=0;i<n;i++){
//j遍历i之前的所有数
for(int j=0;j<i;j++){
//若nums[j]<nums[i]则nums[i]更新为max(dp[i],dp[j]+1)
if(nums[j]<nums[i]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
return *max_element(dp.begin(),dp.end());
}
};
- 第二种解法:贪心+二分查找(时间复杂度为O(nlong),空间复杂度为O(n))
整个算法流程为:
设当前已求出的最长上升子序列的长度为len(初始时为 1),从前往后遍历数组 nums,在遍历到nums[i] 时:
如果nums[i] >d[len] ,则直接加入到 d 数组末尾,并更新len=len+1;
否则,在 d 数组中二分查找,找到第一个比nums[i] 小的数 d[k] ,并更新d[k+1]=nums[i]。
以输入序列[0,8,4,12,2] 为例:
第一步插入 0,d=[0];
第二步插入 8,d=[0,8];
第三步插入 4,d=[0,4];
第四步插入 12,d=[0,4,12];
第五步插入 2,d=[0,2,12]。
最终得到最大递增子序列长度为 3。
来源:力扣(LeetCode)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
int len=1;
if(n==0) {
return 0;
}
vector<int> d(n+1,0);
d[len]=nums[0];
for(int i=1;i<n;i++){
if(nums[i]>d[len]) d[++len]=nums[i];
else{//二分查找
int left=0,right=len,pos=0;
// 如果找不到说明所有的数都比nums[i]大,此时要更新 d[1],所以这里将pos设为0
while(left<=right){
int mid=(left+right)>>1;
if(d[mid]<nums[i]){
pos=mid;
left=mid+1;
}else right=mid-1;
}
d[pos+1]=nums[i];
}
}
return len;
}
};
10.word-break
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:拆分时可以重复使用字典中的单词。你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
auto wordDictSet=unordered_set<string>();
for(auto word:wordDict){
wordDictSet.insert(word);
}
vector<bool> dp(s.size()+1);
dp[0]=true;
for(int i=1;i<=s.size();i++){
for(int j=0;j<i;j++){
if(dp[j]&&(wordDictSet.find(s.substr(j,i-j))!=wordDictSet.end()))
{
dp[i]=true;
break;
}
}
}
return dp[s.size()];
}
};
11.longest-common-subsequence
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
- 解法一:空间复杂度为O(n^2)
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int row = text1.length();
int col = text2.length();
if (row == 0 || col == 0) return 0;
vector<vector<int>> dp(row+1,vector<int>(col+1,0));
for(int i=1;i<=row;i++){
for(int j=1;j<=col;j++){
if(text1[i-1]==text2[j-1]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[row][col];
}
};
- 解法二:空间复杂度为O(n)
因为状态转移是O(i)(j)=O(i-1)(j-1)+1,所以要记得保存O(i-1)(j-1)的值
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int row = text1.length();
int col = text2.length();
if (row == 0 || col == 0) return 0;
//让索引为0的表示空串
//所以下面的遍历中i和j分别要遍历到row和col
vector<int> dp(col + 1, 0);
int i = 1, j = 1;//使用两个指针分别遍历数组
int last = 0;//因为只用了O(n)的空间,last记录dp[i-1][j-1]未更新之前的值
for (i = 1; i <= row; i++,last=0) {//还要注意,每一次计算每一行的第一个元素的时候last需要初始化
for (j = 1; j <= col; j++) {
int t = dp[j];
if (text1[i-1] == text2[j-1]) dp[j] = last + 1;//这里若仍为dp[j]=dp[j-1]+1则会出错,因为dp[j+1]已经被更新
else {
dp[j] = max(dp[j], dp[j-1]);
}
last = t;
}
}
return dp[col];
}
};
12.edit-distance
给定两个单词word1和word2,请计算将word1转换为word2至少需要多少步操作。你可以对一个单词执行以下3种操作:
a)在单词中插入一个字符
b)删除单词中的一个字符
c)替换单词中的一个字符
解题思路:dp[i][j]代表 word1 到 i 位置转换成 word2 到 j 位置需要最少步数
所以,当 word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];
当 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。
针对第一行,第一列要单独考虑,我们引入 ‘’ 下图所示
第一行,是 word1 为空变成 word2 最少步数,就是插入操作
第一列,是 word2 为空,需要的最少步数,就是删除操作
作者:powcai 力扣(LeetCode)
链接:https://leetcode-cn.com/problems/edit-distance/solution/zi-di-xiang-shang-he-zi-ding-xiang-xia-by-powcai-3/
class Solution {
public:
int minDistance(string word1, string word2) {
int row=word1.length();
int col=word2.length();
if(row*col==0) return row+col;
vector<vector<int>> dp(row+1,vector<int>(col+1,0));
//处理第一行和第一列
for(int i=1;i<=row;i++){
dp[i][0]=i;
}
for(int j=1;j<=col;j++){
dp[0][j]=j;
}
for(int i=1;i<=row;i++){
for(int j=1;j<=col;j++){
if(word1[i-1]==word2[j-1]) dp[i][j]=dp[i-1][j-1];
else dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
}
}
return dp[row][col];
}
};
13.coin-change
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max=amount+1;
//自下而上动态规划
//F(i)=minF(i-cj)+1;cj代表第j枚硬币的面值
vector<int> dp(amount+1,Max);
dp[0]=0;
for(int i=1;i<=amount;i++){
for(int j=1;j<coins.size();j++){
if(coins[j]<=i){
dp[i]=min(dp[i],dp[i-coins[j]]+1);
}
}
}
return dp[amount]>amount?-1:dp[amount];
}
};
14.coin-change
在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]
示例1:输入: [3,4,8,5], backpack size=10 输出: 9
示例2:输入: [2,3,5,7], backpack size=12 输出: 12
挑战
O(n x m) 的时间复杂度 and O(m) 空间复杂度,如果不知道如何优化空间O(n x m) 的空间复杂度也可以通过.
class Solution {
public:
int backPack(int m, vector<int>& A) {
int n = A.size();
vector<int> dp(m+1, 0);
//自顶向下
for (int i = 1; i <= n; i++) {
for (int j = m; j>=A[i-1] ; j--) {
dp[j] = max(dp[j], dp[j-A[i-1]]+ A[i-1]);
}
}
return dp[m];
}
};
15.coin-change-ii
有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.问最多能装入背包的总价值是多大?
示例1:
输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9
示例2:
输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10
挑战:O(nm) 空间复杂度可以通过, 不过你可以尝试 O(m) 空间复杂度吗?
注意事项:
A[i], V[i], n, m 均为整数
你不能将物品进行切分;你所挑选的要装入背包的物品的总大小不能超过 m;每个物品只能取一次.
在这里插入代码片