文章目录
分发饼干
思路:贪心+双指针+排序
- 排序:对s和g都进行升序排列
- 双指针遍历:
- 使用两个指针
i
和j
分别遍历孩子数组g
和饼干数组s
。 - 如果当前饼干
s[j]
可以满足当前孩子g[i]
的胃口(即s[j] >= g[i]
),则满足一个孩子,ans
加 1,并移动到下一个孩子(i++
)。 - 无论当前饼干是否满足当前孩子,都要移动到下一个饼干(
j++
)。
- 使用两个指针
代码
#include <stdlib.h>
// 比较函数,用于排序
int cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize) {
// 对孩子的胃口和饼干的大小进行排序
qsort(g, gSize, sizeof(int), cmp);
qsort(s, sSize, sizeof(int), cmp);
int i = 0, j = 0;
int ans = 0;
// 使用双指针遍历孩子和饼干数组
while (i < gSize && j < sSize) {
// 如果当前饼干可以满足当前孩子的胃口
if (s[j] >= g[i]) {
ans++; // 满足一个孩子
i++; // 移动到下一个孩子
}
j++; // 无论是否满足,都移动到下一个饼干
}
return ans;
}
摆动序列
思路:贪心
如图,我们可以将所有序列的关系化为这样的摆动图,取所有的峰值和低谷就可以得到摆动序列,计算峰值与低谷的数量即可。
代码
int wiggleMaxLength(int* nums, int numsSize) {
if (numsSize < 2) {
return numsSize; // 如果数组长度小于2,直接返回长度
}
int prevDiff = nums[1] - nums[0]; // 前一个差值
int ans;
if(prevDiff==0)ans=1; // 初始长度,根据前两个元素是否相等决定
else ans=2;
for (int i = 2; i < numsSize; i++) {
int currDiff = nums[i] - nums[i - 1]; // 当前差值
if ((prevDiff <= 0 && currDiff > 0) || (prevDiff >= 0 && currDiff < 0)) {
ans++; // 如果差值符号发生变化,摆动序列长度加1
prevDiff = currDiff; // 更新前一个差值
}
}
return ans;
}
最大子数组和
思路一:贪心
遍历数组:
- 如果当前子数组的和
sum
大于0,说明当前子数组对后续的子数组和有贡献,因此继续累加当前元素。 - 如果当前子数组的和
sum
小于或等于0,说明当前子数组对后续的子数组和没有贡献,因此重新开始计算子数组的和,从当前元素开始。 - 在每一步更新最大子数组和
ans
,确保ans
始终保存当前找到的最大子数组和。
int maxSubArray(int* nums, int numsSize) {
// 初始化结果变量为数组的第一个元素
int ans = nums[0];
// 初始化当前子数组的和为数组的第一个元素
int sum = nums[0];
// 从数组的第二个元素开始遍历
for (int i = 1; i < numsSize; i++) {
// 如果当前子数组的和大于0,继续累加当前元素
if (sum > 0) {
sum += nums[i];
}
// 否则,重新开始计算子数组的和,从当前元素开始
else {
sum = nums[i];
}
// 更新最大子数组和
ans = fmax(sum, ans);
}
// 返回最大子数组和
return ans;
}
思路二:动态规划
- 动态规划:使用动态规划的思想,通过定义状态和状态转移方程来解决问题。
- 状态定义:
dp[i]
表示以nums[i]
结尾的最大子数组和。 - 状态转移方程:
dp[i] = max(dp[i-1] + nums[i], nums[i])
。- 如果
dp[i-1]
是正数,那么加上nums[i]
会使得子数组和更大。 - 如果
dp[i-1]
是负数,那么从nums[i]
重新开始计算子数组和会更优。
- 如果
- 状态定义:
- 初始化:
dp[0]
初始化为nums[0]
,因为以第一个元素结尾的最大子数组和就是它本身。ans
初始化为dp[0]
,用于记录全局最大子数组和。
int maxSubArray(int* nums, int numsSize) {
// 动态规划数组,dp[i] 表示以 nums[i] 结尾的最大子数组和
int dp[numsSize];
// 初始化 dp 数组为 0
memset(dp, 0, sizeof(dp));
// 初始化 dp[0] 为数组的第一个元素
dp[0] = nums[0];
// 初始化结果为 dp[0]
int ans = dp[0];
// 从数组的第二个元素开始遍历
for (int i = 1; i < numsSize; i++) {
// 状态转移方程:dp[i] 要么是前一个子数组和加上当前元素,要么是当前元素本身
dp[i] = fmax(dp[i - 1] + nums[i], nums[i]);
// 更新全局最大子数组和
ans = fmax(ans, dp[i]);
}
// 返回全局最大子数组和
return ans;
}
买卖股票的最佳时机Ⅱ
思路:贪心
- 贪心策略:通过捕捉所有正收益的交易来最大化利润。
- 只要今天的价格比前一天高,就认为可以获利,并将差价累加到总利润中。
- 初始化:
- 将最大利润
ans
初始化为 0。
- 将最大利润
- 遍历价格数组:
- 从第二天开始遍历,计算当前价格与前一天的差价
tmp
。 - 如果差价
tmp
大于 0,说明可以获利,将其累加到ans
中。
- 从第二天开始遍历,计算当前价格与前一天的差价
代码
int maxProfit(int* prices, int pricesSize) {
// 初始化最大利润为 0
int ans = 0;
// 从第二天开始遍历
for (int i = 1; i < pricesSize; i++) {
// 计算当前价格与前一天的差价
int tmp = prices[i] - prices[i - 1];
// 如果差价大于 0,说明可以获利,累加到结果中
if (tmp > 0) {
ans += tmp;
}
}
// 返回最大利润
return ans;
}
跳跃游戏
思路:贪心
- 贪心算法:贪心策略,通过维护一个变量
sum
来记录当前能到达的最远位置。- 在遍历数组时,不断更新
sum
,确保它始终表示从当前位置能跳到的最远位置。
- 在遍历数组时,不断更新
- 初始化:
- 将
sum
初始化为 0,表示初始位置。
- 将
- 遍历数组:
- 对于每个位置
i
,检查是否能从之前的位置跳到当前位置(即sum >= i
)。 - 如果能跳到当前位置,则更新
sum
为max(sum, i + nums[i])
,表示从当前位置能跳到的最远位置。 - 如果
sum
已经超过或等于最后一个位置(numsSize - 1
),则返回true
。
- 对于每个位置
代码
bool canJump(int* nums, int numsSize) {
int sum = 0;
for (int i = 0; i < numsSize; i++) {
// 如果当前位置无法到达,直接返回 false
if (sum < i) {
return false;
}
// 更新能到达的最远位置
sum = fmax(sum, i + nums[i]);
// 如果最远位置已经超过或等于最后一个位置,返回 true
if (sum >= numsSize - 1) {
return true;
}
}
return false;
}
跳跃游戏Ⅱ
思路一:贪心
- 使用贪心策略,每次在当前跳跃范围内选择能跳得最远的位置。
- 维护两个变量:
sum
:当前能够到达的最远位置。cur
:在当前跳跃范围内,能够到达的最远位置。
- 当遍历到
sum
时,表示当前跳跃范围已经用尽,需要进行一次新的跳跃,并更新sum
为cur
。
int jump(int* nums, int numsSize) {
if (numsSize <= 1) return 0; // 如果数组长度为 0 或 1,不需要跳跃
int sum = 0; // 当前能够到达的最远位置
int cnt = 0; // 跳跃次数
int cur = 0; // 在当前跳跃范围内,能够到达的最远位置
for (int i = 0; i < numsSize - 1; i++) {
if (sum >= numsSize - 1) return cnt; // 如果已经能够到达终点,直接返回跳跃次数
// 更新当前能够到达的最远位置
if (cur < nums[i] + i) {
cur = nums[i] + i;
}
// 当遍历到 sum 时,表示当前跳跃范围已经用尽,需要进行一次新的跳跃
if (sum == i) {
sum = cur; // 更新 sum 为当前能够到达的最远位置
cnt++; // 增加跳跃次数
}
}
return cnt; // 返回跳跃次数
}
思路二:动态规划
-
定义状态:
- 设
dp[i]
表示从起点(位置0
)跳到位置i
所需的最小跳跃次数。
- 设
-
状态转移方程:
-
对于每个位置
i
,遍历所有可以跳到i
的位置j
(即j + nums[j] >= i
),然后更新dp[i]
为dp[j] + 1
的最小值。 -
状态转移方程可以表示为:
dp[i]=min(dp[i],dp[j]+1)
其中j+nums[j]≥i
-
-
初始化:
dp[0] = 0
,因为起点不需要跳跃。- 其他位置的
dp[i]
初始化为一个较大的值(如INT_MAX
),表示暂时无法到达。
-
最终结果:
dp[numsSize - 1]
即为从起点跳到终点的最小跳跃次数。
#include <limits.h>
int jump(int* nums, int numsSize) {
if (numsSize <= 1) return 0; // 如果数组长度为 0 或 1,不需要跳跃
int dp[numsSize]; // 定义 dp 数组
for (int i = 0; i < numsSize; i++) {
dp[i] = INT_MAX; // 初始化 dp 数组为最大值
}
dp[0] = 0; // 起点不需要跳跃
// 动态规划求解
for (int i = 1; i < numsSize; i++) {
for (int j = 0; j < i; j++) {
if (j + nums[j] >= i) { // 如果可以从 j 跳到 i
dp[i] = fmin( dp[i] ,dp[j] + 1); // 更新 dp[i]
}
}
}
return dp[numsSize - 1]; // 返回到达终点的最小跳跃次数
}
K次取反后最大化的数组和
思路:贪心
- 核心思想:
- 为了使数组的和最大,应该优先对最小的负数取反(因为负数取反后会变成正数,增加总和)。
- 如果所有负数都取反后,还有剩余的取反次数,应该对最小的正数反复取反(因为最小的正数取反后对总和的影响最小)。
- 步骤:
- 将数组排序,方便优先处理最小的负数。
- 遍历数组,对负数取反,直到用完
k
次取反操作或没有负数为止。 - 如果还有剩余的取反次数,对最小的正数反复取反。
- 计算最终的总和。
给出两种代码,一种是if-else但是速度更快,击败100%,一种是优化后的代码,便于阅读。
代码一:
#include <stdlib.h> // 用于 qsort 函数
#include <math.h> // 用于 fabs 和 fmin 函数
// 比较函数,用于排序
int cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b; // 升序排序
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
// 对数组进行升序排序,方便优先处理最小的负数
qsort(nums, numsSize, sizeof(int), cmp);
int sum = 0; // 用于存储最终的和
// 如果数组只有一个元素
if (numsSize == 1) {
if (k & 1) { // 如果 k 是奇数,取反一次
return 0 - nums[0];
}
return nums[0]; // 如果 k 是偶数,直接返回原值
}
// 遍历数组
for (int i = 0; i < numsSize; i++) {
if (nums[i] < 0) { // 如果当前元素是负数
if (k > 0) { // 如果还有取反次数
sum -= nums[i]; // 取反并累加到 sum
k--; // 减少取反次数
} else {
sum += nums[i]; // 如果没有取反次数,直接累加
}
} else { // 如果当前元素是非负数
if (k % 2 == 0) { // 如果剩余的取反次数是偶数
sum += nums[i]; // 直接累加
} else { // 如果剩余的取反次数是奇数
if (i == 0) { // 如果是第一个元素
sum -= nums[i]; // 取反并累加
k = 0; // 取反次数用完
} else if (nums[i] + nums[i - 1] > 0) { // 如果当前元素和前一个元素的和为正
sum += 2 * nums[i - 1]; // 将前一个元素取反两次(相当于恢复原值)
sum += nums[i]; // 累加当前元素
k = 0; // 取反次数用完
} else { // 如果当前元素和前一个元素的和为负
sum -= nums[i]; // 取反并累加
k = 0; // 取反次数用完
}
}
}
}
// 如果遍历完数组后,还有剩余的取反次数(k 是奇数)
if (k & 1) {
int Minabs = fabs(nums[0]); // 初始化最小绝对值为第一个元素的绝对值
for (int i = 1; i < numsSize; i++) {
Minabs = fmin(fabs(nums[i]), Minabs); // 找到数组中的最小绝对值
}
sum -= 2 * Minabs; // 减去两倍的最小绝对值(相当于取反)
}
return sum; // 返回最终的和
}
代码二:(优化)
#include <stdlib.h>
#include <math.h>
// 比较函数,用于排序
int cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int largestSumAfterKNegations(int* nums, int numsSize, int k) {
// 将数组排序,方便优先处理最小的负数
qsort(nums, numsSize, sizeof(int), cmp);
int sum = 0;
int minAbs = INT_MAX; // 记录最小绝对值
// 遍历数组,优先对负数取反
for (int i = 0; i < numsSize; i++) {
if (nums[i] < 0 && k > 0) {
nums[i] = -nums[i]; // 取反
k--;
}
sum += nums[i]; // 累加和
minAbs = fmin(minAbs, nums[i]); // 更新最小绝对值
}
// 如果还有剩余的取反次数,对最小绝对值取反
if (k % 2 == 1) {
sum -= 2 * minAbs; // 减去两倍的最小绝对值(相当于取反)
}
return sum;
}
加油站
思路:贪心
- 问题理解
- 每个加油站的油量差为
gas[i] - cost[i]
,表示从第i
个加油站出发,到达下一个加油站后剩余的油量。 - 如果总油量差(所有加油站的油量差之和)小于 0,说明无论如何都无法完成一圈,直接返回
-1
。 - 如果总油量差大于等于 0,说明存在一个起点,可以从该起点出发完成一圈。
- 贪心算法
- 从第一个加油站开始,依次计算每个加油站的油量差,并累加到
cursum
中。 - 如果在某个加油站,
cursum
达到最小值,说明从该加油站出发,油量最“紧张”。因此,下一个加油站(即ans + 1
)可能是最佳起点。 - 遍历完成后,检查
cursum
是否小于 0。如果是,则无法完成一圈,返回-1
;否则返回最佳起点索引。
- 关键点
- 最小油量差的位置:
minsum
的位置决定了最佳起点。因为从minsum
的下一个位置出发,油量差会逐渐增加,更容易完成一圈。 - 总油量差的判断:如果总油量差小于 0,说明无论如何都无法完成一圈。
代码
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
// 初始化当前油量差和最小油量差
int cursum = gas[0] - cost[0];
int minsum = gas[0] - cost[0];
// 初始化起点索引
int ans = 0;
// 遍历所有加油站
for (int i = 1; i < gasSize; i++) {
// 更新当前油量差
cursum = cursum + gas[i] - cost[i];
// 如果当前油量差小于最小油量差,更新最小油量差和起点索引
if (cursum < minsum) {
ans = i;
minsum = cursum;
}
}
// 如果总油量差小于0,说明无法完成一圈
if (cursum < 0) return -1;
// 返回起点索引,因为起点索引是minsum对应的下一个位置
return (ans + 1) % gasSize;
}
分发糖果
思路:贪心
- 第一次遍历(从左到右):
- 确保每个孩子比左边评分低的孩子多一个糖果。
- 这样保证了从左到右的规则。
- 第二次遍历(从右到左):
- 确保每个孩子比右边评分低的孩子多一个糖果。
- 同时,如果当前孩子的糖果数已经比右边孩子的糖果数加1多,则不需要更新。
- 这样保证了从右到左的规则。
- 最终结果:
- 将每个孩子的糖果数相加,得到总的糖果数。
代码
#include <stdio.h>
#include <stdlib.h>
int candy(int* ratings, int ratingsSize) {
// 创建一个数组 nums,用于存储每个孩子分到的糖果数
int nums[ratingsSize];
// 初始化每个孩子至少分到一个糖果
for (int i = 0; i < ratingsSize; i++) {
nums[i] = 1;
}
// 第一次遍历:从左到右
// 如果当前孩子的评分比左边孩子高,则当前孩子的糖果数比左边孩子多一个
for (int i = 1; i < ratingsSize; i++) {
if (ratings[i] > ratings[i - 1]) {
nums[i] = nums[i - 1] + 1;
}
}
// 第二次遍历:从右到左
// 如果当前孩子的评分比右边孩子高,则当前孩子的糖果数比右边孩子多一个
// 同时,如果当前孩子的糖果数已经比右边孩子的糖果数加1多,则不需要更新
for (int i = ratingsSize - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
if (nums[i] < nums[i + 1] + 1) {
nums[i] = nums[i + 1] + 1;
}
}
}
// 计算总的糖果数
int ans = 0;
for (int i = 0; i < ratingsSize; i++) {
ans += nums[i];
}
return ans;
}
柠檬水找零
思路:贪心+模拟
- 使用两个变量
fivenums
和tennums
分别记录当前拥有的 5和5和10 的数量。 - 遍历顾客支付的钞票:
- 如果是 $5,直接收下,
fivenums++
。 - 如果是 10,需要找零10,需要找零5,
fivenums--
,同时tennums++
。 - 如果是 20,优先找零20,优先找零10 + 5(因为5(因为10 更有用),如果没有 10,则找零10,则找零5 + 5+5+5。
- 如果是 $5,直接收下,
- 如果无法找零,返回
false
;否则遍历结束后返回true
。
代码
bool lemonadeChange(int* bills, int billsSize) {
int fivenums = 0; // 记录当前拥有的 $5 的数量
int tennums = 0; // 记录当前拥有的 $10 的数量
// 遍历每个顾客支付的钞票
for (int i = 0; i < billsSize; i++) {
if (bills[i] == 5) {
// 如果是 $5,直接收下,不需要找零
fivenums++;
} else if (bills[i] == 10) {
// 如果是 $10,需要找零 $5
if (fivenums > 0) {
fivenums--; // 给顾客 $5
tennums++; // 收下 $10
} else {
// 如果没有 $5,无法找零,返回 false
return false;
}
} else {
// 如果是 $20,优先找零 $10 + $5
if (tennums > 0 && fivenums > 0) {
tennums--; // 给顾客 $10
fivenums--; // 给顾客 $5
} else if (fivenums >= 3) {
// 如果没有 $10,尝试找零 $5 + $5 + $5
fivenums -= 3; // 给顾客 3 张 $5
} else {
// 如果无法找零,返回 false
return false;
}
}
}
// 如果所有顾客都能成功找零,返回 true
return true;
}
根据身高重建队列
思路:排序+插入
- 排序:首先按照身高
h
从高到低排序,如果身高相同,则按照k
从小到大排序。这样做的目的是为了确保在插入时,高个子的人先被处理,这样后续插入的矮个子不会影响已经插入的高个子的k
值。 - 插入:遍历排序后的数组,按照每个人的
k
值将其插入到结果队列的相应位置。由于高个子已经排好序,插入矮个子时不会影响高个子的k
值。
代码一:身高降序,k值升序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
// 定义结构体表示每个人的身高和 k 值
typedef struct {
int h;
int k;
} person;
// 比较函数,用于排序
int cmp(const void* a, const void* b) {
person *p1 = (person*)a;
person *p2 = (person*)b;
// 按身高从高到低排序,如果身高相同,则按 k 从小到大排序
if (p1->h == p2->h) {
return p1->k - p2->k;
}
return p2->h - p1->h;
}
int** reconstructQueue(int** people, int peopleSize, int* peopleColSize, int* returnSize, int** returnColumnSizes) {
// 设置返回数组的大小
*returnSize = peopleSize;
// 分配结果数组的内存
int **res = malloc(sizeof(int*) * peopleSize);
// 分配列大小数组的内存
*returnColumnSizes = malloc(sizeof(int) * peopleSize);
// 将列大小数组的值全部设置为 2
for (int i = 0; i < peopleSize; i++) {
(*returnColumnSizes)[i] = 2;
}
// 将输入转换为 person 结构体数组
person p[peopleSize];
for (int i = 0; i < peopleSize; i++) {
p[i].h = people[i][0];
p[i].k = people[i][1];
}
// 对 person 数组进行排序
qsort(p, peopleSize, sizeof(person), cmp);
// 插入排序
for (int i = 0; i < peopleSize; i++) {
int pos = p[i].k; // 需要插入的位置
// 将 pos 之后的元素向后移动
for (int j = i; j > pos; j--) {
res[j] = res[j - 1];
}
// 插入当前元素
res[pos] = malloc(sizeof(int) * 2);
res[pos][0] = p[i].h;
res[pos][1] = p[i].k;
}
return res;
}
还有一种思路是将身高由小到大排序。
每次处理当前队列中身高最小的,再根据k值,在其前面留下k个空位,这样可以使后面处理身高比其大的人时,满足刚好有k个大于等于他的人在前面。
代码二:身高升序,k值升序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 比较函数,用于排序
int cmp(const void* _a, const void* _b) {
int *a = *(int**)_a, *b = *(int**)_b;
// 如果身高相同,则按 k 值从大到小排序;否则按身高从小到大排序
return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];
}
int** reconstructQueue(int** people, int peopleSize, int* peopleColSize, int* returnSize, int** returnColumnSizes) {
// 对 people 数组进行排序
qsort(people, peopleSize, sizeof(int*), cmp);
// 分配结果数组的内存
int** ans = malloc(sizeof(int*) * peopleSize);
// 设置返回数组的大小
*returnSize = peopleSize;
// 分配列大小数组的内存
*returnColumnSizes = malloc(sizeof(int) * peopleSize);
// 初始化列大小数组为 0
memset(*returnColumnSizes, 0, sizeof(int) * peopleSize);
// 遍历排序后的 people 数组
for (int i = 0; i < peopleSize; ++i) {
// 计算当前元素需要插入的位置
int spaces = people[i][1] + 1;
// 在结果数组中找到合适的位置
for (int j = 0; j < peopleSize; ++j) {
// 如果当前位置未被占用
if ((*returnColumnSizes)[j] == 0) {
spaces--;
// 如果 spaces 减到 0,说明找到了插入位置
if (!spaces) {
// 标记当前位置已被占用
(*returnColumnSizes)[j] = 2;
// 分配内存并插入当前元素
ans[j] = malloc(sizeof(int) * 2);
ans[j][0] = people[i][0], ans[j][1] = people[i][1];
break;
}
}
}
}
// 返回结果数组
return ans;
}
用最少数量的箭引爆气球
思路:排序+贪心
贪心策略:
- 按气球的结束位置(或者起始位置)排序。
- 从第一个气球开始,用一支箭射爆所有与当前气球区间重叠的气球。
- 如果遇到一个气球的起始位置 > 当前箭的射击区间的结束位置,说明需要新增一支箭
代码一:按结束位置排序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
// 比较函数,用于按气球区间的结束位置由小到大排序
int cmp(const void* a, const void* b) {
int* interval1 = *(int**)a;
int* interval2 = *(int**)b;
if(interval1[1] <interval2[1])return -1;
else return 1;
}
int findMinArrowShots(int** points, int pointsSize, int* pointsColSize) {
if (pointsSize == 0) return 0;
// 按气球区间的结束位置排序
qsort(points, pointsSize, sizeof(int*), cmp);
// 初始化箭的数量
int cnt = 1;
// 当前箭的射击区间
int end = points[0][1];
// 遍历所有气球区间
for (int i = 1; i < pointsSize; i++) {
// 如果当前气球的起始位置 > 当前箭的射击区间的结束位置
if (points[i][0] > end) {
// 需要新增一支箭
cnt++;
// 更新当前箭的射击区间
end = points[i][1];
}
}
return cnt;
}
代码二:按结束位置排序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
// 比较函数,用于按气球区间的起始位置由小到大排序
int cmp(const void* a, const void* b) {
int* interval1 = *(int**)a;
int* interval2 = *(int**)b;
if(interval1[0] <interval2[0])return -1;
else return 1;
}
int findMinArrowShots(int** points, int pointsSize, int* pointsColSize) {
if (pointsSize == 0) return 0;
// 按气球区间的结束位置排序
qsort(points, pointsSize, sizeof(int*), cmp);
// 初始化箭的数量
int cnt = 1;
// 当前箭的射击区间
int end = points[0][1];
// 遍历所有气球区间
for (int i = 1; i < pointsSize; i++) {
// 如果当前气球的起始位置 > 当前箭的射击区间的结束位置
if (points[i][0] > end) {
// 需要新增一支箭
cnt++;
// 更新当前箭的射击区间
end = points[i][1];
}
else{
end=fmin(end,points[i][1]);//更新(缩小)射击区间
}
}
return cnt;
}
无重叠区间
思路:贪心
刚读完题感觉和上一题(452. 用最少数量的箭引爆气球)很像,在上一题的代码基础上稍作修改就通过了。
- 问题描述:
- 给定一组区间,找到需要移除的最小区间数量,使得剩余的区间互不重叠。
- 贪心策略:
- 按区间的起始位置排序。
- 遍历区间,如果当前区间与前一个区间重叠,则移除结束位置较大的区间(保留结束位置较小的区间,以减少与后续区间重叠的可能性)。
- 关键点:
- 按起始位置排序后,遍历时只需比较当前区间与前一个区间的结束位置。
- 如果当前区间与前一个区间不重叠,则保留当前区间。
- 如果重叠,则移除结束位置较大的区间,并更新结束位置为较小的值。
代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
// 比较函数,用于按区间的起始位置从小到大排序
int cmp(const void* a, const void* b) {
int* interval1 = *(int**)a;
int* interval2 = *(int**)b;
// 按起始位置从小到大排序
if (interval1[0] < interval2[0]) return -1;
else return 1;
}
int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize) {
// 如果没有区间,直接返回 0
if (intervalsSize == 0) return 0;
// 按区间的起始位置排序
qsort(intervals, intervalsSize, sizeof(int*), cmp);
// 初始化计数器,记录保留的区间数量
int cnt = 1;
// 当前保留区间的结束位置
int end = intervals[0][1];
// 遍历所有区间
for (int i = 1; i < intervalsSize; i++) {
// 如果当前区间的起始位置 >= 当前保留区间的结束位置
if (intervals[i][0] >= end) {
// 保留当前区间
cnt++;
// 更新当前保留区间的结束位置
end = intervals[i][1];
} else {
// 如果重叠,保留结束位置较小的区间
end = fmin(end, intervals[i][1]);
}
}
// 需要移除的区间数量 = 总区间数量 - 保留的区间数量
return intervalsSize - cnt;
}
划分字母区间
思路:贪心
- 问题描述:
- 给定一个字符串
s
,要求将字符串划分为若干片段,使得每个字母最多出现在一个片段中。 - 返回每个片段的长度。
- 给定一个字符串
- 关键点:
- 每个字母的最后出现位置决定了片段的边界。
- 遍历字符串时,动态更新当前片段的结束位置。
- 算法步骤:
- 首先记录每个字母的最后出现位置。
- 遍历字符串,更新当前片段的结束位置。
- 当遍历到当前片段的结束位置时,记录片段长度,并开始下一个片段。
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int* partitionLabels(char* s, int* returnSize) {
// 分配结果数组的内存
int* ans = malloc(sizeof(int) * 500);
// 初始化返回的片段数量为 0
*returnSize = 0;
// 字符串的长度
int n = strlen(s);
// 记录每个字母的最后出现位置
int last[26] = {0};
// 遍历字符串,记录每个字母的最后出现位置
for (int i = 0; i < n; i++) {
last[s[i] - 'a'] = i;
}
// 初始化当前片段的起始位置和结束位置
int start = 0, end = 0;
// 遍历字符串
for (int i = 0; i < n; i++) {
// 更新当前片段的结束位置
end = fmax(end, last[s[i] - 'a']);
// 如果遍历到当前片段的结束位置
if (i == end) {
// 记录当前片段的长度
ans[(*returnSize)++] = end - start + 1;
// 更新下一个片段的起始位置
start = end + 1;
}
}
// 返回结果数组
return ans;
}
合并区间
思路:贪心
- 按区间的起始位置排序。
- 遍历区间,如果当前区间与前一个区间重叠,则合并它们;否则,将前一个区间加入结果列表。
个人感觉比较简单~~
代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
// 比较函数,用于按区间的起始位置从小到大排序
int cmp(const void* a, const void* b) {
int* intervala = *(int**)a;
int* intervalb = *(int**)b;
// 按起始位置从小到大排序
if (intervala[0] - intervalb[0] < 0) return -1;
return 1;
}
int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes) {
// 按区间的起始位置排序
qsort(intervals, intervalsSize, sizeof(int*), cmp);
// 分配结果数组的内存
int** res = malloc(sizeof(int*) * intervalsSize);
// 初始化返回的区间数量为 0
*returnSize = 0;
// 初始化当前区间的起始和结束位置
int start = intervals[0][0];
int end = intervals[0][1];
// 遍历所有区间
for (int i = 1; i < intervalsSize; i++) {
// 如果当前区间与前一个区间重叠
if (intervals[i][0] <= end) {
// 合并区间,更新结束位置
end = fmax(end, intervals[i][1]);
} else {
// 如果不重叠,将前一个区间加入结果列表
res[*returnSize] = malloc(sizeof(int) * 2);
res[*returnSize][0] = start;
res[*returnSize][1] = end;
(*returnSize)++;
// 更新当前区间的起始和结束位置
start = intervals[i][0];
end = intervals[i][1];
}
}
// 将最后一个区间加入结果列表
res[*returnSize] = malloc(sizeof(int) * 2);
res[*returnSize][0] = start;
res[*returnSize][1] = end;
(*returnSize)++;
// 分配列大小数组的内存
*returnColumnSizes = malloc(sizeof(int) * (*returnSize));
// 设置每个区间的列大小为 2
for (int i = 0; i < *returnSize; i++) {
(*returnColumnSizes)[i] = 2;
}
// 返回结果数组
return res;
}
单调递增的数字
思路:贪心
- 问题描述:
- 给定一个整数
n
,找到小于或等于n
的最大单调递增数字。 - 单调递增数字是指从左到右每一位数字都不大于下一位数字。
- 给定一个整数
- 关键点:
- 从右到左遍历数字,找到第一个不满足单调递增的位置。
- 将该位置减 1,并将其后的所有位置设置为 9。
- 算法步骤:
- 将数字转换为数组形式,方便逐位操作。
- 从右到左遍历数组,找到第一个不满足单调递增的位置。
- 将该位置减 1,并将其后的所有位置设置为 9。
- 将数组转换回整数形式。
注:
-
如果遇到
332
这样的需要将第一个3减一,后面的数都变为9。因此当遇到不满足单调递增的位置时,需要回去找到第一个等于该数的位置(相当于找到332
中第一个3的位置),将其减一,后面置为9. -
下面将数字转换为数组的函数都是倒序的,例如
12345
对应的数组为54321
;
代码
#include <stdio.h>
#include <stdlib.h>
// 将整数转换为数组形式
void int2vec(int x, int* res, int* returnSize) {
int cnt = 0;
// 从低位到高位依次提取数字
while (x) {
res[cnt++] = x % 10;
x /= 10;
}
// 返回数组的长度
*returnSize = cnt;
}
// 将数组形式的数字转换为整数
int vec2int(int* num, int numSize) {
int ans = 0;
// 从高位到低位依次计算整数
for (int i = numSize - 1; i >= 0; i--) {
ans = ans * 10 + num[i];
}
return ans;
}
int monotoneIncreasingDigits(int n) {
// 如果 n 是一位数,直接返回
if (n < 10) return n;
// 分配数组内存,用于存储数字的每一位
int* num = malloc(sizeof(int) * 10);
int len = 0;
// 将 n 转换为数组形式
int2vec(n, num, &len);
// 从右到左遍历数组
for (int i = len - 1; i >= 1; i--) {
// 如果当前位大于前一位
if (num[i] > num[i - 1]) {
// 找到连续相等的高位
int j = i + 1;
while (j < len && num[j] == num[i]) j++;
// 将高位减 1
num[j - 1]--;
// 将低位全部设置为 9
for (int k = 0; k < j - 1; k++) num[k] = 9;
}
}
// 将数组转换回整数形式
return vec2int(num, len);
}
监控二叉树
思路:二叉树遍历
-
贪心算法:
- 从叶子节点向上遍历,尽量在父节点安装摄像头,以覆盖更多的节点。
- 使用后序遍历(左右根)来遍历二叉树。
-
状态定义:
- 每个节点有三种状态:
0
:未被监控。1
:被监控,但没有安装摄像头。2
:安装了摄像头。
- 每个节点有三种状态:
-
状态转移:
- 如果左孩子或右孩子中有一个未被监控(状态
0
),则当前节点必须安装摄像头(状态2
)。 - 如果左孩子或右孩子中有一个安装了摄像头(状态
2
),则当前节点被监控(状态1
)。 - 否则,当前节点未被监控(状态
0
)。
- 如果左孩子或右孩子中有一个未被监控(状态
-
边界条件:
- 如果根节点未被监控,则需要在其上安装摄像头。(易漏)
代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
// 全局变量,记录摄像头的数量
int result = 0;
//0:无覆盖
//1:有覆盖
//2:有摄像头
// 后序遍历函数,返回节点的状态
int traversal(struct TreeNode* root) {
// 空节点默认是被监控的(状态 1)
if (root == NULL) return 1;
// 递归遍历左子树和右子树
int left = traversal(root->left);
int right = traversal(root->right);
// 如果左孩子或右孩子中有一个未被监控,当前节点必须安装摄像头
if (left == 0 || right == 0) {
result++;
return 2;
}
// 如果左孩子或右孩子中有一个安装了摄像头,当前节点被监控
if (left == 2 || right == 2) {
return 1;
}
// 否则,当前节点未被监控
return 0;
}
// 主函数,返回需要安装的摄像头数量
int minCameraCover(struct TreeNode* root) {
// 初始化摄像头数量
result = 0;
// 如果根节点未被监控,需要安装摄像头
if (traversal(root) == 0) {
result++;
}
return result;
}