枚举右,维护左
1512.好数对的数目
Q:
给出整数数组nums,统计满足 i<j 且 numsi=numsj 的数对个数。
我的思路:
hash数组记录每个数字出现的次数,对于出现次数elem大于1的,将1+2+3+...+(i-1)的结果加到ans中,得到ans
cpp
class Solution {
public:
int numIdenticalPairs(vector<int>& nums) {
vector<int> hash(101);
for(const auto& elem:nums){
++hash.at(elem);
}
int ans = 0;
for(const auto& elem:hash){
if(elem > 1){
for(int i = 1;i < elem;i++){
ans += i;
}
}
}
return ans;
}
};
改进:
- 用unordered_map代替vector实现hash数组
- 思想 从第一个元素开始遍历,并维护一个hash数组,记录每个元素出现的次数;遍历到元素i时,hash数组记录了前面出现过多少次元素i,这些元素i能与当前元素构成数对,将hashi加到结果ans中
cpp
class Solution {
public:
int numIdenticalPairs(vector<int>& nums) {
int ans = 0;
unordered_map<int,int> hash;
for(const auto& elem:nums){
ans += hash[elem];
++hash[elem];
}
return ans;
}
};
3805. 统计凯撒加密对数目
Q:
给出vector<string> words,每个字符串仅包含小写英文字母,统计满足下列条件的下标对(i, j)的数量:
1.i < j
2.wordsi中的每个字母替换为字母表中下一个字母(循环替换),最终与wordsj相等
我的思路:
用vector<int> hash(elem.size())记录每个字符串相邻两个字符之间的ASCII差值
map<vector<int>, int>记录每个vector对应的字符串出现的次数
枚举右维护左,统计答案
cpp
class Solution {
public:
long long countPairs(vector<string>& words) {
map<vector<int>, int> hash;
long long ans = 0;
for(const auto& elem : words){
vector<int> temp(elem.size());
for(int i = 1; i < elem.size(); i++){
temp[i] = (elem[i] - elem[i-1] + 26) % 26;
}
ans += 1LL * hash[temp];
++hash[temp];
}
return ans;
}
};
改进:
1.遍历每个字符串时,将 用vector统计相邻两个字符的差 改为 用字符串本身存储每个字符相对于第一个字符的偏移量,如字符串"abc"记为"012",unordered_map<string, int>记录每个字符串的出现次数
2.优化结果的更新次数时,先记录words中原字符串的出现个数,然后将每组相同的原字符串统一化(即统一为每个字符相对于第一个字符偏移量如"012"的形式),统一化后,当前新字符串(个数为c)与已经记录的新字符串能形成下标对,当前新字符串彼此之间能形成 C c 2 C_c^2 Cc2个下标对,更新进答案
cpp
class Solution {
public:
long long countPairs(vector<string>& words) {
unordered_map<string, int> cnt_words;
for(const auto& elem : words){
++cnt_words[elem];
}
long long ans = 0;
unordered_map<string, int> hash;
for(auto& [s, c] : cnt_words){
string temp = s;
char base = temp[0];
for(char& ch : temp){
ch = (ch - base + 26) % 26;
}
ans += 1LL * hash[temp] * c + 1LL * c * (c-1) / 2;
hash[temp] += c;
}
return ans;
}
};
这里对于string temp = s,灵神的解答给的是string temp = std::move(s),将s转化为右值后赋值给temp.
需要注意的是unordered_map的元素为pair<const K, V>,即这里的s是const string,这对于后面我们要更改temp的值来说是不符合const correctness的,由于string的move constructor接受的是std::string&&类型,故实际上会调用copy constructor,将const std::string&&隐性转化为const std::string&,最终temp的类型是std::string
负数的取模
在C++/Java等语言中,负数取模的结果为负数或零
-3 % 2 = -1
-2 % 2 = 0
a % b = a - (a / b) * b //C++取模公式向零取整
枚举中间
i < j < k 类型,枚举j
2506. 统计相似字符串对的数目
Q:
给出字符串数组words,如果两个字符串由相同的字符组成,则这两个字符串相似e.g."abca"与"bbccaa"相似,统计满足字符串wordsi和wordsj相似的下标对(i, j)个数
我的思路:
维护哈希表中map的key时,我选择了std::set<char>作为key,对于本题来说时间消耗与内存消耗均较高
改进:
使用位运算,选择一个26位二进制整数作为key,每位数表示对应字母是否在字符串中出现
e.g. "abca"含字符'a' 'b' 'c',则第一二三位设为1,其余位为0
cpp
int m = 0; //key
for(const char& ch : str){
m |= 1 << (ch - 'a'); // 1 << (ch-'a')得到ch对应的位为1
}
对角线遍历
1329. 将矩阵按对角线排序
Q:
给出m*n矩阵,将矩阵的每个对角线上的元素升序排列(左上到右下),返回排序好的矩阵
A:
按对角线遍历元素时,我们从最右侧的对角线开始遍历。
对于同一条对角线内每个的元素(i, j),i - j 为定值,我们记录 k = i - j + n ,最右侧的对角线对应的k = 0 - (n-1) + n = 1,最左侧的对角线k = (m-1) - 0 + n = m + n - 1,k的取值范围为 1, m+n-1
对于一个确定的k,在其所对应的对角线中,我们遍历纵坐标j,j的最小值当i = 0时取到,且j不为负数,有min_j = max(n-k, 0);j的最大值当i = m-1时取到,且j不超过n-1,有max_j = min(m-1+n-k, n-1)
按对角线遍历的所有元素就被我们转化为先遍历对角线(k: 1, m+n-1 ),再遍历对角线内的元素(j: min_j, max_j )
cpp
class Solution {
public:
vector<vector<int>> diagonalSort(vector<vector<int>>& mat) {
int m = mat.size();
int n = mat[0].size();
for(int k = 1; k < m+n-1; k++){ // k = i-j+n
int min_j = max(0, n-k);
int max_j = min(n-1, m+n-k-1);
vector<int> temp;
for(int j = min_j; j <= max_j; j++){
temp.push_back(mat[k-n+j][j]);
}
sort(temp.begin(), temp.end());
for(int j = min_j; j <= max_j; j++){
mat[k-n+j][j] = temp[j-min_j];
}
}
return mat;
}
};
前缀和
模板: 统计数组vector<int> nums的前缀和vector<int> prev
其中previ 表示数组nums中的前i个数,即nums0, nums1, ... numsi-1
cpp
int n = nums.size();
vector<int> prev(n+1, 0);
for(int i = 0; i < n; i++){
prev[i+1] = prev[i] + nums[i];
}
2588.统计美丽子数组数目
Q:
给定整数数组nums,每次操作中可以:
选择两个不同下标的i和j,将numsi和numsj同时减去 2 k 2^k 2k(k为非负整数,numsi与numsj二进制对应位为1)
如果一个子数组在若干次操作内(包括0次)可以变成一个全为0的数组,称它为一个美丽的子数组
返回数组nums中美丽子数组的个数
我的思路:
在维护前缀和哈希表时,用一个数组vector<int> sum记录二进制对应位数相加的和(mod 2)
遍历数组时,将每个数转化为二进制,对应位数加到sum中
若维护的哈希表中出现mod 2结果完全相同的前缀和,说明两前缀和相减对应二进制位上数字之差为2的倍数,可以消除
改进:
二进制对应位数相加,我们要得到结果mod 2的值
使用异或 来操作
1^1 = 0 0^0 = 0表示奇数与奇数的差结果为偶数,偶数与偶数的差结果为偶数,满足统计条件
0^1 = 1表示偶数与奇数的差为奇数,不满足统计条件
cpp
class Solution {
public:
long long beautifulSubarrays(vector<int>& nums) {
long long ans = 0;
unordered_map<int, int> hash = {{0, 1}};
int sum = 0;
for(int i : nums){
sum ^= i;
if(hash.find(sum) != hash.end()){
ans += hash[sum];
}hash[sum]++;
}
return ans;
}
};
差分数组
对于数组a,定义差分数组
d[] = {a[0], a[1]-a[0], a[2]-a[1]...}
di += n 表示将下标 >= i 的数都加上n
二维差分数组
对于二维数组aij,定义差分数组di+1j+1
其中d[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]
dmn += n 表示将下标 i >= m, j >= n 的所有数都加上n
要更新(x1, y1) - (x2, y2)里的所有数都加上n
cpp
d[x1][y1] += n;
d[x1][y2+1] -= n;
d[x2+1][y1] -= n;
d[x2+1]y2+1] += n;
通过差分数组得到原数组
a[i][j] = d[i][j] +a[i-1][j] + a[i][j-1] - a[i-1][j-1]
边遍历边更新
栈
getline
复习一下getline(istream& is, string& str, char delim)
将输入流is的内容截止到delim之前存储到字符串str中(delim默认值为'\n')
e.g. 读取Unix文件路径转化为标准路径 "/home/whyteafo/.../"
cpp
string str = "/home/whyteafo/../";
vector<string> stk;
isstringstream is(str);
string temp;
while(getline(is, temp, '/'){
if(temp == '.' || temp.empty()){
continue;
}
if(temp != '..'){
stk.push_back(s);
}else if(!stk.empty()){
stk.pop_back();
}
}
// stk to ans
1209. 删除字符串中的所有相邻重复项 II
Q:
给定字符串s,整数k,对于字符串s中相邻k个相同的字符将其删除,对剩余字符串继续进行该操作,返回最终的字符串
题目数据保证结果唯一
A:
存储"aasssswwww"这类有连续相同字符的字符串,我们使用vector<pair<char, int>> ,将字符串看成一个个相同字符构成的pair,pair.first表示该字符,pair.second为相同的字符个数
我们利用栈解决这道题
当pair.second增加到k时,表示相同的k个字符,我们将这个pair从栈中弹出
当当前字符与上个字符不同时,我们压入一个新的pair
cpp
class Solution {
public:
string removeDuplicates(string s, int k) {
vector<pair<char, int>> stk;
for(char c : s){
if(!stk.empty() && stk.back().first == c){
if(++stk.back().second == k){
stk.pop_back();
}
}else{
stk.push_back(pair(c, 1));
}
}string ans;
for(auto& p : stk){
int cnt = p.second;
while(cnt--){
ans += p.first;
}
}return ans;
}
};
1006. 笨阶乘
Q:
给定整数n,1 <= n <= 10000,用 * / + - 连接从n到1的排列,按四则运算顺序计算结果,除法向下取整
e.g. n = 10 ans = 10 * 9 / 8 + 7 - 6 * 5 / 4 + 3 - 2 * 1
A:
这里用栈记录结果
若当前运算为加减法,则将当前元素压入栈中(负数则压入当前元素的相反数)
若当前运算为乘除法,对栈顶元素做乘除运算
用一个整数index记录当前的运算种类
index = 1, 2, 3, 4 对应 * / + -
cpp
class Solution {
public:
int clumsy(int n) {
int ans = 0;
int index = 0; // * / + -
vector<int> stk;
stk.push_back(n);
for(int i = n - 1; i > 0; i--){
if(index == 0){
stk.back() *= i;
}else if(index == 1){
stk.back() /= i;
}else if(index == 2){
stk.push_back(-i);
}else{
stk.push_back(i);
}
i = (i+1) % 4;
}
ans = reduce(stk.begin(), stk.end(), 0);
return ans;
}
};
单调队列
单调队列 = 滑动窗口 + 单调栈
三步:入 / 出 / 更新
顺序根据题意变化
若更新答案要用到新元素,则入-更新 ;若更新答案不用到新元素,则更新-入
239. 滑动窗口最大值
Q:
给定整数数组nums,大小为k的滑动窗口从数组最左侧移动到最右侧,每次只移动一位
返回每个滑动窗口中元素的最大值组成的数组
A:
维护双端队列deque<int> q,记录更新答案要用的元素的下标 ,并保证q中元素单调递减
入: 遍历数组,在插入每个新元素时,我们检查队列q末尾的元素是否<=新元素,若条件为真,则后续挑选滑动窗口最大值时,只有可能选择新元素作为最大值,队列末尾元素永远不可能被选中 ,我们将其删除
出: 若队列头元素超出滑动窗口范围,删除
更新: 由于队列q单调递减,队列头元素一定是当前滑动窗口中的最大元素 ,我们将其更新为答案
(每次写q.back()等记得想一想q.empty()要不要判断)
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
vector<int> ans(n-k+1, 0);
deque<int> q;
for(int i = 0; i < n; i++){
// insert new element:
// delete all elements not greater than new element
// since they will never serve as the biggest number
while(!q.empty() && nums[q.back()] <= nums[i]){
q.pop_back();
}
q.push_back(i);
// delete element out of range
int left = i - k + 1;
if(q.front() < left){
q.pop_front();
}
// update ans
if(left >= 0){
ans[left] = nums[q.front()];
}
}
return ans;
}
};