首页 > 试题广场 >

有效序列的数量

[编程题]有效序列的数量
  • 热度指数:1976 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解
我们定义一个有效序列为:该序列两端的数一个为最小值,另一个为次小值。(即序列两端以外的数一定大于等于最左边的数且大于等于最右边的数)

现在给你一个序列 a ,想让你找到它的连续子序列中有多少个有效序列(比如 ,1 2 ,2 3,1 2 3 是序列 1 2 3 的连续子序列,但是 1 3 不是)

注:长度为 2 的子序列,一定为有效序列,长度为 1 的子序列,一定不是有效序列

输入描述:
第一行输入一个整数 n 代表这个序列的长度
接下来输入 n 个整数,a[i] 代表系列中第 i 个元素

对于 20% 的数据, 1 ≤ n ≤ 100
对于 70% 的数据, 1 ≤ n ≤ 3,000
对于 100% 的数据, 1 ≤ n ≤ 100,000

对于 100% 的数据, 1 ≤ a[i] ≤ 1,000,000,000


输出描述:
输出一个正整数表示有效序列的数量。
示例1

输入

4
1 3 1 2

输出

4

说明

一共有 4 组有效序列,分别为:
子序列[1,3] 因为长度为 2,一定为有效序列
子序列[1,3,1] 因为第2个数 “3” 大于第 1 个数和第 3 个数
子序列[3,1] 因为长度为 2,一定为有效序列
子序列[1,2] 因为长度为 2,一定为有效序列
示例2

输入

4
1 1 2 1

输出

5

说明

一共有6个长度不小于2的连续子序列,除了[1,1,2]以外,其他5个都是有效子序列
示例3

输入

7
1 4 2 5 7 1 3

输出

10

说明

一共有10组,分别为:
[1,4], [1,4,2], [1,4,2,5,7,1], [4,2], [2,5], [2,5,7,1], [5,7], [5,7,1], [7,1], [1,3]

单调栈

很不好想,需要根据业务来构建单调栈的单调逻辑。构建一个大压小的栈,保持栈底到栈顶是单调递增的,用一个例子来说明一下算法流程:

正序遍历

arr:[1,1,2,1]

stack:[]

i=0,考察以arr[0]结尾,且以arr[0]为最小值的有效序列数,由于arr[0]的左边没有元素,所以直接入栈(为了考虑重复数字,需要记录某个数字的重复次数)

stack:[node(val=1,times=1)]

i=1,考察以arr[1]结尾,且以arr[1]为最小值的有效序列数,由于arr[0]=stack.top.val,直接压栈(与栈顶元素相同,增加栈顶元素的出现次数)

stack:[node(val=1,times=2)]

i=2,考察以arr[2]结尾,且以arr[2]为最小值的有效序列数,由于arr[2]>stack.top.val,直接压栈

stack:[node(val=1,times=2),node(val=2,times=1)]

i=3,考察以arr[3]结尾,且以arr[3]为最小值的有效序列数,而此时arr[3]<stack.top.val,破坏单调性,开始弹栈,栈顶元素为2,且只有一个2,说明可以与arr[3]构成一个有效的序列[2,1](刚好是以arr[3]结尾,且arr[3]为最小值,2为次小值)。

弹栈之后发现栈顶为1,且1出现了两次,可以与arr[3]构成两个有效序列,一个是①:[1,2,1],另一个是②:[1,1,2,1]。①中左边的1是把栈顶的2弹出后的次大值(由于栈是大压小的单调性,所以弹出2之后,如果栈顶元素仍然不小于arr[3],则栈顶元素一定可以作为次小值);同理②弹出的1也可以作为次小值。而两个1是同时弹出的,所以直接累加上2就可以了,此时已经有了[2,1],[1,2,1],[1,1,2,1]三个有效序列。

这时候注意到弹出的node(val=1,times=2),其中的两个1也可以构成一个有效序列[1,1]。如果弹出的某个栈顶元素出现了n次,除了n个有效序列贡献给当前破坏单调性的那个元素,它本身也可以构成个有效序列,组合数表示选择两个数作为序列的左右端点。

倒序遍历

正序遍历完成后,还需要倒序遍历一遍,对于某个元素arr[i]往右边找次小值,同样的流程可以再抓出序列[1,2]。但在倒序遍历的过程中需要注意,相同元素的有效序列不需要再计算了,因为正序遍历时已经算过了。
import java.io.*;
import java.util.*;

class Node {
    public int value;
    public int times;
    public Node(int v, int t) {
        value = v;
        times = t;
    }
}

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        if(n < 2){
            System.out.println(0);
            return;
        }
        String[] strs = br.readLine().split(" ");
        int[] arr = new int[n];
        for(int i = 0; i < n; i++){
            arr[i] = Integer.parseInt(strs[i]);
        }
        long ans = 0;
        Stack<Node> stack = new Stack<>();
        for(int i = 0; i < n; i++){
            while(!stack.isEmpty() && stack.peek().value > arr[i]){
                Node topNode = stack.pop();
                ans += topNode.times + cn2(topNode.times);
            }
            if(!stack.isEmpty() && arr[i] == stack.peek().value){
                stack.peek().times++;
            }else{
                stack.push(new Node(arr[i], 1));
            }
        }
        while(!stack.isEmpty()){
            ans += cn2(stack.pop().times);
        }
        for(int i = n - 1; i >= 0; i--){
            while(!stack.isEmpty() && stack.peek().value > arr[i]){
                Node topNode = stack.pop();
                ans += topNode.times;
            }
            if(!stack.isEmpty() && arr[i] == stack.peek().value){
                stack.peek().times++;
            }else{
                stack.push(new Node(arr[i], 1));
            }
        }
        System.out.println(ans);
    }
    
    private static long cn2(int n) {
        return (long)n*(n - 1) >> 1;
    }
}

复杂度分析

单调栈撸了两遍数组,每次遍历时每个数最多只会经历一次压栈、一次弹栈,所以时间复杂度为O(N);开辟了一个栈空间,额外空间复杂度为O(N)。
编辑于 2022-04-14 12:19:54 回复(2)

思路:单调栈+dp

首先采用dp思想分割问题,考虑右端点为i时符合题意子序列的数量,将i遍历一遍即可。那么,如何得到右端点为i时有效子序列数量呢?
如果O(n)暴力枚举,最终复杂度就达到O(n^2)超时。
实际上,如果找到当前固定右端点i的最长有效子序列,其左端点如果是最小值,那么只要该有效子序列中大于左端点且单调不减的值都可以作为新有效子序列的左端点,而如果左端点>=右端点的值,那么>=左端点且单调不减的值都可以作为新有效子序列的左端点。
因此我们只需找到固定右端点时,最远的左端点即可,而中间单调不减的值数量可以用单调栈的高度计算出来。
那么,最远左端点在哪里呢?其实就是单调栈中小于右端点的最大值,若不存在则是大于等于右端点的最小值。
我们用dp数组记录单调栈中每个值对应的最远左端点,如果当前右端点值比小于等于栈顶元素小则就去找dp数组记录的栈顶元素的左端点,否则说明最远左端点就是当前栈顶元素,在单调栈中跨越过元素的数量就是固定右端点i有效的子序列数量,注意到我们找最远左端点时,找的是严格小于当前右端点的值(除非找不到),而栈只需单调不减就行,因此仅在栈顶元素大于右端点时弹栈,相等时通过dp数组向前找,不弹栈,这个dp数组的含义和kmp数组的含义类似,这样就避免了单调栈中相等元素过多而重复遍历退化为O(n^2)的情况,向前找最远左端点时循环次数不超过弹栈次数+1,每个元素最多进出栈一次,因此最终时间复杂度O(n),空间复杂度O(n).
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <vector>

using namespace std;
const int maxsize = 100019;
int stk[maxsize], n, top = 0, dp[maxsize];
long long ans = 0;

int main()
{
	int i, x, h, j;
	cin >> n;
	dp[0] = -1;
	for (i = 0; i < n; i++)
	{
		cin >> x;
		h = top;
		while (top > 0 && stk[top - 1] > x)
		{
			top = dp[top - 1] + 1;
		}
		j = top - 1;
		if (j > -1 && stk[j] == x) j = dp[j];
		ans += h - ((j == -1) ? 0 : j);
		dp[top] = j;
		stk[top++] = x;
	}
	cout << ans << endl;
}


发表于 2022-06-15 18:04:19 回复(1)
脑子烧了,参考大佬改编的python版本:
def cn2(x):
    return x*(x-1)>>1
n=int(input())
a=list(map(int,input().split()))
ans=0
stk=[]
for i in range(n):
    while stk and a[i]<stk[-1][0]:
        node=stk.pop()
        ans+=node[1]+cn2(node[1])
    if stk and a[i]==stk[-1][0]:
        stk[-1][1]+=1
    else:
        stk.append([a[i],1])
while stk:
    ans+=cn2(stk.pop()[1])
for i in range(n-1,-1,-1):
    while stk and a[i]<stk[-1]:
        stk.pop()
        ans+=1
    stk.append(a[i])
print(ans)
发表于 2023-09-12 17:25:26 回复(0)
#include <stack>
#include<iostream>
typedef long long ll;
using namespace std;
int n, a[100005];
ll ans = 0;
int main() {
    int i, j;
    cin >> n;
    int ans = 0;
    for (i = 0; i < n; i++) cin >> a[i];
    // 记录一串递增数列,在出现足够小的值时可以用来组成有效序列
    stack<pair<int, int>> pre; //记录下前面出现的数字,以及数字出现次数。例如 11 455 1此时就能+2有效序列了。
    for (i = 0; i < n; i++) {
        while (!pre.empty() && pre.top().first > a[i]) { //如果栈内的值更大,那么需要出栈
            ans += pre.top().second;
            pre.pop();
        }
        if (pre.empty()) {
            pre.push(make_pair(a[i], 1));
        }
        else {
            if (pre.top().first == a[i]) {
                ans += pre.top().second;
                pre.top().second++;
                if (pre.size() > 1) { //如果pre大于1,那么就意味着左边还有更小的值可以创建有效序列 ,如 1 2 455 2
                    ans++;
                }
            }
            else {
                pre.push(make_pair(a[i], 1));
                ans++;
            }
        }
    }
    cout << ans;

}

发表于 2023-08-03 21:27:14 回复(0)
C++ 单调栈
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    long long ans = 0;
    cin >> n;
    vector<int> st;
    vector<int> s;
    for(int i = 0; i < n; ++i) {
        int num;
        cin >> num;        
        if(!st.empty()) {
            int b = 0, e = st.size() - 1;
            while(b < e) {
                int m = (b + e)/2;
                if(s[st[m]] < num) {
                    b = m + 1;
                } else {
                    e = m;
                }
            }
            while(b > 0 && s[st[b]] >= num) b--;
            if(b >= 0) {
                ans += st.size() - b;
            }
        }
        while(!st.empty() && s[st.back()] > num) {
            st.pop_back();
        }
        st.push_back(i);
        s.push_back(num);
    }
    cout << ans << endl;
}
// 64 位输出请用 printf("%lld")


发表于 2023-07-22 08:44:23 回复(0)
贴个Java纯单调栈解法。
  • 根据单调栈的知识,能联想到如果遇到一个小的数,那么在它前面比它大的数可以不要了,所以是单调递增栈。
  • 但最核心的难点在于,本题很难确定是严格的单调递增栈,还是非严格的单调递增栈。也就是说,遇到重复元素到底如何处理
  • 用几个test case尝试,比如当前栈是1,3,3,后面如果来了一个3,可以和3个数都组成有效序列。result要加上3,所以重复元素信息不能丢,为非严格的单调递增栈。但而不把3出栈,怎么知道前面有没有1呢,因为如果当前是3,3,没有1,当前result只用加2,所以3还是得都出栈,看是否为空,只不过丢掉的3都得重新入栈
  • 分析到这里,逻辑就很清晰了。该单调栈一定得记录重复元素信息,为非严格的,每次更新result的时候要出栈所有重复,看栈是否为空,然后加回重复。
  • 但是上述解法虽然可以解决,但就不满足单调栈的性质了,因为每个元素不只是被入栈出栈一遍,最差的情况有可能是n遍,复杂度到了O(n^2)
  • 用一个技巧,放入栈的元素改为2维数组,一个是元素,另一个记录其个数,这样重复出栈的时候只用判断的size是否大于1,而不用真的把该元素出栈了。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        
        int n = in.nextInt();
        in.nextLine();
        String[] str = in.nextLine().split(" ");
        int[] input = new int[n];

        for (int i = 0; i < str.length; i++) {
            input[i] = Integer.valueOf(str[i]);
        }

        Deque<int[]> stack = new ArrayDeque<>();
        long result = 0;

        for (int i = 0; i < input.length; i++) {
            int val = input[i];
            while (!stack.isEmpty() && val < stack.peekLast()[0]) {
                int[] cur = stack.pollLast();
                result += cur[1];
            }
            if (stack.isEmpty()) {
                stack.offerLast(new int[]{val, 1});
            } else {
                int[] cur = stack.peekLast();
                if (val == cur[0]) {
                    result += cur[1];
                    cur[1]++;
                    if (stack.size() > 1) {
                        result++;
                    }

                } else { // if val > cur[0]
                    result++;
                    stack.offerLast(new int[]{val, 1});
                }
            }
        }
        System.out.println(result);
    }
}


编辑于 2023-03-27 01:21:08 回复(0)
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;

int main() {
	int n;
	cin >> n;
	vector<int> nums(n, 0);
	vector<int> v_stack;
	long long ans = 0;
	int top = -1;
	for (int i = 0; i < n; ++i) {
		int x;
		cin >> x;
		nums[i] = x;
	}
	int h = 0;   //以i为右端点的符合条件子序列数量
	for (int i = 0; i < n; ++i) {
		if (top >= 0) {
			if (nums[v_stack[top]] >= nums[i]) {
				while (top >= 0 && nums[v_stack[top]] > nums[i]) {
					++h;
					v_stack.pop_back();
					--top;
				}
				if (top >= 0) {
					++h;
					if (nums[v_stack[top]] == nums[i]) {
						h += v_stack.size() - 1;
					}
				}
			}
		}
		v_stack.push_back(i); 
		++top;
		if (i > 0) {
			h = h ? h : 1;
			ans += h;
		}
		h = 0;
	}
    
	cout << ans << endl;
	return 0;
}
只遍历一次的单调栈,不知道为什么最后一个样例过不了,数值差了998,莫名其妙的,求解
\*****************************************************************************\
已解决:
加了个dp,
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;

int main() {
	int n;
	cin >> n;
	vector<int> nums(n, 0);
	vector<int> v_stack;
	vector<int> dp(n, 0);
	long long ans = 0;
	int top = -1;
	for (int i = 0; i < n; ++i) {
		int x;
		cin >> x;
		nums[i] = x;


	}
	int h = 0;
	for (int i = 0; i < n; ++i) {
		if (top >= 0) {
			if (nums[v_stack[top]] >= nums[i]) {
				while (top >= 0 && nums[v_stack[top]] > nums[i]) {

					++h;
					v_stack.pop_back();
					--top;
				}
				if (top >= 0) {
					if (nums[v_stack[top]] == nums[i]) {
						dp[i] = dp[v_stack[top]] + 1;
						h += dp[i];
						if(top >= dp[i]){
							++h;
						}
					}else{
						++h;
					}
				}
			}
		}
		v_stack.push_back(i);
		++top;
		if (i > 0) {
			h = h ? h : 1;
			ans += h;
		}
		h = 0;
	}
	cout << ans << endl;
	return 0;
}


编辑于 2022-08-22 23:26:22 回复(0)
// 单调栈实现
// 简述单调栈:
// 栈上任两个位置的元素i、j,表示(i,j)范围内的x,
// 对于任一x满足a[x] >= a[i]且a[x] >= a[j]

6/10的正确率,感觉思路应该是对的,但为什么没有完全正确,麻烦大佬指正!!!
#include<iostream>
#include<algorithm>
#include<stack>
#include<vector>
 
using namespace std;
 
int main()
{
    int n;
    vector<long> a;
    a.clear();
     
    cin >> n;
    for(int i = 0; i < n; i++)
    {
        long val;
        cin >> val;
        a.push_back(val);
    }
 
    // 只有两个元素,统计满足条件的情况
    // 所以下面只要考虑三个及三个元素的情况
    long count = n - 1;
    
    // 下标入栈
    stack<int> s;
    for(int j = 0; j < n; j++)
    {
        long val = a[j];
        
        while(!s.empty() && val < a[s.top()])
        {
            // 此过程,目的是
            // 维持单调栈,入栈的下标所对应的数据一定是单调递增(等于也算)
            // 即栈上任意两个下标i,j范围内的任一x下标满足:
            // a[x] >= a[i]且a[x] >= a[j] (j > i,i + 1 != j)
            // 同时其他元素出栈的时候,符合条件的情况也一并统计
            if(s.top() + 1 != j)
                count += 1;
            
            // 不符合条件的出栈
            s.pop();
        }
         
        if(!s.empty())
        {
            if(s.top() + 1 != j)
                // 那么的话,
                // 只要是栈上任两个的元素i,j(i + 1 == j)
                // 就有s.size()种情况以a[j]为最右侧,且满足条件。
                // 如果i + 1 == j,就是只有两个元素的情况,在一开始已经统计,无需重复统计
                count += s.size();
            else
            {
                // 特殊情况:
                // 栈上的元素对应的数据有可能是一样的
                // 这时候,要剔除只有两个元素的情况即可
                if(a[s.top()] == val)
                     count += s.size() -1;
            }
        }
         
        // j入栈
        s.push(j);
    }
     
    cout << count << endl;
    return 0;
}


编辑于 2022-05-30 01:06:48 回复(1)
c++版本单调栈!单调栈可以即使处理数组中出现过的每一个“升降结构”
当数据相同时需要特殊处理(所以我们定义一个node数据结构)
正序遍历处理一左多右
以及反向处理一右多左
#include<iostream>
#include<stack>
using namespace std;
int arr[100005];
struct Node{
    int value;
    int times;
};
long cn2(int x){
    return (long)x*(x-1)>>1;
}
int main(){
    int n;
    long ans=0;
    cin>>n;
    if(n<2) cout<<0<<endl;
    for(int i=0;i<n;i++)cin>>arr[i];
    stack<Node> stack;
    for(int i = 0; i < n; i++){
            while(!stack.empty() && stack.top().value > arr[i]){
                Node topNode;
                topNode = stack.top();
                stack.pop();
                ans += topNode.times + cn2(topNode.times);
            }
            if(!stack.empty() && arr[i] == stack.top().value){
                stack.top().times++;
            }else{
                Node neww;
                neww.value=arr[i];
                neww.times=1;
                stack.push(neww);
            }
        }
        while(!stack.empty()){
            ans += cn2(stack.top().times);
            stack.pop();
        }
    for(int i = n - 1; i >= 0; i--){
            while(!stack.empty() && stack.top().value > arr[i]){
                Node topNode;
                topNode = stack.top();
                stack.pop();
                ans += topNode.times;
            }
            if(!stack.empty() && arr[i] == stack.top().value){
                stack.top().times++;
            }else{
                Node neww;
                neww.value=arr[i];
                neww.times=1;
                stack.push(neww);
            }
        }
        cout<<ans<<endl;
    }

发表于 2022-05-08 11:37:04 回复(0)