1 条题解

  • 0
    @ 2026-7-5 19:31:26

    提供两种做法,一种是依赖于单调栈和 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 行做法,妙不妙?

    • 1

    信息

    ID
    11740
    时间
    2000ms
    内存
    1024MiB
    难度
    10
    标签
    递交数
    2
    已通过
    1
    上传者