专题10:动态规划
题目509:斐波那契数(NO)
- 解题思路:动态五部曲
动态五部曲:这里我们用一个一维数组来保存递归的结果
-
确定dp数组以及下标的含义
dp[i]的定义为:第i个数的斐波那契数值是dp[i] -
确定递推公式
这道题已经把递推公式直接给了:状体转移方程dp[i]=dp[i-1]+dp[i-2]; -
dp数组如何初始化
这题题目把如何初始化也直接给了
dp[0]=0;
dp[1]=1; -
确定遍历顺序
从递归公式**dp[i]=dp[i-1]+dp[i-2]**中可以看出,dp[i]是依赖dp[i-1]和dp[i-2],那么遍历顺序一定是从前到后遍历的。 -
列举推导dp数组
按照这个递推公式dp[i]=dp[i-1]+dp[i-2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55
如果代码写出来发现结果不对,就把dp数组打印出来看看和我们推导的数列是否一致的。
- 题目
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
class Solution {
public:
int fib(int n) {
if(n<=1)
{
return n;
}
vector<int>dp(n+1);
dp[0]=0;
dp[1]=1;
//这里实际的个数会是n+1
for(int i=2;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
题目70:爬楼梯(NO)
- 解题思路:依旧是动规5步曲。
- 确定dp数组以及下标的含义
dp[i]:表示爬到第i层楼梯,有dp[i]种方法
这里来推导一下:dp[1]=1,dp[2]=2;dp[3]=3;dp[4]=5;这样递推又会得到和上题一样的答案
- 确定递推公式
dp[i]=dp[i-1]+dp[i-2]
- dp数组如何初始化
因为dp[0]是没有任何意义的,所以不用对其进行初始化,只需要知道dp[1]=1;dp[2]=2就可以了
- 确定遍历顺序
从递推公式dp[i]=dp[i-1]+dp[i-2]可以看出,遍历顺序一定是从前往后遍历的
- 列举dp数组
上面列举过了。
class Solution {
public:
int climbStairs(int n) {
if(n<=1) return n;
vector<int>dp(n+1);
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
- 疑惑点:为什么可以用dp[i]=dp[i-1]+dp[i-2]作为到达i层的方法数呢
具体解释如下:
- 到达第 i 层楼梯的最后一步: 想要到达第 i 层楼梯,最后一步只有两种可能:
- 从第 i-1 层爬一步。
- 从第 i-2 层爬两步。
- 方法数的累加:
- 从第 i-1 层爬一步,方法数为 dp[i-1],因为到达第 i-1 层有 dp[i-1] 种方法。
- 从第 i-2 层爬两步,方法数为 dp[i-2],因为到达第 i-2 层有 dp[i-2] 种方法。
- 总方法数: 由于最后一步只有这两种可能,所以到达第 i 层楼梯的总方法数就是这两种情况的方法数之和,即 dp[i] = dp[i-1] + dp[i-2]。
简单来说,dp[i] 就是把所有到达第 i 层楼梯的最后一步可能情况的方法数加起来。
举个例子:
假设我们要爬到第 4 层楼梯。
- 到达第 4 层的最后一步可以是:
- 从第 3 层爬一步,方法数为 dp[3]。
- 从第 2 层爬两步,方法数为 dp[2]。
- 因此,到达第 4 层的总方法数为 dp[4] = dp[3] + dp[2]。
总结:
dp[i] = dp[i-1] + dp[i-2] 这个公式体现了爬楼梯问题的递推关系,它将到达第 i 层楼梯的方法数分解为两种最后一步可能情况的方法数之和,从而简化了问题的求解过程。
题目746:使用最小花费爬楼梯(YES)
- 解题思路:动规5步曲
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
-
确定dp数组的含义
这里的dp[i]表示到达第i层所需要的最少的花费。 -
确定递推公式
dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]); -
如何初始化dp数组
根据题目就可以知道dp[0]=0 ,dp[1]=0; -
确定遍历的顺序
这里毫无疑问必然是从前往后,因为dp[i]需要用前面的两个数才能确定。 -
列举出dp数组
这里的本质意义是方便调试,但是这里比较动态,不好推导,不用列举也行。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
/*
这题依旧是动规5步曲:
1.确定dp数组的含义
dp[i]表示到达第i层最低的开销
2.确定递推公式
dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
3.dp数组如何初始化
dp[0]=0 dp[1]=0; 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
这句话表示不需要花费
*/
int n=cost.size();
vector<int>dp(n+1);
if(n<=1)
{
return 0;
}
dp[0]=0;
dp[1]=0;
for(int i=2;i<=n;i++)
{
dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[n];
}
};
题目392:判断子序列(YES)
- 解题思路:这题就是要判断s再t中顺序的出现过了,所以只要对t进行一次for只要出现了s的元素s的指针就前进比较下一个。
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
class Solution {
public:
bool isSubsequence(string s, string t) {
int count=0;
if(s=="")
{
return true;
}else if(t=="")
{
return false;
}
for(int i=0;i<t.size();i++)
{
if(s[count]==t[i])
{
count++;
}
if(count>=s.size())
{
return true;
}
}
return false;
}
};
专题11:贪心算法
题目680:验证回文串||(NO)
- 解题思路:这题的解题思路是再普通验证回文数的方法上进行叠加的,如果left和right不相等就判断左边去掉一位(left++)或者右边去掉一个(right–)后的字符是否是回文数。这个是回文数的特性。
给你一个字符串 s,最多 可以从中删除一个字符。
请你判断 s 是否能成为回文字符串:如果能,返回 true ;否则,返回 false 。
class Solution {
public:
//自己封装一个接口用来判断是否是回文字符串
bool isPalindrome(string s,int left,int right)
{
int len=s.size();
if(len==0) false;
if(len==1) true;
while(left<right)
{
if(s[left]!=s[right])
{
return false;
}
left++;
right--;
}
return true;
}
bool validPalindrome(string s) {
int left = 0, right = s.size() - 1;
while (left < right) {
char c1 = s[left], c2 = s[right];
if (c1 == c2) {
++left;
--right;
} else {
return isPalindrome(s, left, right - 1) || isPalindrome(s, left + 1, right);
}
}
return true;
}
};
题目860:柠檬水找零(NO)
- 解题思路:这题一上来想到的就是哈希表,因为能开苏查找是否有钱,当是给5元时,直接收钱就行,当给10元时,需要退5元,当要给20时,退15,而这15有两种分配方法,10+5和5+5+5。分别看这些情况能否满足要求即可。
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
//使用哈希表来存钱
unordered_map<int,int>map;
for(int i=0;i<bills.size();i++)
{
switch(bills[i])
{
case 5:
//直接收钱
map[5]++;
break;
case 10:
//需要找5元,查找是否有5元
if(map[5])
{
//有
map[5]--;
map[10]++;
}else
{
return false;
}
break;
case 20:
//需要找15元
//情况1:10+5
if(map[10]&&map[5])
{
map[10]--;
map[5]--;
map[20]++;
}else if(map[5]>=3)
{
map[5]-=3;
map[20]++;
}else
{
return false;
}
break;
}
}
return true;
}
};
题目942:增减字符串匹配(NO)
- 解题思路:I就放剩余数字中的最小数,D就放剩余数字中的最大数。(这个其实也不好想)
由范围 [0,n] 内所有整数组成的 n + 1 个整数的排列序列可以表示为长度为 n 的字符串 s ,其中:
如果 perm[i] < perm[i + 1] ,那么 s[i] == ‘I’
如果 perm[i] > perm[i + 1] ,那么 s[i] == ‘D’
给定一个字符串 s ,重构排列 perm 并返回它。如果有多个有效排列perm,则返回其中 任何一个 。
- 官方题解
class Solution {
public:
vector<int> diStringMatch(string s) {
int n = s.length(), lo = 0, hi = n;
vector<int> perm(n + 1);
for (int i = 0; i < n; ++i) {
perm[i] = s[i] == 'I' ? lo++ : hi--;
}
perm[n] = lo; // 最后剩下一个数,此时 lo == hi
return perm;
}
};
题目1005:K次取反后最大化的数组和(YES)
- 解题思路:这个k表示又这么多的负号,这些符号应该优先将负数变为正数,后面如果k不为0则拿最小的正数来处理这个k。
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
- myself(这个代码我调试的时候打了一下调试信息)
class Solution {
public:
//冒泡排序
void bubble_sort(vector<int>&nums)
{
int len=nums.size();
for(int i=0;i<len-1;i++)
{
for(int j=0;j<len-i-1;j++)
{
if(nums[j]>nums[j+1])
{
int temp=nums[j];
nums[j]=nums[j+1];
nums[j+1]=temp;
}
}
}
}
int largestSumAfterKNegations(vector<int>& nums, int k) {
//这个k实际就是又多少个负号,
//先进行排序
bubble_sort(nums);
int sum=0;//统计最大值
int current_min=INT_MAX;//记录正数的最小值
int current_min_index=0;//记录最小值的下表
for(int i=0;i<nums.size();i++)
{
//这次遍历先处理数据
//将所有的负数变成正数
if(nums[i]<0&&k>0)
{
nums[i]=abs(nums[i]);
k--;
}
//统计最小值
if(current_min>nums[i])
{
current_min=nums[i];
current_min_index=i;
}
}
for(int i=0;i<nums.size();i++)
{
std::cout<<nums[i]<<" ";
}
std::cout<<std::endl;
std::cout<<"k="<<k<<std::endl;
std::cout<<"min="<<current_min<<std::endl;
//上面这次遍历又两种情况
//一种是k==0那这种就直接加就行
//另一种是k!=0说明现在nums全部都是正数,则拿一个最小正数来处理这个就行
int ans=0;
if(k==0)
{
for(int i=0;i<nums.size();i++)
{
ans+=nums[i];
}
}else
{
for(int i=0;i<nums.size();i++)
{
if(i==current_min_index)
{
//用这个来处理剩下的k
if(k%2==1)
{
nums[i]=(-nums[i]);
}
}
ans+=nums[i];
}
}
return ans;
}
};
专题12:二分查找
题目278:第一个错误的版本(NO)
- 解题思路:使用二分查找,二分查找的本质就是每次都比较中间的数,根据中间数的情况进行变化。
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
- 官方题解
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
class Solution {
public:
int firstBadVersion(int n) {
//看似写的简单,但是这个官方题解边界处理的很好
int left = 1, right = n;
while (left < right) { // 循环直至区间左右端点相同
//这里防溢出的原因是如果right很大,你在使用(left+right)/2有溢出的风险
//用减法代替加法很巧妙
int mid = left + (right-left) / 2; // 防止计算时溢出
if (isBadVersion(mid)) {
right = mid; // 答案在区间 [left, mid] 中
} else {
left = mid + 1; // 答案在区间 [mid+1, right] 中
}
}
// 此时有 left == right,区间缩为一个点,即为答案
return left;
}
};
- 这里的int mid = left+(right-left)/2防溢出处理是非常巧妙的,设想一下如果你使用int mid=(left+right)/2有什么不妥的地方呢。编译一下你就会发现,left+right是可能溢出的因为right可以是INT_MAX。所以用减法代替加法非常巧妙。
题目367:有效的完全平方数(YES)
- 解题思路:这里使用的是二分查找,使用二分查找麻烦的地方是害怕溢出,尤其这次主要比较的还是mid*mid,所以我控制溢出第一个点就是int right=num/2+1;任何数不可以比自己一半平方还大。第二点是乘法既然如此容易溢出,那直接除法代替乘法,这和int mid;减法代替加法思想是一样的。
给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
不能使用任何内置的库函数,如 sqrt 。
- myself
class Solution {
public:
bool isPerfectSquare(int num) {
//二分查找
if(num==1)
{
return true;
}
int left=1;
int right=num/2+1;
while(left<right)
{
//还是一样使用防止溢出的策略
int mid=left+(right-left)/2;
//不丢失精度
if(num%mid==0 && mid==num/mid)
{
return true;
}else if(mid<num/mid)
{
//小了
left=mid+1;
}else
{
right=mid;
}
}
return false;
}
};
题目374:猜数字大小(YES)
- 解题思路:使用二分法查找
我们正在玩猜数字游戏。猜数字游戏的规则如下:
我会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,我选出的数字比你猜测的数字大了还是小了。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有三种可能的情况:
-1:你猜的数字比我选出的数字大 (即 num > pick)。
1:你猜的数字比我选出的数字小 (即 num < pick)。
0:你猜的数字与我选出的数字相等。(即 num == pick)。
返回我选出的数字。
- myself
class Solution {
public:
int guessNumber(int n) {
//依旧使用二分查找
int left=1;
int right=n;
while(left<right)
{
//防溢出策略
int mid = left+(right-left)/2;
if(guess(mid)==0)
{
return mid;
}else if(guess(mid)==-1)
{
//向下判断
right=mid-1;
}else
{
left=mid+1;
}
}
return left;
}
};
- 稍微整理一下二分查找的思路
在二分查找中,通过不断调整left和right的值来确定目标值的位置。在每次迭代中,通过计算中间索引mid,将搜索范围缩小一半,以便更快地找到目标值。
-
确定mid值:通过 mid = left + (right - left) / 2 计算得到中间索引mid。
-
判断mid位置:根据 mid 处的值与目标值的关系,可以分为三种情况:
- 如果 mid 值等于目标值,则找到目标,返回 mid;
- 如果 mid 值小于目标值,则目标值只可能在 mid 的右侧(mid + 1 到 right);
- 如果 mid 值大于目标值,则目标值只可能在 mid 的左侧(left 到 mid - 1)。
- 缩小搜索范围:根据上述判断结果,调整left和right的值:
- 如果 mid 处值小于目标值,则更新 left = mid + 1,排除 mid 位置;
- 如果 mid 处值大于目标值,则更新 right = mid - 1,排除 mid 位置。
通过这种方式不断地缩小搜索范围,并根据mid处的值来判断目标值可能的位置,可以确保在搜索过程中不会漏掉任何一种情况。最终,当left和right相遇时,找到的位置就是目标值的位置。这种方法能够高效地定位目标值,避免遗漏或重复搜索。
题目744:寻找比目标字母大的最小字母(NO)
- 解题思路:还是二分查找,不过如何处理二分细节还是不容易。
给你一个字符数组 letters,该数组按非递减顺序排序,以及一个字符 target。letters 里至少有两个不同的字符。
返回 letters 中大于 target 的最小的字符。如果不存在这样的字符,则返回 letters 的第一个字符。
class Solution {
public:
char nextGreatestLetter(vector<char>& letters, char target) {
int left=0, right=letters.size()-1;
int ret = 0;
while(left <= right){
int mid = left + (right-left)/2;
if(letters[mid] <= target){
//小了或相等都是需要前进
left = mid + 1;
}else{
//假如大了right-1导致结束的话,说明原来就是比他稍大的那个
ret = mid;
right = mid - 1;
}
}
return letters[ret];
}
};
题目1385:两个数组间的距离值(YES)
- 解题思路:暴力解,直接两层for
给你两个整数数组 arr1 , arr2 和一个整数 d ,请你返回两个数组之间的 距离值 。
「距离值」 定义为符合此距离要求的元素数目:对于元素 arr1[i] ,不存在任何元素 arr2[j] 满足 |arr1[i]-arr2[j]| <= d 。
- myself
class Solution {
public:
int findTheDistanceValue(vector<int>& arr1, vector<int>& arr2, int d) {
//最容易想到应该是暴力解
int ans=0;
for(int i=0;i<arr1.size();i++)
{
bool sign=true;
for(int j=0;j<arr2.size();j++)
{
if(abs(arr1[i]-arr2[j])<=d)
{
sign=false;
break;
}
}
if(sign)
{
ans++;
}
}
return ans;
}
};