算法学习记录~2023.X.XX~章节DayX~题目号.题目标题 & 题目号.题目标题
134. 加油站
题目链接
思路1:暴力解法
模拟从每一个加油站出发,然后再分别模拟一圈。
如果中途出现油不足以去下一站则退出,将下一个加油站设为出发地。如果都没问题则说明符合结果,返回这个出发地。
很明显就是O(n^2)的。
for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历
自己写的这个代码在leetcode上超时了
代码
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int result = -1; //初始化结果
for (int i = 0; i < gas.size(); i++){ //以每一个加油站为起点
int cur = i; //cur记录当前加油站开始的路径走到了哪里,初始化为起点
int count = 0; //记录每一圈走过了多少站
int sum = 0; //当前汽油总量
while (count != gas.size()){ //判断环形终点
sum += gas[cur]; //走到某一个点的汽油总量为以前的加上当前加油站的
if (sum < cost[cur]){ //只要其中一站无法走到下一步,则本加油站为起点不满足要求,继续考虑下一个加油站为起点的环形
result = -2; //此条环路走不通,设为-2来和初始化的-1做区分
break;
}
else{
sum -= cost[cur]; //减去到下一站烧的油
count ++; //本站能去下一站就加上
cur ++; //向前走了一步
if (cur == gas.size()) //到头了就重置为最开头
cur = 0;
}
}
result = (result == -2) ? -1 : i; //如果此条路走不通就返回-1,否则返回出发加油站
if (result == i) //如果找到了结果直接退出
break;
}
return result;
}
};
思路2:从全局进行考虑
这个思路想了非常久,而且其实现在也无法确定是否是正确的。先把carl哥的说法贴下来
“
直接从全局进行贪心选择,情况如下:
情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
”
前两种情况很好理解,对于情况3,目前的想法是这样的。
为什么要从后向前呢?如果想到达这个点,那么从出发点到这个点的累积油量一定要大于等于0。那么从后往前遍历其实可以理解为倒着开,从i点开到0的和是min,是负的,那么如果想要变正就还得往前开,也就是从最后开始再往前,直到这个min也就是累积油量大于等于0,这样才能跑满一圈。由于它是累积油量的min,因此如果有唯一解那一定是从这个缺口开始找到的。
但感觉也不是很对劲,不知道这么理解对不对,非常蒙
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int min = INT_MAX; // 从起点出发,油箱里的油量最小值
for (int i = 0; i < gas.size(); i++) {
int rest = gas[i] - cost[i];
curSum += rest;
if (curSum < min) {
min = curSum;
}
}
if (curSum < 0) return -1; // 情况1
if (min >= 0) return 0; // 情况2
// 情况3
for (int i = gas.size() - 1; i >= 0; i--) {
int rest = gas[i] - cost[i];
min += rest;
if (min >= 0) {
return i;
}
}
return -1;
}
};
思路3:贪心算法(感觉也不是很算?)
如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。
每个加油站的剩余量rest[i]为gas[i] - cost[i]。
i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。
(这里是因为i处能够用掉[0,i]累积下来的所有油,且由于之前累积的油必然大于等于0,所以i之前的都不可以)
那么为什么一旦[0,i] 区间和为负数,起始位置就可以是i+1呢,i+1后面就不会出现更大的负数?
如果出现更大的负数,就是更新i,那么起始位置又变成新的i+1了。
那有没有可能 [0,i] 区间选某一个作为起点,累加到 i 这里 curSum是大于零呢?如图所示
如果 curSum<0 说明 区间和1 + 区间和2 < 0, 那么 假设从上图中的位置开始计数curSum不会小于0的话,就是 区间和2>0。
区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择起始位置了。
(但总觉得这个解释和上面会出现更大的负数就更新i的解释冲突?)
那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
(其实还是不能理解这个思路,总觉得非常奇怪)
时间复杂度:O(n)
空间复杂度:O(1)
代码
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int totalSum = 0;
int start = 0;
for (int i = 0; i < gas.size(); i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if (curSum < 0) { // 当前累加rest[i]和 curSum一旦小于0
start = i + 1; // 起始位置更新为i+1
curSum = 0; // curSum从0开始
}
}
if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
return start;
}
};
总结
首先是按照思路1的时候,找代码错误找了一个多小时,最后发现是“?:”的语法记出问题用错了…因为一个非常低级的错误浪费了这么久真的很崩溃,尤其是发现这样还超时…
思路2和思路3理解起来非常的吃力,这道题可能花了三小时才稍微理解了,而且感觉理解还是存在问题。这个情况来看很没信心之后遇到时候自己还能想起来。具体对于这两种思路的考虑和困难都写在上面对应思路的部分了。
不知道是不是要考虑放弃下这道题,贪心算法真的就是一个萝卜一个坑没有任何题目之间能举一反三,自信心打击器了属于是。
这道题目前可以标记为不会。
135. 分发糖果
题目链接
思路1:(自己想的)暴力枚举所有情况
将评分排序,随后从最低评分开始依次给糖果。
用一个copy数组保存原来的数组,原数组sort排序,candy数组记录分配情况,used数组判断元素是否被处理。
暴力枚举所有情况,主要有以下几种:
- 在中间 且 左右都有数了
- 左右评分和中间相同 --> 设为1
- 左相同右边低 --> 比右边多1
- 左边低右相同 --> 比左边多1
- 左右都低 --> 比左右两边最大的多1 - 在中间 且 左无右有
- 和右边相同 --> 设为1
- 大于右边 --> 比右边多1 - 在中间 且 左有右无
- 和左边相同 --> 设为1
- 大于左边 --> 比左边多1 - 在中间 且 左右都没数
- 设为1 - 左边界
- 右边没数 --> 设为1
- 右边有数且评分小 --> 比右边多1
- 右边有数且评分相同 --> 设为1 - 右边界
- 左边没数 --> 设为1
- 左边有数且评分小 --> 比左边多1
- 左边有数且评分相同 --> 设为1
最后结果应该是没啥问题,但是太暴力了,n*n,超时
代码
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> copy(ratings); //复制原数组
int result = 0;
sort(ratings.begin(), ratings.end()); //原数组进行排序
vector<int> candy(copy.size()); //具体糖果数量
vector<int> used(ratings.size(), 0); //元素处理情况,0为为处理,1为已经处理
if (ratings.size() == 1)
return 1;
for (int i = 0; i < ratings.size(); i++){ //从评分最低的开始
for (int j = 0; j < copy.size(); j++){
if (copy[j] == ratings[i] && used[j] == 0){ //找到对应元素并且判断该元素还没有处理过
if (j != 0 && j != copy.size() - 1 && used [j - 1] == 1 && used[j + 1] == 1){ //在中间 且 左右都有数了
if (copy[j] == copy[j - 1] && copy[j] == copy[j + 1]) //左中右评分相同,则中间的为1
candy[j] = 1;
else if (copy[j] == copy[j - 1] && copy[j] > copy[j + 1])
candy[j] = candy[j + 1] + 1;
else if (copy[j] > copy[j - 1] && copy[j] == copy[j + 1])
candy[j] = candy[j - 1] + 1;
else
candy[j] = max(candy[j - 1], candy[j + 1]) + 1;
}
else if (j != 0 && j != copy.size() - 1 && used [j - 1] == 0 && used[j + 1] == 1){ //在中间 且 左无右有
if (copy[j] == copy[j + 1]) //和右边相同,设为1
candy[j] = 1;
else{ //比右边大,则比右边多1
candy[j] = candy[j + 1] + 1;
}
}
else if (j != 0 && j != copy.size() - 1 && used [j - 1] == 1 && used[j + 1] == 0){ //在中间 且 左有右无
if (copy[j] == copy[j - 1]) //和左边相同,设为1
candy[j] = 1;
else{ //比左边大,则比左边多1
candy[j] = candy[j - 1] + 1;
}
}
else if (j != 0 && j != copy.size() - 1 && used [j - 1] == 0 && used[j + 1] == 0) { //在中间 且 左右都没数
candy[j] = 1;
}
else if (j == 0){ //左边界
if (used[j + 1] == 0){ //右边没数,设为1
candy[j] = 1;
}
else if(used[j + 1] == 1 && copy[j] > copy[j + 1]){ //右边有数且评分小
candy[j] = candy[j + 1] + 1;
}
else{ //右边有数且评分相等
candy[j] = 1;
}
}
else if (j == copy.size() - 1){ //右边界
if (used[j - 1] == 0){ //左边没数,设为1
candy[j] = 1;
}
else if(used[j - 1] == 1 && copy[j] > copy[j - 1]){ //左边有数且评分小
candy[j] = candy[j - 1] + 1;
}
else{ //左边有数且评分相等
candy[j] = 1;
}
}
used[j] = 1; //该元素已经处理过,标为1
break;
}
}
}
for (int i = 0; i < candy.size(); i++){ //统计总数
result += candy[i];
}
return result;
}
};
思路2:贪心
分两次贪心。
这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。
先确定右边评分大于左边的情况(也就是从前向后遍历)
此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
局部最优可以推出全局最优。
再进行反向操作,需要注意的是反方向时也要从后向前遍历,因为由于时左边大于右边,所以前面的元素会受后边的影响。
同时当第一次贪心和第二次贪心结果冲突时,应取更大的值,这样才能同时满足两个要求
代码
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candy(ratings.size(), 1);
//从前往后,左 < 右
for (int i = 0; i < ratings.size() - 1; i++){
if (ratings[i] < ratings[i + 1]){
candy[i + 1] = candy[i] + 1;
}
}
//从后往前,左 > 右
for (int i = ratings.size() - 1; i >= 1; i--){
if (ratings[i - 1] > ratings[i]){
candy[i - 1] = max(candy[i] + 1, candy[i - 1]); //注意需要取两次遍历最大值
}
}
int result = 0;
for (int i = 0; i < ratings.size(); i++)
result += candy[i];
return result;
}
};
总结
860.柠檬水找零
题目链接
思路
从头到尾遍历挨个处理和记录每种货币数量。
总共只有三种情况:
- 账单是5 --> count5加1
- 账单是10 --> count5减1count10加1
- 账单是20 --> 一张10加一张5,或者三张5
对每种情况进行代码编写,处理当前账单,在每层最后判断是否有负值,如果有则说明此次找不开了
代码
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int count5 = 0;
int count10 = 0;
int count20 = 0;
for (int i = 0; i < bills.size(); i++){
if (bills[i] == 5){
count5++;
}
else if (bills[i] == 10){
count5--;
count10++;
}
else{ //20的
if(count10 >= 1){ //有10块的就组合找钱
count10--;
count5--;
count20++;
}
else{ //否则找3张5块
count5 -= 3;
}
}
if (count5 < 0 || count10 < 0) //操作完发现数值为负则当前无法找零
return false;
}
return true;
}
};
总结
406.根据身高重建队列
题目链接
思路
和 135. 分发糖果 类似,先确定一个维度,再处理另一个维度。
首先一定要先考虑 h 维度,因为先考虑 k ,会发现并不太能找打到一个合适的规则去排序,而且也无法继续在 h 维度上进一步处理。
因此要先处理身高 h 。
处理身高 h ,一定要高的在前面,因为这样可以防止后排入人员影响先排入人员的位置。对于相同身高,则 k 越大越在后面。
接着处理 k ,由于从前往后身高递减,因此从前往后处理时,对每一个要被排入的新人员,已经处于排好队列的人身高一定 >= 当前身高,因此新排入的人员的位置索引就是这个人的 k ,因为新排入一定是最矮的,因此一定不会影响之前已经排好人员的位置。
代码1:使用vector数组
class Solution {
public:
static bool cmp (const vector<int>& a, const vector<int>& b){
if (a[0] == b[0]) //身高相同则 k 小的在前,k 大的在后
return a[1] < b[1];
return a[0] > b[0]; //身高大的在前
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), cmp); //第一次贪心排序
vector<vector<int>> que; //重新排列后的队列
for (int i = 0; i < people.size(); i++){
int position = people[i][1]; //取k值,这就是针对前面已排好que的下一元素的对应位置
que.insert(que.begin() + position, people[i]); //把身高h值插入到对应位置
}
return que;
}
};
代码2:使用list链表优化性能
class Solution {
public:
static bool cmp (const vector<int>& a, const vector<int>& b){
if (a[0] == b[0]) //身高相同则 k 小的在前,k 大的在后
return a[1] < b[1];
return a[0] > b[0]; //身高大的在前
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), cmp); //第一次贪心排序
list<vector<int>> que; //list底层是链表实现,插入效率比vector高的多
for (int i = 0; i < people.size(); i++){
int position = people[i][1]; //取k值,这就是针对前面已排好que的下一元素的对应位置
std::list<vector<int>>::iterator it = que.begin();
while (position--){ //寻找插入位置
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end()); //转回对应格式
}
};
总结
一开始并没想到和 135. 分发糖果 类似,没意识到其实可以同样抽象为两个维度分别处理的问题。
对于这种类型,原则是,遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。
同时学习下 list 的索引之类的各种基本语法。