11/27 907. 子数组的最小值之和
给定一个数组,求出其每个子数组内最小值的累加和。
题目链接
题目数组长度为
3
×
1
0
4
3\times10^4
3×104,意味着不能直接累加【
O
(
n
2
)
O(n^2)
O(n2)】,而需要从元素本身去考虑。
考虑数组最小值的贡献:在所有包含该元素的子数组中,最小值都是它。
考虑数组第二小值的贡献:在所有不包含最小值而包含它的子数组中,最小值都是它。
以此类推,我们可以发现,一个元素可产生的贡献次数取决于该元素左右两边比它小的元素所在位置。
由乘法原理不难想到,以
[
l
,
r
]
[l,r]
[l,r]为界,包含第i位元素的子数组共有
(
i
−
l
)
(
r
−
i
)
(i-l)(r-i)
(i−l)(r−i)个,于是题目转化为对每个元素找其左右较小元素再计算贡献。
但仅仅如此暴力仍然是【
O
(
n
2
)
O(n^2)
O(n2)】,要进一步高效找一个元素的左右边界,此时想到单调栈。
维护一个单调栈,每次新元素到来时弹出栈内所有大于它的元素,这样所得到的栈顶就是左边第一个比它小的元素;对于所有被弹出栈顶的元素而言,新来的元素就是它们的右边界。(这种弹出方式的正确性显然【考虑新来元素的位置和大小】,且放下标更合适)
于是我们将边界计算压缩到一个单调栈一轮循环结束,
O
(
n
)
O(n)
O(n)达成。
class Solution {
public:
int sumSubarrayMins(vector<int> &arr) {
long long ans = 0;
arr.push_back(-1);
stack<int> st;
st.push(-1); // 哨兵
for (int r = 0; r < arr.size(); ++r) {
while (st.size() > 1 && arr[st.top()] >= arr[r]) {
int i = st.top();
st.pop();
ans += 1ll * arr[i] * (i - st.top()) * (r - i); // 累加贡献
}
st.push(r);
}
return ans % 1000000007;
}
};
11/28 1670. 设计前中后队列
实现一个能在左中右三个位置进行push/pop操作的类队列数据结构。
题目链接
大模拟是最乏善可陈的题目,没啥意思。
在正中间操作,本质是要求将单个队列以正中间为界裂出两个队列。
奇数和偶数的正中间稍有不同,处理时要注意维护“正中间”。
具体操作可以看代码。
class FrontMiddleBackQueue {
deque<int>front;
deque<int>end;
public:
FrontMiddleBackQueue() {
front.clear();
end.clear();
}
void pushFront(int val) {
front.emplace_front(val);//放在正前方
if(front.size() > end.size() + 1){
//当前队列的size大于后队列size+2时,为调整正中间,向后移动一位
end.emplace_front(front.back());
front.pop_back();
}
}
void pushMiddle(int val) {
//中间在我们的定义中就是front.back(),因此若front元素少于或等于end,直接推入front就行
if(front.size() <= end.size()) front.emplace_back(val);
else{
//否则,说明需要调整,将end调整后再加入队列
end.emplace_front(front.back());
front.pop_back();
front.emplace_back(val);
}
}
void pushBack(int val) {
end.emplace_back(val);
if(end.size() > front.size() + 1){
//当后队列的size大于前队列size+2时,为调整正中间,向前移动一位
front.emplace_back(end.front());
end.pop_front();
}
}
int popFront() {
int ans = -1;
if(!front.empty()){
ans = front.front();
front.pop_front();
//弹后维护正中间
if(end.size() > front.size() + 1){
front.emplace_back(end.front());
end.pop_front();
}
}
else if(!end.empty()){
//没有前队列了(元素数为1)
ans = end.front();
end.pop_front();
}
return ans;
}
int popMiddle() {
int ans = -1;
//再强调一遍,正中间是front.back()
if(front.size() >= end.size() && !front.empty()){
//没有前队列了(元素数为1) ans = front.back();
front.pop_back();
}//没有前队列了(元素数为1)
else if(!end.empty()){
ans = end.front();
end.pop_front();
}
return ans;
}
int popBack() {
int ans = -1;
if(!end.empty()){
ans = end.back();
end.pop_back();
if(front.size() > end.size() + 1){
end.emplace_front(front.back());
front.pop_back();
}
}
else if(!front.empty()){
ans = front.back();
front.pop_back();
}
return ans;
}
};
11.29 2336. 无限集中的最小数字
实现一个能“去除最小元素”“添加元素”的集合。
题目链接
杀鸡切勿用牛刀,你看这数据结构n<1000,一个set足矣。
每次remove最小元素就是remove head,add就是增加元素条目。
class SmallestInfiniteSet {
private:
set<int> s;
public:
SmallestInfiniteSet() {
for (int i = 1; i < 1001; ++i) {
s.insert(i);
}
}
int popSmallest() {
int res = *s.begin();
s.erase(s.begin());
return res;
}
void addBack(int num) {
s.insert(num);
}
};
11.30 1657. 确定两个字符串是否接近
两个字符串接近,当且仅当能通过如下两个变换变成彼此,试问给定的两个字符串是否接近:
- 交换两个字母的存在(即a替换为b,b替换为a)
- 交换两个字母的位置(即ab变为ba)
题目链接
由题目,我们可以将相同的字母直接视作同一类token,两字符串相似意味着:
- 长度相同
- 出现的token类别完全相同
- 将token从大到小排列后,出现次数能一一对应(否则无法通过交换存在和位置确认是否接近,证明显然)
哈希化token之后直接作比较就好了。
class Solution {
public:
bool closeStrings(string word1, string word2) {
if(word1.length() != word2.length()) return false;
int wordmap1[26]={0}, wordmap2[26]={0};
int wordhash1=0, wordhash2=0;
for(int i=0;i<word1.length();i++) wordmap1[word1[i]-'a']++, wordhash1|=(1 << (word1[i]-'a'));
for(int i=0;i<word2.length();i++) wordmap2[word2[i]-'a']++, wordhash2|=(1 << (word2[i]-'a'));
if(wordhash1 != wordhash2) return false;
sort(wordmap1, wordmap1+26);sort(wordmap2, wordmap2+26);
for(int i=0;i<26;i++) if(wordmap1[i] != wordmap2[i]) return false;
return true;
}
};
12/01 2661. 找出叠涂元素
给你一个下标从 0 开始的整数数组 arr 和一个 m x n 的整数 矩阵 mat 。arr 和 mat 都包含范围 [1,m * n] 内的 所有 整数。
从下标 0 开始遍历 arr 中的每个下标 i ,并将包含整数 arr[i] 的 mat 单元格涂色。
请你找出 arr 中第一个使得 mat 的某一行或某一列都被涂色的元素,并返回其下标 i 。
题目链接
看起来很复杂,但说穿了就是要求给定一个数字,能用O(1)的时间找到其对应的行列。
又是哈希表的时刻,预先存储每个元素值对应的所在行列,之后根据序列模拟涂色即可。
class Solution {
public:
int firstCompleteIndex(vector<int>& arr, vector<vector<int>>& mat) {
int n = mat.size(), m = mat[0].size();
vector<pair<int,int>> e(n*m+1);
for(int i=0;i<mat.size();i++)
for(int j=0;j<mat[0].size();j++) e[mat[i][j]] = {i, j};
vector<int> rowcount(n+1,0);
vector<int> colcount(m+1,0);
for(int i=0;i<n*m;i++){
int color = arr[i];
rowcount[e[color].first]++;
colcount[e[color].second]++;
if(rowcount[e[color].row] == m || colcount[e[color].col] == n) return i;
}
return 0;
}
};
12/02 1094. 拼车
给定一个乘客乘车序列【起始点,到达点,乘车人数】,车的最大容载量,判断一趟行程能否满足所有乘客的乘车需求。
题目链接
简单模拟一下所有乘客在同一时刻发出请求,记录每个站点上下客的人数,转化为乘员变动序列。
之后让车开一遍,并根据乘员变动序列增减乘客,看是否会超过容载量即可。
行车不规范,亲人两行泪。
class Solution {
public:
bool carPooling(vector<vector<int>>& trips, int capacity) {
int off[1001];
memset(off, 0, sizeof(off));
for(auto pass: trips){
off[pass[1]] += pass[0];
off[pass[2]] -= pass[0];
}
int passnum = 0;
for(int i=0;i<=1000;i++){
passnum += off[i];
if(passnum > capacity) return false;
}
return true;
}
};
12/03 1423. 可获得的最大点数
给定一个卡牌序列,可以取k次,每次可以从当前牌堆头或者尾取一张,求能得到的卡牌点数和最大值。
题目链接
不难看出最后结果一定是从开头连续取
k
1
k_1
k1张,从末尾倒着连续取
k
2
k_2
k2张的两段连续序列之和(
k
1
+
k
2
=
k
k_1+k_2 = k
k1+k2=k)。
于是可以用滑动窗口或前缀和解决。
滑动窗口:最开始是从开头取k张,之后逐渐向左滑(类似首尾相接的纸带)。官方解的逆向思维也很好。
前缀和:用前缀和算出两段序列大小即可。
class Solution {
public:
int maxScore(vector<int>& cardPoints, int k) {
int n = cardPoints.size(), ans = 0;
//滑窗
int l = n-1, r = k-1, sum = 0;
for(int i=0;i<k;i++) sum+=cardPoints[i];
ans = sum;
while(r){
ans = max(ans, sum-cardPoints[r--]+cardPoints[l--];
}
//前缀和
int prefix[n+1];
memset(prefix,0,sizeof(prefix));
for(int i=1;i<=n;i++) prefix[i] = prefix[i-1] + cardPoints[i-1];
for(int i=0;i<=k;i++){
ans = max(ans, prefix[i] + prefix[n] - prefix[n-k+i]);
}
cout << "\n";
return ans;
}
};