首页 > 试题广场 >

有效序列的数量

[编程题]有效序列的数量
  • 热度指数: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]
贴个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)

单调栈

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

正序遍历

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)