在刷题之前需要反复练习的编程技巧,尤其是手写各类数据结构实现,它们好比就是全真教的上乘武功
55. 跳跃游戏
// 尽可能到达最远位置(贪心)。
// 如果能到达某个位置,那一定能到达它前面的所有位置
bool canJump(int *nums, int numsSize)
{
// reach 记录当前能达到最远的地方
int reach = nums[0];
for (int i = 0; i < numsSize; ++i) {
// 如果 i > reach, 说明i位置已经跳出了reach的范围,返回false. i为当前位置
if (i > reach) {
return false;
}
// 根据当前位置i及当前能跳的最远距离nums[i]之和来更新 reach.
reach = fmax(i + nums[i], reach);
}
return true;
}
45. 跳跃游戏 II
int jump(int *nums, int numsSize)
{
int steps = 0;
int maxReach = 0;
int maxPos = 0;
// 维护当前能够到达的最大下标位置,记为边界。
// 从左到右遍历数组,到达边界时,更新边界并将跳跃次数增加1
for (int i = 0; i < numsSize-1; ++i) {
// 只是更新maxReach,并没有移动 i
maxReach = fmax((i + nums[i]), maxReach);
if (i == maxPos) {
maxPos = maxReach;
++steps;
if (maxReach >= numsSize - 1) {
return steps;
}
}
}
return 0;
}
0_ 举一个形象的例子_教室调度问题
一、理论知识
「解决一个问题需要多个步骤,每一个步骤有多种选择」这样的描述我们在「回溯算法」「动态规划」算法中都会看到。它们的区别如下:
「回溯算法」需要记录每一个步骤、每一个选择,用于回答所有具体解的问题;
「动态规划」需要记录的是每一个步骤、所有选择的汇总值(最大、最小或者计数);
「贪心算法」由于适用的问题,每一个步骤只有一种选择,一般而言只需要记录与当前步骤相关的变量的值。
对于不同的求解目标和不同的问题场景,需要使用不同的算法。动态规划与贪心算法的区别,我们画在下面这张图里。
可以使用「贪心算法」的问题需要满足的条件:
1. 最优子结构:规模较大的问题的解由规模较小的子问题的解组成,区别于「动态规划」,可以使用「贪心算法」的问题「规模较大的问题的解」只由其中一个「规模较小的子问题的解」决定;
2. 无后效性:后面阶段的求解不会修改前面阶段已经计算好的结果;
3. 贪心选择性质:从局部最优解可以得到全局最优解。
对「最优子结构」和「无后效性」的理解同「动态规划」,「贪心选择性质」是「贪心算法」最需要关注的内容。
二、通过具体例子理解贪心算法
455. 分发饼干
– 将最大的饼干给最贪心的小朋友吃
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 g[i] ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 s[j] 。如果 s[j] >= g[i] ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
输入: [1,2,3], [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
手法1:每个孩子最多只能给一块饼干,所以存在饼干大于孩子的胃口;
手法2:先做排序,然后将饼干和胃口都由小至大来进行分配;
int cmp(int* a, int* b) {
return *a - *b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize) {
qsort(g, gSize, sizeof(int), cmp);
qsort(s, sSize, sizeof(int), cmp);
int numOfChildren = gSize, numOfCookies = sSize;
int count = 0;
for (int i = 0, j = 0; i < numOfChildren && j < numOfCookies; i++, j++) {
while (j < numOfCookies && g[i] > s[j]) {
j++;
}
if (j < numOfCookies) {
count++;
}
}
return count;
}
「贪心算法」总是做出在当前看来最好的选择就可以完成任务;
解决「贪心算法」几乎没有套路,到底如何贪心,贪什么与我们要解决的问题密切相关。因此刚开始学习「贪心算法」的时候需要学习和模仿,然后才有直觉,猜测一个问题可能需要使用「贪心算法」,进而尝试证明,学会证明。
三、找零钱问题
可以使用「贪心算法」的一类经典问题是找零钱问题。
在生活中,我们找给别人零钱,通常都是按照「先给出尽可能多的面值较大的纸币(硬币),然后再给出尽可能多的面值第二大的纸币(硬币)」,直到凑成了我们需要凑出的金额为止,这样找零钱得到的纸币(硬币)的张数(个数)最少。能够这样做,与 可选的硬币(纸币)的面值密切相关,大家可以仔细想一想这个问题,相信会是一个非常不错的思考问题
860. 柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
bool lemonadeChange(int* bills, int billsSize) {
int five = 0, ten = 0;
for (int i = 0; i < billsSize; i++) {
if (bills[i] == 5) {
five++;
} else if (bills[i] == 10) {
if (five == 0) {
return false;
}
five--;
ten++;
} else {
if (five > 0 && ten > 0) {
five--;
ten--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
四、区域选择问题
有一类使用「贪心算法」解决的问题称为「活动选择问题」,解决这一类问题的直觉是「优先选择活动最早的活动」。我们下面给出的三道例题都给出了对这一类问题的直觉的证明。如果还想了解「活动选择问题」的详细讨论,可以查看《算法导论》第 16.1 节「活动选择问题」的叙述。
435. 无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
思路与算法:
我们不妨想一想应该选择哪一个区间作为首个区间。
假设在某一种最优的选择方法中,[l_k, r_k]是首个(即最左侧的)区间,那么它的左侧没有其它区间,右侧有若干个不重叠的区间。设想一下,如果此时存在一个区间 [l_j, r_j] 使得 r_j < r_k ,即区间 j 的右端点在区间 k 的左侧,那么我们将区间 k 替换为区间 j,其与剩余右侧被选择的区间仍然是不重叠的。而当我们将区间 k 替换为区间 j 后,就得到了另一种最优的选择方法。
我们可以不断地寻找右端点在首个区间右端点左侧的新区间,将首个区间替换成该区间。那么当我们无法替换时,**首个区间就是所有可以选择的区间中右端点最小的那个区间。因此我们将所有区间按照右端点从小到大进行排序,那么排完序之后的首个区间,就是我们选择的首个区间。
如果有多个区间的右端点都同样最小怎么办?由于我们选择的是首个区间,因此在左侧不会有其它的区间,那么左端点在何处是不重要的,我们只要任意选择一个右端点最小的区间即可。
当确定了首个区间之后,所有与首个区间不重合的区间就组成了一个规模更小的子问题。由于我们已经在初始时将所有区间按照右端点排好序了,因此对于这个子问题,我们无需再次进行排序,只要找出其中与首个区间不重合并且右端点最小的区间即可。用相同的方法,我们可以依次确定后续的所有区间。
简单点说,Leetcode上网友总结了一套罗志祥算法:
手法1:首先要明白为啥一定要选择最早结束的(见上图–罗志祥算法)也就是选 – 首个区间就是所有可以选择的区间中右端点最小的那个区间
手法2:如果有多个区间的右端点都同样最小怎么办?由于我们选择的是首个区间,因此在左侧不会有其它的区间。-- 假设在某一种最优的选择方法中,[l_k, r_k]是首个(即最左侧的)区间,那么它的左侧没有其它区间,右侧有若干个不重叠的区间。即便左侧有,我们也不关心,没有影响,我们要去掉的是 右侧的重复区间。
int cmp(int** a, int** b) {
return (*a)[1] - (*b)[1];
}
int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize) {
if (intervalsSize == 0) {
return 0;
}
qsort(intervals, intervalsSize, sizeof(int*), cmp);
int right = intervals[0][1]; // 找到首区间
int ans = 1;
for (int i = 1; i < intervalsSize; ++i) { // 更新区间
if (intervals[i][0] >= right) {
++ans;
right = intervals[i][1];
}
}
return intervalsSize - ans; // 只要没能更新进来的,都是重复区间.
}
五、跳跃问题
这一节我们的跳跃问题也是使用「贪心算法」解决的经典问题。对于不同的目标「贪心算法」贪心的点是不一样的。大家可以在学习完这两个例题之后,分析它们之间的区别。
手法1:我们依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置 reach。对于当前遍历到的位置 i,如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置(哪怕是一步一步跳过去,肯定能跳过去)。
手法2:关键就是维护最远可以达到的位置,起初想着用一个数组,但空间复杂度高了,且没有必要,我们只要看最远位置,搞个int reach 就可以了.
bool canJump(int *nums, int numsSize)
{
int reach = nums[0]; // reach 记录当前能达到最远的地方
for (int i = 0; i < numsSize; ++i) {
if (reach < i) { // 如果 reach < i, 说明i位置已经跳出了reach的范围,返回false. i为当前位置,
return false;
}
// 根据当前位置i及当前能跳的最远距离nums[i]之和来更新 reach. 每次i++来更新一次
reach = fmax(i + nums[i], reach);
}
return true;
}
5788. 字符串中的最大奇数
思路:
我们从右到左遍历 num 中的字符,当遍历到第一个值为奇数的字符时,我们假设它的下标为 i,此时子字符串 num[0…i+1] 即为值为奇数且值最大的子字符串,我们返回该子字符串作为答案;
– 说来也巧,有的题,看似是编程题,其实是数学题,细想就是从右到左找到第一个奇数就是最大的奇数.
#define MAX_NUM 100001
char *largestOddNumber(char *num)
{
const char *s = "";
char *numStr = (char *)malloc(sizeof(char) * MAX_NUM);
int len = strlen(num);
int numSingle = 0;
int i = len - 1;
int pos = 0;
int flag = 0;
while (i >= 0 && flag == 0) {
numSingle = (num[i] - '0');
if (numSingle % 2 == 1) {
pos = i;
flag = 1;
continue;
}
i--;
}
for (i = 0; i <= pos; i++) {
numStr[i] = num[i];
}
numStr[i] = '\0';
if (flag == 1) {
return numStr;
}
return (char *)s;
}
376. 摆动序列
观察这个序列可以发现,我们不断地交错选择「峰」与「谷」,可以使得该序列尽可能长
在实际代码中,我们记录当前序列的上升下降趋势。每次加入一个新元素时,用新的上升下降趋势与之前对比,如果出现了「峰」或「谷」,答案加一,并更新当前序列的上升下降趋势。
int wiggleMaxLength(int* nums, int numsSize) {
if (numsSize < 2) {
return numsSize;
}
int prevdiff = nums[1] - nums[0];
int ret = prevdiff != 0 ? 2 : 1;
for (int i = 2; i < numsSize; i++) {
int diff = nums[i] - nums[i - 1];
if ((diff > 0 && prevdiff <= 0) || (diff < 0 && prevdiff >= 0)) {
ret++;
prevdiff = diff;
}
}
return ret;
}