题干:
Feel Good
Time Limit: 3000MS | Memory Limit: 65536K | |
Total Submissions: 12409 | Accepted: 3484 | |
Case Time Limit: 1000MS | Special Judge |
Description
Bill is developing a new mathematical theory for human emotions. His recent investigations are dedicated to studying how good or bad days influent people's memories about some period of life.
A new idea Bill has recently developed assigns a non-negative integer value to each day of human life.
Bill calls this value the emotional value of the day. The greater the emotional value is, the better the daywas. Bill suggests that the value of some period of human life is proportional to the sum of the emotional values of the days in the given period, multiplied by the smallest emotional value of the day in it. This schema reflects that good on average period can be greatly spoiled by one very bad day.
Now Bill is planning to investigate his own life and find the period of his life that had the greatest value. Help him to do so.
Input
The first line of the input contains n - the number of days of Bill's life he is planning to investigate(1 <= n <= 100 000). The rest of the file contains n integer numbers a1, a2, ... an ranging from 0 to 10 6 - the emotional values of the days. Numbers are separated by spaces and/or line breaks.
Output
Print the greatest value of some period of Bill's life in the first line. And on the second line print two numbers l and r such that the period from l-th to r-th day of Bill's life(inclusive) has the greatest possible value. If there are multiple periods with the greatest possible value,then print any one of them.
Sample Input
6
3 1 6 4 5 2
Sample Output
60
3 5
解题报告:
这个题干是真,的,恶,心。一会最大一会最小,搞这么复杂弄的题目看半天。。不过抽出核心部分就是一句话,求一个给定区间的 区间和与该区间最小值的 最大值。这题与HDU-1506那道Largest Rectangle(求最大矩形面积)差不多。
那道题的思路是:反正得是个矩形 还需要最大面积,那上底边一定和某一个高度相等吧?那我就把每个高度所能延伸到的最远距离(也就是矩形的长)求出来,最后"长"乘"高",里面肯定有我要的答案了啊。而求每一个高度对应的"长"的过程,也就是单调栈的过程。
本题也是一样,我就把每一个a[i] 都当做最小值,左边右边分别看看最远能延伸哪(这一步用单调栈),然后乘起来放在maxx里,维护maxx不就好了吗。
另一个题解中的解释我觉得不错:
题意是给出一个序列,要求的是一个区间,这个区间的最小值乘以这个区间数字的和 是最大值。求这个最大值与这个区间。
即:给你n个数,求一个 max,其中max=某个区间和*该区间的最小值。
ac代码1:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<stack>
using namespace std;
const int MAX = 1e6 + 5;
int a[MAX], L[MAX], R[MAX], top;
long long sum[MAX];
int n;
int l,r;
long long maxx;
stack<int > sk;
int main()
{
cin>>n;
for(int i = 1; i<=n; i++) {
scanf("%d",&a[i]);
sum[i] = sum[i-1] + a[i];
}
//需要左侧第一个比a[i]小的元素,所以 从左向右维护一个严格单调递增栈
for(int i = 1; i<=n; i++) {
while(!sk.empty() && a[sk.top() ] >= a[i]) sk.pop();
if(sk.empty() ) L[i] = 0;
else L[i] = sk.top();
sk.push(i);
}
while(!sk.empty() ) sk.pop();
//需要右侧第一个比a[i]小的元素,所以 从右向左维护一个严格单调递增栈
for(int i = n; i>=1; i--) {
while(!sk.empty() && a[sk.top() ] >= a[i]) sk.pop();
if(sk.empty() ) R[i] = n+1;
else R[i] = sk.top();
sk.push(i);
}
l=r=1;
long long tmp;
for(int i = 1; i<=n; i++) {
tmp=sum[R[i] - 1 ] - sum[L[i] ];
tmp*=a[i];
if(tmp>maxx) {
l=L[i]+1;
r=R[i]-1;
maxx=tmp;
}
}
// printf("%d %d\n",L[4],R[4]);
printf("%lld\n",maxx);
printf("%d %d\n",l,r);
return 0 ;
}
初始化的细节!!!!!
注意上面这个代码中,末尾的l=r=1是有讲头的,此处赋值成别的值就wa了。原因如下:(感谢wjh巨巨的分享)
要么maxx赋初始值-1 要么写tmp>=maxx (这两种情况的l和r都不需要赋初始值)。
如果你按ac代码中这样写,那就只能l=r=1才可以!! 因为这题卡了极限样例需要特判一下:这样输入:1\n0\n 或者1\n3\n你会发现输出都是你赋值的l
别的ac代码:(维护单调递减栈?也可以做?)(好像还是递增栈、、、)
实际上这个题目就是要对每一个节点进行扩展,这样扩展的话,复杂度是O(n^2)。减少时间复杂度要用单调栈,单调栈处理的问题就是对每一个节点进行扩展的问题,这个题目要维护的是一个单调递减栈,即从栈顶元素到栈底元素,值是单调递减的,即栈顶元素的值始终是栈的最大值。然后每一个值有属于自己的区间,这个区间目的是为了记录之后的元素向前延伸的用处。
向后延伸就靠从1到n扫描元素,(维护单调递减栈)这样当扫描的元素大于栈顶元素时,直接入栈。
当扫描的元素等于栈顶元素时,不记录,只将区间延伸到后面。
当扫描的元素小于栈顶元素时,这时要计算栈内当前的值。因为扫描的元素时小于栈顶元素的,要求的是一个区间的最小值,所以栈内那些大于该元素的值你会发现没有用处了,只需要将它们的那些区间留下来就对了,这就是向前扩展。
拿题目的sample举例子:
3 1 6 4 5 2
一开始每一个数都有自己的区间:
3(1,1) 1(2,2) 6(3,3) 4(4,4) 5(5,5) 2(6,6) -1(7,7)后面加一个最小值,为了最后计算栈内元素使用。
先是3入栈。栈内元素 3(1,1)
1<3,首先计算一下栈内元素的值,记录下来。然后要把栈内大于1的全部弹出来,但是把它们的区间留下,栈内就变成了1(1,2)。实际上此时就会知道(1,2)这段区间之内的最小值是1。
6>1,直接入栈,栈内元素变为1(1,2),6(3,3)。
4<6,将6弹出,弹出之前计算值。然后栈内就变为1(1,2),4(3,4)。
5>4,直接入栈。栈内元素是1(1,2),4(3,4),5(5,5)。会发现因为5没有办法向前扩展了所以会知道5只能够在(5,5)的区间内最小,所以说站内元素是在自己区间的左端点与栈顶元素的右端点,这段区间之内满足着最小值的关系。1是在(1,5)这段区间内最小,4是在(3,5)这段区间内最小。这些值都会在碰到扫描的元素小于该元素时计算,记录下来,就是这样单调栈完成了对每一个元素进行左右扩展的目的。
2<5,2<4。要把5(5,5) 4(3,4)分别弹出,它们走之前要计算各自区间的值。
最后是-1,目的就是要将栈内所有元素弹出,计算每一个元素左右扩展的值。
#include <functional>
#include <algorithm>
#include <iostream>
#include <fstream>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <stack>
#include <map>
#include <bitset>
#include <set>
#include <vector>
using namespace std;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double eps = 1e-15;
typedef long long LL;
typedef pair <int, int> PLL;
static const int N = 100100;
stack <PLL> st;
int val[N];
LL sum[N];
int l[N];
int r[N];
int main() {
int n;
while (~scanf("%d", &n)) {
sum[0] = 0;
for (int i = 1; i <= n; ++i) {
scanf("%d", &val[i]);
sum[i] = sum[i - 1] + val[i];
l[i] = r[i] = i;
}
while (!st.empty()) {
st.pop();
}
for (int i = n; i >= 1; --i) {
if (st.empty()) {
st.push(make_pair(val[i], i));
}
else {
while (!st.empty()) {
PLL u = st.top();
if (val[i] >= u.first) {
break;
}
st.pop();
l[u.second] = i + 1;
}
st.push(make_pair(val[i], i));
}
}
while (!st.empty()) {
PLL u = st.top();
st.pop();
l[u.second] = 1;
}
for (int i = 1; i <= n; ++i) {
if (st.empty()) {
st.push(make_pair(val[i], i));
}
else {
while (!st.empty()) {
PLL u = st.top();
if (val[i] >= u.first) {
break;
}
st.pop();
r[u.second] = i - 1;
}
st.push(make_pair(val[i], i));
}
}
while (!st.empty()) {
PLL u = st.top();
st.pop();
r[u.second] = n;
}
LL ans = -1;
int L, R;
for (int i = 1; i <= n; ++i) {
int y = r[i];
int x = l[i];
if (ans < (sum[y] - sum[x - 1]) * val[i]) {
ans = (sum[y] - sum[x - 1]) * val[i];
L = x;
R = y;
}
}
printf("%lld\n", ans);
printf("%d %d\n", L, R);
}
return 0;
}
总结:
至今不明白为什么要维护一个严格单调递增栈。
答:因为你要的是一个严格小于你的元素,然后要取这两个下标中间的区域作为区间(因为中间就都是比你大的或相等的了)。所以,其实使用单调递增栈(以此举例)的目的不是为了找第一个严格比你小的值(虽然具体实现是这样的)而是为了找中间那部分都比你大的值的所在区间。严格单调递增栈(实现语句为带等号的 a[sk.top() ] >= a[i] -> then pop掉),找通过找严格小于他的第一个元素,目的是寻求大于等于他的那部分区间区域。这几个逻辑关系要搞清楚!!
(但有的题 用单调递减栈目的就是很单纯的找第一个比他高的元素,eg HDU 5033)稍后贴博客
与滑窗算法做区别?
滑动窗口的精髓是在一个数列里保存着一个递增数列,同时这个数列维持了它在原数列的位次递增,这个窗口里保存的第一个数即在这个区间里最小的数。这样不停得把新输入的数同这个滑动窗口里右边的数比较,如果比它大,删除窗口里的这个数,同时删除的数统治区域的最右边就是新输入的数,它的左统治区域即新输入的数的左统治区域,然后不停地向窗口的左边比。这样只需要一个数组记录它左边区域的边界就可以了, 右边同理..
附一个滑窗算法典型题目 【POJ - 2823】 Sliding Window
题解:https://blog.csdn.net/qq_41289920/article/details/81071905