总结:
问:给你一组数,求这一组数中,每一个子序列(子序列的长度大于1)的最大值减最小值,
并求总和。 可以使用单调栈来进行求解。
什么意思:
1 3 2
中子序列1,3的值为 3 - 1
子序列3, 2 的值为 3 - 2
子序列1 , 3 , 2 的值为 3 - 1
总和为 2 + 1 + 2 == 5
分析:
这种题目就可以使用单调栈来求。求出每一个值 作为 最大值时的次数,作为最小值时的次数。
然后( 最大值时的次数 - 最小值时的次数) * 这个值就是它对答案的影响。
最后求出所有的和。
而求最大值的次数,就是向左找第一个比它大的数的位置(所以可以使用单调栈),这个区间就是左边的区间 temp1,向右找第一个比它大的数(所以可以使用单调栈),这个就是右边的区间 temp2。
如 4 2 3 1 4
找3下标为3作为最大值时,左边到4,下标为1,左边区间为1(一个2, 也就是3的下标 - 4的下标 - 1).
找3的右边区间,右边到4,下标为5,区间也为1,
左边区间 + 右边区间 + 左边区间 * 右边区间
(2,3) (3,1) (2,3,1)的组合
同样,作为最小值的情况,左边区间第一个比它小的值, 右边区间第一个比它小的值
区间左temp1 + 区间右 temp2 + 左 * 右.
再如:4 3 2
3作为最大值:
区间左为0 , 区间右为1
所以 0 + 1 + 0 * 1 就是1.
同时要注意一个细节。
由于我们找右边区间第一个比它大或者右边区间第一个比它小的时候,我们从右往左存入到栈中。而我们已经找过左边的区间了。
单调栈找左边第一个大于 右边第一个大于的时候,单调栈退栈条件一个要大于等于栈顶,一个只能大于栈顶不能等于栈顶。才能退栈
同理,找左边第一个小于,右边第一个小于的时候,也是一样,一个小于等于,一个小于。
这是为什么呢?
拿1 3 1 , 找1作为最小值 为例
左边 小于等于退栈。
第一个1的左边比1小的为下标0.
第二个1的左边比1小的也为下标0(因为3退栈,第一个1退栈,所以找到的下标为0).
所以第一个1的左边区间没有可以让其作为最小值
第二个1的左边区间有2个可以让其作为最小值,也就是3,1的情况, 以及1,3,1的情况
然后我们找右区间,1作为最小值的情况
这时 如果也小于等于
则结果与上面左边区间类似
右边那个1,也就是第二个1,它右区间0个可以让他做为最小值
左边那个1,第一个1,它右区间2个可以让它作为最小值(因为右边1即第二个1退栈,3退栈,所以找到的下标为4)。也就是1,3的情况,以及1,3,1的情况
于是就统计了两个1,3,1的情况,一次左边1作为最小值,一次右边1作为最小值,3作为最大值,然而实际上这两种情况是一样的。
所以需要一个小于等于退栈,一个小于退栈。
如左区间小于等于退栈。则与上面一样
右区间小于退栈。、
则右边1,为0个
左边1,为1个,(因为3退栈,右边1没有退栈,到了右边1停止了。),所以只有1,3的情况。就不会重复。
题目链接:https://ac.nowcoder.com/acm/problem/15815
此题的中文形式题目:https://ac.nowcoder.com/acm/problem/20806
题目:
给你一组数,求这一组数中,每一个子序列(子序列的长度大于1)的最大值减最小值,
并求总和。
代码实现:
# include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N];
int st[N],idx = -1; // 用于找比当前a[i]大的时候的单调栈,这个时候a[i]为最大值
int lmin[N]; //左边区间第一个比a[i]小的值的下标
int lmax[N]; //左边区间第一个比a[i]大的值的下标
int rmin[N]; // 右边区间第一个比a[i]小的值的下标
int rmax[N]; //右边区间第一个比a[i]大的值的下标
int n;
/* 为什么再对应的两次单调栈中 一个有等号,一个没有等号呢? 这是因为,
如 1 3 1的情况下,第一个存在等号,
则后面一个1找到了下标为0的时候,
也就是131中最后一个1已经做了两次最小值,31,131.
而如果再从右往左来求的话,
如果也加等号,则左边第一个1也会算两次,13,131.
而实际上131的情况发生了重复,需要减去。
所以一个加等号,一个不加等号来进行消去
*/
int main()
{
while(~scanf("%d",&n))
{
a[0] = 0x3f3f3f3f; // 单调栈中的守卫
for(int i = 1 ; i <= n ; i++)
{
scanf("%d",&a[i]);
}
a[n + 1] = 0x3f3f3f3f;
idx = -1;
// 首先统计a[i]作为左边区间最大值的时候,找左边比它更大的值的下标,存入到lmax中
st[++idx] = 0;
for(int i = 1 ; i <= n ; i++)
{
while(idx != -1 && a[i] >= a[ st[idx] ] )
{
idx--;
}
lmax[i] = st[idx]; // 左边第一个比它大的值的下标
st[++idx] = i;
}
//再统计以a[i]作为最大值的右区间,找右边比它更大的值的下标,存入到rmax中
idx = -1;
st[++idx] = n + 1;
for(int i = n ; i >= 1 ; i--)
{
while(idx != -1 && a[i] > a[st[idx]])
{
idx--;
}
rmax[i] = st[idx];// 右边第一个比它大的值的下标
st[++idx] = i;
}
a[0] = -0x3f3f3f3f;
a[n + 1] = -0x3f3f3f3f;
//再统计以a[i]作为最小值的左区间,找左边把它更小的值的下标,存入到lmin中
idx = -1;
st[++idx] = 0;
for(int i = 1 ; i <= n ; i++)
{
while(idx != -1 && a[i] <= a[st[idx]]) // 这个时候不要等于号,防止与上面找最大值的区间重复了
{
idx--;
}
lmin[i] = st[idx];
st[++idx] = i;
}
idx = -1;
st[++idx] = n + 1;
for(int i = n ; i >= 1 ; i--)
{
while(idx != -1 && a[i] < a[st[idx]]) // 这个时候不要等于号,防止与上面找最大值的区间重复了
{
idx--;
}
rmin[i] = st[idx];
st[++idx] = i;
}
long long res = 0;
// 先算作为大值的情况
for(int i = 1 ; i <= n ; i++)
{
int len1 = i - lmax[i] - 1;
int len2 = rmax[i] - i - 1;
int len3 = i - lmin[i] - 1;
int len4 = rmin[i] - i - 1;
long long temp1 = len1 + len2 + (long long)len1 * len2; //作为最大值的次数
long long temp2 = len3 + len4 + (long long)len3 * len4; //作为最小值的次数
res += a[i] * (temp1 - temp2);
// res += a[i] * ( (long long)(i - lmax[i]) * (rmax[i] - i) - (long long)(i - lmin[i]) * (rmin[i] - i) );
}
printf("%lld\n",res);
}
return 0;
}