1 条题解
-
0
提供两种做法,一种是依赖于单调栈和 ST 表的 做法,思维难度较低;还有一个绝妙的做法,根本想不到,但不需要任何现成的 算法/数据结构, 时间复杂度 。
做法一
分别考虑每一个数在多少个区间内成为了次大值。
最大值在前面时,能当最大值的只有离当前这一个(位置记为 )最近的比他大的,当取到这个最大值(位置记为 )时,区间的最大和次大已确定,只需要把区间 取进去, 左边和 右边比他俩都小的去不去都无所谓。设 左边第一个比 大的为 , 右边第一个比 大的是 , 则 对答案的贡献是 。
实际做的时候还要再跑一遍最大值在后面的。
用单调栈可以方便地维护 ,那么如何维护 呢? 可以使用 ST 表实现,类似倍增求 LCA 那样能跳的救跳,即若这一段最大值还是小于 就跳。
代码:
#include <cstdio> #include <stack> #include <algorithm> #define int long long const int N = 100005; int n, pre[N], suc[N], stL[N][20], stR[N][20], a[N], ans; std::stack<int> st; int findL(int now, int m) { for(int i = 17; i >= 0; i--) if(stL[now][i] < m && now - (1 << i) >= 1) now = now - (1 << i); return now; } int findR(int now, int m) { for(int i = 17; i >= 0; i--) if(stR[now][i] < m && now + (1 << i) <= n) now = now + (1 << i); return now; } signed main() { scanf("%lld", &n); for(int i = 1; i <= n; i++) scanf("%lld", &a[i]); for(int i = 1; i <= n; i++) stL[i][0] = a[i-1], stR[i][0] = a[i+1]; for(int j = 1; j <= 17; j++) for(int i = 1; i <= n; i++) if(i - (1 << j) >= 1) stL[i][j] = std::max(stL[i][j-1], stL[i - (1 << (j-1))][j-1]); for(int j = 1; j <= 17; j++) for(int i = n; i >= 1; i--) if(i + (1 << j) <= n) stR[i][j] = std::max(stR[i][j-1], stR[i + (1 << (j-1))][j-1]); a[0] = n+1, a[n+1] = n+1; st.push(0); for(int i = 1; i <= n+1; i++) { while(!st.empty() && a[st.top()] < a[i]) { suc[st.top()] = i; st.pop(); } pre[i] = st.top(); st.push(i); } for(int i = 1; i <= n; i++) { int lBer = pre[i], rBer = suc[i]; int tmpl = findL(lBer, a[i]), tmpr = findR(rBer, a[i]); if(lBer >= 1) ans += (lBer - tmpl + 1) * (rBer - i) * a[i]; if(rBer <= n) ans += (tmpr - rBer + 1) * (i - lBer) * a[i]; } printf("%lld", ans); return 0; }做法二
来考虑如何优化做法一,或者你不会单调栈,也不会 ST 表怎么办?
做法一看上去似乎没有可以优化的地方了,但实际上线性的做法特别的简洁,让人体会到算法竞赛的魅力:
换一种求解的顺序
在原来的做法里,我们先统计第一个对答案的贡献,再统计第二个对答案的贡献,没有什么是现成的,一切都需要重新求解。
但如果我们从 等于 的位置开始求解呢?
这时候,其它所有的显然都比它大,不需要求解,而后面再求解的数显然都比这个要大,所以遇到这一个直接跳过就好了。
细品一下这一段话,我们其实是利用了原先求解的结果,去掉了不必要的求解。因为原先做法中,用 ST 表就是为了跳过所有比当前这个小的,但用从小到大的求解顺序,只需要把原来求到过的都跳过就行了,不需要重新求解,所以降低了时间复杂度。
每次更新时,比当前更小的都被跳过了,直接调用没跳过的就是大于当前的,所以这样是对的。
具体的实现使用双向链表 表示这个去掉所有跳过的左边是什么, 表示去掉跳过的 的右边是什么。最后更新一下,跳过当前这一个就可以了。
代码:
#include <cstdio> #define int long long const int N = 100005; int L[N], R[N], x, map[N], n; signed main() { scanf("%lld", &n); for (int i = 1; i <= n; i++) { scanf("%lld", &x); map[x] = i; L[i] = i - 1, R[i] = i + 1; } int ans = 0; for (int i = 1; i <= n; i++) { int l = L[map[i]], r = R[map[i]]; if (l >= 1) ans += i * (l - L[l]) * (r - map[i]); if (r <= n) ans += i * (R[r] - r) * (map[i] - l); L[r] = l, R[l] = r; } printf("%lld", ans); return 0; }仅 行,从各个方面吊打原来的 行做法,妙不妙?
- 1
信息
- ID
- 11740
- 时间
- 2000ms
- 内存
- 1024MiB
- 难度
- 10
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者