文章目录
- 刷题笔记(2024.6.5-6.29)
- 575.分糖果(set)
- 1103.分糖果II(模拟)
- 523.连续的子数组和(前缀和、哈希表)
- 2938.区分黑球与白球(贪心)
- 1685.有序数组中差绝对值之和(前缀和、距离和)
- 2602.使数组元素全部相等的最少操作次数(前缀和、距离和)
- 437.路径总和III(前缀和、树的遍历)
- 881.救生艇(贪心、双指针)
- 136.只出现一次的数字(位运算)
- 419.甲板上的战舰(规律、模拟)
- 2595.奇偶位数(位运算)
- 231.2的幂(位运算)
- 342.4的幂(位运算)
- 1009.十进制整数的反码(位运算)---476
- 2786.访问数组中的位置使分数最大(动态规划、位运算)
- 191.位1的个数(位运算)
- 338.比特位计数(位运算)
- ---未解决---2288.价格减免(字符串、模拟)
- 316.去除重复字母(哈希表、字符串、单调栈)---1081
- 2748.美丽下标对的数目(模拟、哈希表)
- LCP61.气温变化趋势(数组、模拟)
- 2663.字典序最小的美丽字符串(数组、贪心)
- 503.下一个更大元素II(单调栈、循环数组)
刷题笔记(2024.6.5-6.29)
575.分糖果(set)
unordered_map:
class Solution {
public:
int distributeCandies(vector<int>& candyType) {
unordered_map<int, int> hash;
int n = candyType.size();
int type = 0;
for(int i = 0; i < n; i++)
{
if(hash.count(candyType[i]) == 0)
{
ha sh[candyType[i]]++;
type++;
}
}
return min(n/2, type);
}
};
unordered_set:
class Solution {
public:
int distributeCandies(vector<int>& candyType) {
unordered_set<int> hash(candyType.begin(), candyType.end());
return min(hash.size(), candyType.size()/2);
}
};
map是自己写的,set是灵神写的,真神啊没话说,我的map其实就是模拟的set的初始化,思路是一样的,但是不同的数据结构用起来展现的简洁程度果然还是不一样。
1103.分糖果II(模拟)
class Solution {
public:
vector<int> distributeCandies(int candies, int num_people) {
vector<int> ans(num_people, 0);
for(int cnt = 1; candies > 0; cnt++)
{
ans[(cnt-1)%num_people] += min(cnt, candies);
candies -= cnt;
}
return ans;
}
};
简单题模拟,思考的时候想复杂了,想用前缀和或者差分数组来写的,主要是特点太像了,但是想不出怎么运用到里面去,最后还是用模拟来写,也还好只用一个for循环。这一题的精华语句在于ans[(cnt-1)%num_people] += min(cnt, candies);
简洁且明了,%num_people是为了数组要在0-num_people-1的范围内,确保数组不越界的写法可以学一学,然后+= min(cnt,candies)替代了if和else言简意赅值得学习
523.连续的子数组和(前缀和、哈希表)
class Solution {
public:
bool checkSubarraySum(vector<int>& nums, int k) {
if(nums.size() < 2) return false;
unordered_map<int, int> hash;//前缀和%k到下标的映射
long long sum = 0;//防止int溢出
hash[0] = -1;//必须要初始化 表示前缀和%k=0的下标为-1
for(int i = 0; i < nums.size(); i++) {
//sum = (sum+nums[i])%k;
sum += nums[i];
long long t = (sum%k+k)%k;
if(hash.count(t)) {
int index = hash[t];
if(i-index >= 2) return true;//一定不能写成i-index+1 >= 2
}
else hash[t] = i;//一定要有else 否则样例[5,0,0,0],k=3过不去 因为会一直更新hash[t]的下标
}
return false;
}
};
这一题和974.和可被k整除的子数组很像,都是用到的前缀和+哈希表,最大的不同就在于它对子数组的长度进行了限定,从而导致了我们哈希表使用的不同。写完这一题后我对哈希表有了新的认识,哈希表和动态规划数组dp有相似的运用。当运用到哈希表的时候应该从这几个方面入手:1、清楚哈希表的定义(从…到…的映射),523是前缀和%k到下标的映射而974是前缀和%k到个数的映射,这一点可以从题目的答案入手2、哈希表的初始化,我觉得哈希表的初始化最好要单独拎出来比较好,这样比较好理解,有时候某的题解为了代码简洁就将初始化和赋值一同放进for循环里头。
这一题呢哈希表定义成前缀和%k到下标的映射,最大的不同就在初始化的部分,hash[0] = -1,有点类似于添加了一个哨兵的感觉,目的在于让第一个sum%k==0时是成立了,eg[1,3],k=2,其它要注意的点就在注释里啦~
2938.区分黑球与白球(贪心)
class Solution {
public:
long long minimumSteps(string s) {
long long ans = 0, cnt = 0;
for(char c : s) {
if(c == '1') cnt++;
else ans += cnt;
}
return ans;
}
};
这里用到的是贪心的算法,唉感觉我还是想复杂了,我与之前做过的一个字符串转换大小问题给串联在一起了,结果推不出表达式,那个字符串转换大小问题是左边小写右边大写并不是交换的意思,而是直接修改当前字符,与这一题的交换还是差一点意思,所以这一题放弃了前后缀和的思路。然后我盯着交换两字,我就联想到编辑距离问题,发现不是两个字符串的意思,就是不知道目标字符串,所以也不能用编辑距离的思路去写。最后这个贪心思路也是巧妙啊,果然贪心问题考察的是思维,代码还是很简单易理解的。遍历字符串遇到1就cnt++遇到0表明这个0左边的1都要交换到这个0的右边,所以ans+=cnt
1685.有序数组中差绝对值之和(前缀和、距离和)
class Solution {
public:
vector<int> getSumAbsoluteDifferences(vector<int>& nums) {
int n = nums.size();
vector<int> prefix(n+1);
vector<int> ans;
for(int i = 1; i <= n; i++) prefix[i] = prefix[i-1]+nums[i-1];
for(int i = 0; i < n; i++) {
int left = i*nums[i]-prefix[i];
int right = prefix[n]-prefix[i+1]-(n-1-i)*nums[i];
ans.push_back(left+right);
}
return ans;
}
};
首先分析这一题,nums的长度达到了1e5,那么这一题用两个for循环一定是会超时的,所以我们需要想另外的方法来解决这个问题,这里用到的是前缀和,与以往的题不同在于他要求当前数与其它数的差的绝对值之和。所以这里的前缀和还与下标关联了起来,也就是数的个数,它的技巧就是在于将一次次的模拟差求绝对值变成了乘法,由加法变成乘法使得时间复杂度大大降低了,所以这里的left表示i的左边所有数与nums[i]差的绝对值和,right表示i的右边所有数与nums[i]差的绝对值和,注意这里为什么没有abs?因为给的是一个有序数组,所以i*nums[i]
和prefix[n]-prefix[i+1]
一定是较大的那个数
2602.使数组元素全部相等的最少操作次数(前缀和、距离和)
class Solution {
public:
vector<long long> minOperations(vector<int>& nums, vector<int>& queries) {
sort(nums.begin(), nums.end());
int n = nums.size();
vector<long long> prefix(n+1);
vector<long long> ans;
for(int i = 1; i <= n; i++) prefix[i] = prefix[i-1]+nums[i-1];
for(long long q : queries) {
int interval = lower_bound(nums.begin(), nums.end(), q)-nums.begin();
long long left = q*interval-prefix[interval];
long long right = prefix[n]-prefix[interval]-q*(n-interval);
ans.push_back(left+right);
}
return ans;
}
};
首先将数组排序,可以知道这一题的答案是操作次数,与数组是否有序是没有直接关系的。然后关键的地方就是在与把这个求次数的问题转换成求面积的问题,利用前缀和来优化遍历次数
437.路径总和III(前缀和、树的遍历)
class Solution {
public:
int pathSum(TreeNode* root, int targetSum) {
unordered_map<long long, int> hash;//前缀和到个数的映射
hash[0] = 1;
int ans = 0;
function<void(TreeNode*, long long)> dfs = [&](TreeNode* root, long long sum) {
if(root == nullptr) return;
sum += root->val;
ans += hash[sum-targetSum];
hash[sum]++;
dfs(root->left, sum);
dfs(root->right, sum);
hash[sum]--;//保留现场
};
dfs(root, 0);
return ans;
}
};
这道题可以说含金量很高的题了,考察了前缀和、哈希表、树的遍历和回溯,前缀和与哈希表就不用多说了,和560和为k的子数组是同一个解法,不同就在于这里是对树进行操作,所以遍历数组和遍历树就需要区分一下了。由于树的遍历有回溯的步骤,所以在这里还需要有保留现场的操作,写的时候只考虑了sum要减去当前root的val值,没有考虑到要对hash进行更改,大致的思路还是懂的,只不过写的方式不同然后结果也不知道为什么不同。。。还有就是这里将sum写入了dfs的参数内,这样就省略了对sum的回溯操作,也就是sum-=root->val
不需要写。
881.救生艇(贪心、双指针)
class Solution {
public:
int numRescueBoats(vector<int>& people, int limit) {
sort(people.begin(), people.end());
int light = 0, heavy = people.size()-1, ans = 0;
while(light <= heavy) {
if(people[light]+people[heavy] <= limit) {
light++;
heavy--;
} else {
heavy--;
}
ans++;
}
return ans;
}
};
首先将数组排序,以体重小的视角,如果加上最大的一个后小于等于limit那么就将它们放入同一个船中(已经能和最大的一起装在船上,那么和其它任何一个都行,最优答案就和这个最大的装一起),ans记录答案,否则(和大于limit,那么这条船只装heavy)。
136.只出现一次的数字(位运算)
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0;
for(int x : nums) {
ans ^= x;
}
return ans;
}
};
题中明确规定只能用线性的时间复杂度,异或运算的特点 a ^ a = 0, a ^ 0 = a。以[4,1,2,1,2]为例,
41212 = 4(11)(22) = 400 = 4,所以遍历一遍nums之后得到的ans值就是只出现一次的数字,当然这个题解是有前提的,题干中明确说了只有一个元素出现一次,其它元素都出现了2次。
419.甲板上的战舰(规律、模拟)
class Solution {
public:
int countBattleships(vector<vector<char>>& board) {
int ans = 0;
for(int i = 0; i < board.size(); i++) {
for(int j = 0; j < board[i].size(); j++) {
if(board[i][j] == 'X' && (j == 0 || board[i][j-1] != 'X')
&& (i == 0 || board[i-1][j] != 'X')) {
ans++;
}
}
}
return ans;
}
};
一眼看到这种题想到的是深搜和广搜,但是写不出来,题解给出的思路是:遍历这一个二维数组,记录下遇到的战舰的头部,其余情况不考虑,最后得到的ans就是战舰数。i>0时board[i-1] [j]不是X且j > 0时board[i] [j-1]不是X,这时的board[i] [j]就是战舰头。
2595.奇偶位数(位运算)
class Solution {
public:
vector<int> evenOddBit(int n) {
vector<int> ans(2);
for(int i = 0; n != 0; i ^= 1, n >>= 1) {
ans[i] += n&1;
}
return ans;
}
};
ans[0]表示偶数个数,ans[1]表示奇数个数,如何做到让下标i反复的从0和1之间变换呢?这里用到的是异或运算如何取到n的二进制最后一位呢?这里是将n&1,将结果累加到当前的ans[i]中由于二进制只有0和1,题干又是计算1的个数,所以ans[i] += n&1就能一同取到1的个数了,这里注意一下:如果写成了ans[i] = ans[i] + n&1会导致运算结果不正确,原因是+的运算优先级要高于&的运算优先级,所以会先算ans[i]+ans[i]在算&,这样就不符合题目意思了,所以应该写成ans[i] += n&1或者ans[i] = ans[i] + (n&1)。n的值是不断变小的,所以n这里在不断地右移,也就是将末位剔除,达到n递减的效果,当n所有位数都移出去了,此时n就为0了,也就退出了循环。
231.2的幂(位运算)
解法一:
class Solution {
public:
bool isPowerOfTwo(int n) {
return n>0 && ((n&-n) == n);
}
};
解法二:
class Solution {
public:
bool isPowerOfTwo(int n) {
return n>0 && (n&(n-1)) == 0;
}
};
在这一题中(n&-n) == n
和(n&(n-1)) == 0
都是二进制数中的最低位1的技巧(最右边的),这一题的目的也就是在此,如果一个数要是2的幂的话,那么这个数的二进制表示法中只有一个1,所以我们的目的就是让让这个1来建立关系,这里提供的两个思路就是如此。为什么方法1中(n&-n) == n
有这种效果呢? 由于负数是按照补码规则在计算机中存储的 00100的补码就是11100了,两者相与如果还是n的话表明这是一个2的幂数,这里n的范围可以是负数,所以还需要排出负数的可能。
342.4的幂(位运算)
解法一:
class Solution {
public:
bool isPowerOfFour(int n) {
return n>0 && (n&(n-1)) == 0 && n%3 == 1;
}
};
解法二:
class Solution {
public:
bool isPowerOfFour(int n) {
return n > 0 && (n & (n - 1)) == 0 && (n & 0xaaaaaaaa) == 0;
}
};
首先如果一个数是4的幂,那么它一定是2的幂,所以前面的两个式子是判断它是2的幂,最后一个式子是通过4的幂数的特点来得出的结论,n%3 == 1
是因为4的幂数除以3的余数一定是1的特点。(n & 0xaaaaaaaa) == 0
是因为4的幂数的二进制表示1一定出现在偶数位。主要是要发现这种特点吧,前提一定是要多做题积累点经验
1009.十进制整数的反码(位运算)—476
class Solution {
public:
int bitwiseComplement(int n) {
long long a = 1;
if(n == 0) return 1;
while(true) {
if(a <= n) {
a <<= 1;
} else {
return a - n - 1;
}
}
}
};
这两题中的反码和补数是同一个概念,举一个例子8的二进制是1000,它的反码是0111,它们之间有这么一个等量关系就是10000=1000+0111+0001,这一段代码就是模拟的这一过程
2786.访问数组中的位置使分数最大(动态规划、位运算)
记忆化搜索:
class Solution {
public:
long long maxScore(vector<int>& nums, int x) {
int n = nums.size();
//初始化值为-1表示没有计算过
vector<vector<long long>> memo(n, vector<long long>(2, -1));
auto dfs = [&](auto&& dfs, int i, int j) -> long long {
if(i == n) return 0;
long long& res = memo[i][j];//一定要是引用
if(res != -1) return res;
if(nums[i]%2 != j) return res = dfs(dfs, i+1, j);
return res = max(dfs(dfs, i+1, j), dfs(dfs, i+1, j^1)-x)+nums[i];
};
return dfs(dfs, 0, nums[0]%2);
}
};
与以往不同的是:dfs模板参数中发生变化,以前function<>的模板放在这里会超时,所以以后还是写这种模板吧;这里的递归方向是从前往后递归,递归开始的下标是0,结束位置是i==n,以前都是从后往前递归这样突然遇到了一个从前往后递归的题确实有一点不能动手。位运算的巧妙:基于奇偶性质的转移使用j^1的方法实现j的奇偶变换,这里不需要考虑j进行位运算后的值是什么,这里只需要关心它的奇偶性。
1:1转化成递推:
class Solution {
public:
long long maxScore(vector<int>& nums, int x) {
int n = nums.size();
vector<vector<long long>> dp(n+1, vector<long long>(2));
for(int i = n-1; i >= 0; i--) {
int v = nums[i];
int r = v%2;
dp[i][r^1] = dp[i+1][r^1];
dp[i][r] = max(dp[i+1][r], dp[i+1][r^1]-x)+v;
}
return dp[0][nums[0]%2];
}
};
191.位1的个数(位运算)
class Solution {
public:
int hammingWeight(int n) {
int cnt = 0;
while(n) {
n &= n-1;
cnt++;
}
return cnt;
}
};
我最开始的写法是每一位遇到1就cnt++,然后n右移一位。题解的写法更为快,也就是之前231的方法,最低位1反转,cnt++,当n变为0的时候没有位数1能进行反转了,那么循环结束。
338.比特位计数(位运算)
class Solution {
public:
vector<int> countBits(int n) {
vector<int> ans;
for(int i = 0; i <= n; i++) {
int cnt = 0;
int j = i;
while(j > 0) {
j &= j-1;
cnt++;
}
ans.push_back(cnt);
}
return ans;
}
};
这个题解也就是191再套个for循环,诶哟我真是2b哦,写的时候没用j作为临时变量来保存i,结果死循环了,而且还没看出来?!我真是越学越蠢了!这么简单的错误都发现不了。这一题还有动规写法
—未解决—2288.价格减免(字符串、模拟)
class Solution {
public:
string discountPrices(string sentence, int discount) {
double d = 1-discount/100.0;
stringstream ss(sentence);
string ans, w;
while(ss >> w) {
if(!ans.empty()) {
ans += ' ';
}
if(w.length() > 1 && w[0] == '$' && all_of(w.begin()+1, w.end(), ::isdigit)) {
stringstream s;
s << fixed << setprecision(2) << '$' << stoll(w.substr(1))*d;
ans += s.str();
} else {
ans += w;
}
}
return ans;
}
};
先留着,太多陌生的用法了
316.去除重复字母(哈希表、字符串、单调栈)—1081
class Solution {
public:
string removeDuplicateLetters(string s) {
vector<int> left(26), in_ans(26);
for(char c : s) {
left[c-'a']++;
}
string res = "";
for(char c : s) {
left[c-'a']--;//每访问一个字母它的剩余数就-1
if(in_ans[c-'a']) continue;//栈中已经有c了
//剔除栈顶字母,并维护两个哈希表
while(!res.empty() && c < res.back() && left[res.back()-'a']) {
in_ans[res.back()-'a'] = false;
res.pop_back();
}
in_ans[c-'a'] = true;
res.push_back(c);
}
return res;
}
};
看似简单,实则是多个算法的总和应用,最开始没看题解时以为是动态规划的题目,看了标签后实则是一道单调栈的问题,然后我就记忆单调栈的写法写这一题,结果呢,示例二过不了,然后再仔细看题目,它的意思是给出的字符串中所有字母都需要出现一次,而单纯用单调栈的话是不能确保所有字母都出现一次的,所以要解决这一题还需要定义哈希表,而且是两个哈希表,这里的哈希表用的是哈希数组,由于仅仅只是26个字母是已知的数量。可以通过s[i]-'a’来得到对应下标。这里需要定义两个哈希表,left[i]表示字符串i后面还会出现几次,in_ans[i]表示字符i是否已经在单调栈字符串(指的是res)中,true表示在,false表示不在。再就是单调栈的逻辑,在每一次操作的时候需要维护两个哈希表,两个哈希表的变化是有讲究的,在看完题解自己写的过程中就在哈希表的环节出现了错误。
2748.美丽下标对的数目(模拟、哈希表)
class Solution {
public:
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a%b);
}
int countBeautifulPairs(vector<int>& nums) {
int cnt = 0;
for(int i = 0; i < nums.size(); i++) {
for(int j = i+1; j < nums.size(); j++) {
int t = nums[i];
while(t/10) t /= 10;
if(gcd(t, nums[j]%10) == 1) cnt++;
}
}
return cnt;
}
};
这里借此回顾gcd的代码,还有这一题理解错了题目,它要取nums[i]的首位和nums[j]的尾位。。。还有种哈希表的写法,先留着吧
int gcd(int a, int b) {
while(b != 0) {
int t = a;
a = b;
b = t%b;
}
return a;
}
哈希表:
class Solution {
public:
int countBeautifulPairs(vector<int>& nums) {
int ans = 0;
vector<int> cnt(10, 0);
//int cnt[10]{};
//memset(cnt, 0, 10);
for(int x : nums) {
for(int digit = 1; digit <= 9; digit++) {
if(cnt[digit] && gcd(digit, x%10) == 1) ans += cnt[digit];
}
while(x >= 10) x /= 10;
cnt[x]++;
}
return ans;
}
};
这里使用哈希表的原因是nums[i]的最高位上,最高位的数字一定在1-9之间,这样我们可以在遍历数组的同时,统计最高位的出现次数,这样就只需枚举 [1,9] 中的与 x mod 10 互质的数,把对应的出现次数加到答案中。这里有个要注意的点就是注释两行的数组初始化操作会使力扣能通过样例,但是不能通过提交,不知道是为啥。
LCP61.气温变化趋势(数组、模拟)
class Solution {
public:
int temperatureTrend(vector<int>& temperatureA, vector<int>& temperatureB) {
//x > y ---> 1
//x < y ---> -1
//x == y ---> 0
int n = temperatureA.size();
int ans = 0, cnt = 0;
auto cmp = [](int x, int y) -> int {return (x>y) - (x<y);};
for(int i = 1; i < n; i++) {
if(cmp(temperatureA[i], temperatureA[i-1]) == cmp(temperatureB[i], temperatureB[i-1])) {
cnt++;
} else {
cnt = 0;
}
ans = max(ans, cnt);
}
return ans;
}
};
这一题与题解思路还是一样的,顺利ac了,只不过就是要学习的点就在简洁写法上,比较那里我将a和b分开写了,一共写了6行的if,else。题解是利用lambda表达式,并且利用比较的规律(x>y)-(x<y),实在是巧妙!
三路运算写法(C++20)为太空舱运算符,只需将if语句换成下面这条语句即可:
if(temperatureA[i] <=> temperatureA[i-1] == temperatureB[i] <=> temperatureB[i-1])
2663.字典序最小的美丽字符串(数组、贪心)
class Solution {
public:
string smallestBeautifulString(string s, int k) {
k += 'a';
int n = s.size(), i = n-1;
s[i]++;
while(i < n) {
//需要进位
if(s[i] == k) {
if(i == 0) return "";
s[i] = 'a';
s[--i]++;//先处理前面的回文串
} else if(i && s[i] == s[i - 1] || i > 1 && s[i] == s[i - 2]) {
s[i]++;
} else {
i++;
}
}
return s;
}
};
贪心,唉主要还是考脑力,当然这一题对美丽字符串的判断和贪心思路上还是有很多地方值得学习的,对于美丽字符串的第一个条件:为了便于遍历,将这个k转化成了k进制的形式,所以,在判断需要进位的时候就为s[i]==k,第二个条件:对回文子串进行了转化,对一个回文子串来说删除首尾两个字符后那个子串还是一个回文子串,将这句话做逆否命题为:如果一个子串不为回文,那么这个子串首尾添加一个一个字符也不会是回文的。还有就是给出的长度为2,这很关键,所以我们只需要判断s[i]==s[i-1]和s[i]s[i-2],当然由于出现了i-1和i-2就需要留意字符串是否越界的问题哦!其它情况只需要将i++即可,循环退出的条件也就是当in的时候。如何体现了贪心呢?我们优先从后往前遍历、当出现了多个回文子串,优先处理前面的回文串。
503.下一个更大元素II(单调栈、循环数组)
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
vector<int> res;
int i = 0, n = nums.size();
while(res.size() < n) {
int j = i+1;
while(j%n != i) {
if(nums[j%n] > nums[i]) {
res.push_back(nums[j%n]);
break;
}
j++;
}
if(j%n == i) res.push_back(-1);
i++;
}
return res;
}
};
想到了循环数组的写法,这一点还是值得表扬的,但是没有想到更优的解法,那就是加上单调栈
加上单调栈(由底至顶单调递减)
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
int n = nums.size();
vector<int> ans(n, -1);//初始化为-1,最大的数的更大的数对应-1
stack<int> st;//栈顶存放还未找到下一个更大的数的下标
for(int i = 0; i < 2*n; i++) {
int x = nums[i%n];
while(!st.empty() && x > nums[st.top()]) {
ans[st.top()] = x;//当这个数比栈顶大,就将值赋给ans
st.pop();
}
//避免重复添加下标
if(i < n) st.push(i);
}
return ans;
}
};
与第一个写法相比这里只需要遍历数组的两倍长度即可,也算是混过去分了,更优的解法就是加上单调栈,这里是从左往右遍历,比较好理解,还有从右往左的写法,那个还不是很好理解。。。我个人觉得单调栈的题也需要结合栈的定义,也就是st的含义,这十分重要,这里的st就表示栈顶存放还未找到下一个更大的数的下标。这里的循环数组也有讲究,只需要将数组长度设置成nums大小的2倍即可,这样就把数组变成线性的了,通过取余拿到相对应的下标