源自力扣:
给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。
由于答案可能很大,因此 返回答案模 10^9 + 7 。
实例:
输入:arr = [3,1,2,4]
输出:17
解释:
子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
代码如下:
int sumSubarrayMins(vector<int>& arr) {
int size = arr.size();
long ans = 0;
long mod = 1000000007;
int left[size];
int right[size];
stack<pair<int,int>> s;
for(int i=0;i<size;i++){
if(s.size()==0){
left[i]=-1;
s.push({arr[i],i});
}else{
if(arr[i]>s.top().first){
left[i] = s.top().second;
s.push({arr[i],i});
}else{
//注意,下面的小于判断没有等于
while((s.size()!=0)&&arr[i]<(s.top().first)){
s.pop();
}
if(s.size()==0){
left[i]=-1;
s.push({arr[i],i});
}else{
left[i] = s.top().second;
s.push({arr[i],i});
}
}
}
}
while(!s.empty()) s.pop();
for(int i=size-1;i>=0;i--){
if(s.size()==0){
right[i]=size;
s.push({arr[i],i});
}else{
if(arr[i]>s.top().first){
right[i] = s.top().second;
s.push({arr[i],i});
}else{
//注意,下面的小于判断有等于
while(s.size()!=0&&arr[i]<=s.top().first){
s.pop();
}
if(s.size()==0){
right[i]=size;
s.push({arr[i],i});
}else{
right[i] = s.top().second;
s.push({arr[i],i});
}
}
}
}
// for(auto i : left)
// cout<<i<<" ";
// cout<<endl;
// for(auto i : right)
// cout<<i<<" ";
for(int i=0;i<size;i++){
ans = (ans + (long)arr[i]*(i-left[i])*(right[i]-i))%mod;
}
return (int)ans;
}
由于暴力方法会导致超时,所以我们使用贡献值计算的方法。贡献值计算方法即从所选的某一元素出发,往左和往右走,直到遇到比所选元素小的值便停止。根据乘法原理有:左右边界可以唯一确定字数组 字数组数 = 左边界种类数*右边界种类数,这样便能算出以选定元素为最小值可以产生的子数组个数。
图源自力扣用户超小白的题解用图,计算如下:
- 值为3,下标为0,左边没有最小值,设为-1,右边最近最小值为1,得其下标1,则生成子数组为(0-(-1))*(1-0)=1,则贡献值为3*1=3.
- 值为1,下标为1,左边没有最小值,设为-1,右边没有比它小的值,设为5(意味着右边所有元素都能为其子数组),则生成子数组为(1-(-1))*(5-1)=8,则贡献值为8*1=8.
- 值为2,下标为2,左边有小值,下标为1,右边也有,下标为4,则生成子数组为(2-1)*(4-2)=2,则贡献值为2*2=4.
- 值为4,下标为3,左边有小值,下标为2,右边也有,下标为4,则生成子数组为(3-2)*(4-3)=1,则贡献值为1*4=4.
- 注意,这里存在着重复元素1,此时令其左边找小于等于的小值,右边找严格小于,或者左边严格小于,右边小于等于,目的是为了避免重复和不产生遗漏。假如两边严格小于,会导致搜索子数组遗漏,假如两边不严格小于,会导致重复。
最后把所有贡献值加起来即可。