15届蓝桥杯备赛

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]而是取最后一行数组的最小值!

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值