文章目录
15届蓝桥杯备赛
这里推荐一本电子书,是在github上有60多k星的一本关于算法入门的书,里面用动态图片来解释了难懂的过程,我认为是一本非常好的一本书,这个是传送门[Hello算法]( Hello 算法 (hello-algo.com) )
算法讲师推荐[零茶山艾府]( 灵茶山艾府的个人空间-灵茶山艾府个人主页-哔哩哔哩视频 (bilibili.com) )
两数之和
[传送门]( 1. 两数之和 - 力扣(LeetCode) )
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hashtable;
for(int i = 0; i < nums.size(); i++)
{
auto it = hashtable.find(target-nums[i]);//找差值
if(it != hashtable.end())//找到了
{
return {it->second, i};//直接返回集合,就不需要再vector<int> v这样实例化对象
}
hashtable[nums[i]] = i;
}
return {};
}
};
创建哈希表hashtable,注意搞清key值和value值,这里的key值是nums[i]对应的值,value值是nums[i]对应的下标i。注意使用STL声明哈希表是unordered_map。核心代码在for循环里面,遍历nums.size()次,使用find和end,两者返回的都是迭代器,用auto代替,auto关键字可以自动推导类型,可以达到简写的作用。这里一遍循环将查询和初始化放在了同一步进行,真的十分简洁高效!妙就妙在它使用的是找target和nums[i]的差值,即查找当前数值和前面存好的数值相加是否等于target
刷题统计
[传送门]( 0刷题统计 - 蓝桥云课 (lanqiao.cn) )
#include <iostream>
using namespace std;
typedef long long ll;
int main()
{
// 请在此输入您的代码
ll a, b, n;
cin >> a >> b >> n;
ll res = n / (5*a + 2*b) * 7;//取余得到多少个完整的7天,减少循环次数
n %= (5*a + 2*b);//取到最后可以在7天内完成的题数
ll num[] = {a, a, a, a, a, b, b};//建立一个一周内每天完成题目数量的数组
//遍历1次num数组,因为n是取余后的值,所以不需要考虑会死循环,n<0后会自动终止循环
for(int i = 0; n > 0; i++)
{
n -= num[i];
res++;
}
cout << res;
return 0;
}
首先看到测评用例的范围,考虑到可能会出现int的范围超限。如果数据类型用的都是int的话只会有一部分的数据是能通过测试的。最开始的做题思路:使用循环结构不断累加做题数,直到超过n为止,这样就会存在可能某一次累加的时候会大于long long int的范围。所以,我们不妨从反向来看这道题
,即从n开始减做题数,直到n小于0的时候则代表取到了res的值,又考虑到是每周不断的进行,则可以用取余的方式减少循环次数。在最后一轮时定义一个名为num的数组,用来表示这7天分别的做题数,然后将前面取余得到的商进行累减,减到小于0就得到了天数res
贪心
重新分装苹果
[传送门]( 3074. 重新分装苹果 - 力扣(LeetCode) )
class Solution {
public:
int minimumBoxes(vector<int>& apple, vector<int>& capacity) {
sort(capacity.begin(), capacity.end());
int sum = accumulate(apple.begin(), apple.end(), 0);
int num = 0;
for(int i = capacity.size()-1; i >= 0; i--)
{
sum -= capacity[i];
num++;
if(sum <= 0) break;
}
return num;
}
};
观察题意,这是一道贪心算法
类型的题,我只要每次考虑容量最大的一个装入苹果,装满了就再用剩下最大容量的capacity,直到苹果全部装完,这样就能确保最后得到的值一定是最优的解,这就是贪心算法的特点。
首先将capacity数组用sort函数从小到大进行排序,如果想要从大到小进行排序就需要这样写:
sort(capacity.begin(), capacity.end(), greater<int>());
然后就是计算apple数组的总和,这里不需要遍历apple数组,只需调用accumulate函数就行,不能忘记最后一个参数代表的是从多少开始累加。
幸福值最大化的选择方案
[传送门]( 3075. 幸福值最大化的选择方案 - 力扣(LeetCode) )
class Solution {
public:
long long maximumHappinessSum(vector<int>& happiness, int k) {
long long res = 0;
sort(happiness.begin(), happiness.end());
for(int i = 0; i < k; i++)
{
res += happiness[happiness.size()-1];
happiness.pop_back();
for(auto it = happiness.begin(); it != happiness.end(); it++)
{
if(*it > 0)
{
(*it)--;
}
}
}
return res;
}
};
这是我用的暴力方法,可惜时间超限了,但是从668/674个通过的测试案例可以看出这种暴力方法是没错的,只不过是时间性能上需要得到优化,我考虑到第二层for循环,每结束一轮需要将数组里的数都要-1,就意味着每一轮都要遍历一遍数组,我的思路在用一个迭代器类型的temp,记录最后一个值为0的位置,等到下一轮遍历的时候只需要从temp位置开始遍历修改数组,可是这种办法也没能在时间范围内通过全部案例。
题解:
class Solution {
public:
long long maximumHappinessSum(vector<int>& happiness, int k) {
sort(happiness.begin(), happiness.end(), greater<int>());
long long res = 0;
int i;
for(i = 0; i < k; i++)
{
if(happiness[i] < i) break;
res += happiness[i]-i;
}
return res;
}
};
原来题解可以这么简洁…还是太菜了,还是没有把题目最简化,题解将我的两层循环直接化成一个循环来做,实在是高啊!!!我感觉问题的关键在于你需要发现下标和这个数(-1)之间的关系,下标为多少幸福值结果就需要减去你对应的下标,如果当前的幸福值小于当前的下标就直接跳出循环,因为你的幸福值不够满足-i的条件(也就是减i个-1之后仍然大于0)。
相向双指针
三数之和
[传送门]( 15. 三数之和 - 力扣(LeetCode) )
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> v;
sort(nums.begin(), nums.end());
int n = nums.size();
for(int i = 0; i < n-2; i++)//枚举第一个数nums[i]
{
int x = nums[i];
if(i && x == nums[i-1]) continue;
if(x + nums[i] + nums[i+1] > 0) break;//优化1
if(x + nums[n-2] + nums[n-1] < 0) continue;//优化2
int j = i+1, k = n-1;
while(j < k)
{
int s = x + nums[j] + nums[k];
if(s < 0) j++;
else if(s > 0) k--;
else
{
v.push_back({x, nums[j], nums[k]});
//while(nums[j] == nums[j-1] && j < k) j++;
//while(nums[k] == nums[k+1] && j < k) k--;
for (++j; j < k && nums[j] == nums[j - 1]; ++j); // 跳过重复数字
for (--k; k > j && nums[k] == nums[k + 1]; --k); // 跳过重复数字
}
}
}
return v;
}
};
这一道题是两数之和的加强版,两数之和那道题运用的是相向双指针,但前提是目标数组需要是排好序的。这一道题是枚举(第一个循环)第一个数,然后再对另外两个数采用的双指针法去取得三数之和等于0的三个数,由于这样的数不一定只有一组,所以用到的是二维数组进行存取,在添加数据的时候这里卡壳了,需要加强记忆。
盛最多水的容器
[传送门]( 11. 盛最多水的容器 - 力扣(LeetCode) )
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0, j = height.size() - 1;
int maxV = 0;
while(i < j)
{
maxV = max(maxV, min(height[i], height[j]) * (j-i));
if(height[i] < height[j])
{
i++;
}
else
{
j--;
}
}
return maxV;
}
};
这也是一道双向指针的问题,每次在取数值的时候与之前所记录的maxV做比较,如果比maxV大那么就替代之前的maxV,这里我只用一行就表达了出来:maxV = max(maxV, min(height[i], height[j]) * (j-i));min函数和(j-i)相乘得到的是当前两个下标木板能够盛的水的体积。
接雨水
[传送门]( 42. 接雨水 - 力扣(LeetCode) )
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
vector<int> pre_v(n);//前缀最大数组
pre_v[0] = height[0];
for(int i = 1; i < n; i++)
{
pre_v[i] = max(pre_v[i-1], height[i]);//前一个和当前这个取最大值
}
vector<int> sub_v(n);//后缀最大数组
sub_v[n-1] = height[n-1];
for(int i = n-2; i >= 0; i--)
{
sub_v[i] = max(sub_v[i+1], height[i]);//后一个和当前这个取最大值
}
int res = 0;
for(int i = 0; i < n; i++)
{
res += (min(pre_v[i], sub_v[i]) - height[i]);
}
return res;
}
};
这一道题难就难在解法上面,这里需要两个数组,分别是前缀最大数组和后缀最大数组,求法就是当前这个值与上一个值取较大的那个。求得两个数组之后然后再从头遍历计算res,res += (min(pre_v[i], sub_v[i]) - height[i]);宽度为1就省略不写了
同向双指针(滑动窗口)
长度最小的子数组
[传送门]( 209. 长度最小的子数组 - 力扣(LeetCode) )
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, right = 0, temp = 0, res = nums.size()+1;
while(right < nums.size())
{
temp += nums[right];
//写法1
while(temp-nums[left] >= target)
{
temp -= nums[left++];
}
if(temp >= target)
{
res = min(res, right - left + 1);//记录长度
}
//写法2
//while(temp >= target)
//{
// res = min(res, right-left+1);
// temp -= nums[left++];
//}
right++;
}
return res <= nums.size() ? res : 0;
}
};
注意这里的题目条件:满足总和大于等于,我第一遍以为只需要等于就行了,导致结果出错。这里还是运用的双指针,只不过是两个同向的双指针,所以这题也称为滑动窗口,看似用到了两个循环,实则上这个解法的时间复杂度只有O(n)。滑动窗口的动态过程就是用right指针不断向右遍历,期间的数据用temp求累和,当temp-左端点值大于等于target的时候可以放心去掉左端点值。题目要求最短长度,则每次达到符合条件的数组就与上一个取min,右边界right递增需要在循环末尾再加,不能在temp += nums[right]这里加,因为会影响right-left+1的值。最后用到的三目运算符是为了解决整个数组之和都没有大于等于target的情况。
乘积小于k的子数组
[传送门]( 713. 乘积小于 K 的子数组 - 力扣(LeetCode) )
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if(k <= 1) return 0;
int left = 0, temp = 1, res = 0;
for(int right = 0; right < nums.size(); right++)
{
temp *= nums[right];
while(temp >= k)
{
temp /= nums[left++];
}
res = res + (right - left + 1);
}
return res;
}
};
这道题和上一道题有异曲同工之妙,方法都是用了同向双指针。这里我第一遍做题没有考虑到提示所给的信息,数组元素是大于1的正整数,所以根本不需要考虑乘0和除0的问题。另外一个问题就是求出以[left,right]为边界的子数组个数,这个题解给出的公式实在是太庙了!!!我初次看到这个的时候以为会出现重复计算的情况,但是仔细带入几组数据发现这就是一个通用的公式。比如[5, 2, 6]计算出满足的子数组为[5, 2, 6]、[2, 6]、[6]一共三个,它是不会包含[5, 2]这种情况的所以不会出现重复计算的情况,因为右边界是6,所以一定要有6这个元素的子数组。
无重复字符的最长子串
[传送门]( 3. 无重复字符的最长子串 - 力扣(LeetCode) )
写法一:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0, res = 0;
unordered_map<char, int> m;
for(int right = 0; right < s.size(); right++)
{
char c = s[right];
while(m.find(c) != m.end())//有重复元素,find函数返回的是迭代器
{
m.erase(s[left++]);
}
m[c] = right;//value值为当前字符下标
res = max(res, right-left+1);
}
return res;
}
};
写法二:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0, res = 0;
unordered_set<char> set;
for(int right = 0; right < s.size(); right++)
{
char c = s[right];
while(set.count(c))//有重复元素返回值为1,无重复值返回值为0跳出循环
{
set.erase(s[left++]);
}
set.insert(c);
res = max(res, right-left+1);
}
return res;
}
};
这道题感觉做过了无数次了,但是每次回过头来看这道题就是毫无头绪…字串类似于子数组,这道题也能用滑动窗口的方法来解,由于题目中提到无重复字符,所以我们需要定义一个哈希表,这里定义哈希表有两种:unordered_map<char, int>和unordered_set< char >,两者差别在于尖角号里面数据类型个数不同,但表达的意思其实是相同的,因为char和int分别就表示字符和在s中的下标,并且是从左至右顺序遍历的s,所以这里用哪个哈希表都可以
动态规划
不同路径
[传送门]( 62. 不同路径 - 力扣(LeetCode) )
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n));
for(int i = 0; i < m; i++)//把第一列都初始化为1,都只有一种到达的情况
{
dp[i][0] = 1;
}
for(int j = 0; j < n; j++)//把第一行都初始化为1,都只有一种到达的情况
{
dp[0][j] = 1;
}
for(int k = 1; k < m; k++)//将其他格子赋值
{
for(int l = 1; l < n; l++)
{
dp[k][l] = dp[k-1][l] + dp[k][l-1];
}
}
return dp[m-1][n-1];
}
};
激动的心,颤抖的手,看到提交通过的一瞬间我都要激动的跳起来,这是我第一次做出除了斐波那契类型之外的动态规划题型,一定要好好记录一下,感觉我又行了,哈哈哈。
讲解一下当时的做题思路,将网格可以理解成二维数组,定义一个动态数组dp并初始化行m和列n,这里的vector数组初始化需要注意一下。然后写出递推关系式dp[i] [j] = dp[i-1] [j] + dp[i] [j-1],写到这里其实并没有结束,我们应当也要考虑到边界的问题,当行或列为0的时候i-1和j-1就会导致数组越界出现错误,所以行和列为0的情况下我们得分开讨论,也就是程序前两个for循环所做的事,最后一个for循环就是将其他格子按照递推关系式补全,然后返回终点格子值即dp[m-1] [n-1]。
最小路径和
[传送门]( 64. 最小路径和 - 力扣(LeetCode) )
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n));
dp[0][0] = grid[0][0];
for(int i = 1; i < m; i++)
{
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for(int j = 1; j < n; j++)
{
dp[0][j] = dp[0][j-1] + grid[0][j];
}
for(int i = 1; i < m; i++)
{
for(int j = 1; j < n; j++)
{
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
};
这道题与上一道题有异曲同工之妙,只是在初始化和递推式的地方稍有不同,在行列都>=1的情况下,其递推式为dp[i] [j] = min(dp[i-1] [j], dp[i] [j-1]) + grid[i] [j],这里与上一题不同点在输出的是最小路径和,所以得取当前位置的左和上dp小的那个再加上当前格子数值才为当前格子的dp。这里还有一个值得注意的地方,grid是一个二维数组,我们如何取到它的行和列?----->row(行) = grid.size()和colume(列) = grid[0].size()
不同路径II
[传送门]( 63. 不同路径 II - 力扣(LeetCode) )
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));//都初始化为0,表示路径为0
for(int i = 0; i < m; i++)
{
if(obstacleGrid[i][0] == 1)
{
break;
}
dp[i][0] = 1;
}
for(int j = 0; j < n; j++)
{
if(obstacleGrid[0][j] == 1)
{
break;
}
dp[0][j] = 1;
}
for(int i = 1; i < m; i++)
{
for(int j = 1; j < n; j++)
{
if(obstacleGrid[i][j] == 1)
{
continue;
}
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
连续三道动态规划顺利ac,我想狂一下了哈哈哈!这一道题就是在第一题的情况下加了一个条件:增加了一个障碍物,障碍物是不能在路径之内的。说一下我的解题过程:还是先定义一个dp,但不同的是我先将这个大小为m*n的dp都初始化为0,然后还是先分别将第一行和第一列初始化为1,只不过在循环里面添加了一个判断的条件,如果遇到了障碍就直接跳出循环,因为第一行和第一列的路径只有两种情况:1、路径都为1;2、有障碍物阻挡,那么障碍物所在格子和它之后的格子路径都为0 。再就是在其他格子的情况,当计算障碍物所在格子的dp的时候直接continue就行了因为默认值就为0,在后续累加到该路径时计算的也是0,就达到了题目中所要表达的添加障碍物之后的路径条数的意思了。
三角形最小路径和
[传送门]( 120. 三角形最小路径和 - 力扣(LeetCode) )
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int m = triangle.size();//行
if(m == 1) return triangle[0][0];
else if(m == 2) return triangle[0][0] + min(triangle[1][0], triangle[1][1]);
for(int i = 1; i < m; i++)
{
//对第一列初始化
triangle[i][0] += triangle[i-1][0];
//除了第一列和对角线之外的格子
for(int j = 1; j < i; j++)
{
triangle[i][j] += min(triangle[i-1][j-1], triangle[i-1][j]);
}
//对对角线操作
triangle[i][i] += triangle[i-1][i-1];
}
//取最后一行的最小值,即为最短路径
int minPath = triangle[m-1][0];
for (int j = 1; j < m; j++) {
minPath = min(minPath, triangle[m-1][j]);
}
return minPath;
}
};
最开始是分成三个for循环写的,看了题解之后原来可以放在一起写,实在是妙啊!三角形实际上就是一部分的矩阵,只不过在边界条件下需要注意的地方多一点,自己写的时候问题就出现在嵌套循环里的边界条件那里,导致数组越界和答案错误,这里我没有重新定义一个动态数组,而是在原来数组基础上进行更改的。解决方法跟上面三道题基本一致,就不再过多赘述了。需要注意的是:最后输出的最小路径和不是triangle[m-1] [m-1]而是取最后一行数组的最小值!