妙哉!Second Sum!

提供两种做法,一种是依赖于单调栈和 ST 表的 O(nlgn)\mathcal O(n \lg n) 做法,思维难度较低;还有一个绝妙的做法,根本想不到,但不需要任何现成的 算法/数据结构, 时间复杂度 O(n)\mathcal O(n)

传送门

做法一

分别考虑每一个数在多少个区间内成为了次大值。

最大值在前面时,能当最大值的只有离当前这一个(位置记为 rr)最近的比他大的,当取到这个最大值(位置记为 ll)时,区间的最大和次大已确定,只需要把区间 [l,r][l, r] 取进去,ll 左边和 rr 右边比他俩都小的去不去都无所谓。设 ll 左边第一个比 ara_r 大的为 ll', rr 右边第一个比 ara_r 大的是 rr', 则 ara_r 对答案的贡献是 (ll)×(rr)(l - l') \times (r' - r)

实际做的时候还要再跑一遍最大值在后面的。

用单调栈可以方便地维护 ll ,那么如何维护 ll' 呢? 可以使用 ST 表实现,类似倍增求 LCA 那样能跳的救跳,即若这一段最大值还是小于 ara_r 就跳。

代码:

#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 表怎么办?

做法一看上去似乎没有可以优化的地方了,但实际上线性的做法特别的简洁,让人体会到算法竞赛的魅力:

换一种求解的顺序

在原来的做法里,我们先统计第一个对答案的贡献,再统计第二个对答案的贡献,没有什么是现成的,一切都需要重新求解。

但如果我们从 aia_i 等于 11 的位置开始求解呢?

这时候,其它所有的显然都比它大,不需要求解,而后面再求解的数显然都比这个要大,所以遇到这一个直接跳过就好了。

细品一下这一段话,我们其实是利用了原先求解的结果,去掉了不必要的求解。因为原先做法中,用 ST 表就是为了跳过所有比当前这个小的,但用从小到大的求解顺序,只需要把原来求到过的都跳过就行了,不需要重新求解,所以降低了时间复杂度。

每次更新时,比当前更小的都被跳过了,直接调用没跳过的就是大于当前的,所以这样是对的。

具体的实现使用双向链表 LiL_i 表示这个去掉所有跳过的左边是什么,RiR_i 表示去掉跳过的 ii 的右边是什么。最后更新一下,跳过当前这一个就可以了。

代码:

#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;
}

2020 行,从各个方面吊打原来的 6060 行做法,妙不妙?