可见的山峰对数量(单调栈)

可见的山峰对数量(单调栈)

问题重述:

一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度。比如,{3,1,2,4,5},{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山。3->1->2->4->5->3 方向叫作 next 方向(逆时针),3->5->4->2->1->3 方向叫作 last 方向(顺时针)。

山峰 A 和 山峰 B 能够相互看见的条件为:

  1. 如果 A 和 B 是同一座山,认为不能相互看见。
  2. 如果 A 和 B 是不同的山,并且在环中相邻,认为可以相互看见。
  3. 如果 A 和 B 是不同的山,并且在环中不相邻,假设两座山高度的最小值为 min。如果 A 通过 next 方向到 B 的途中没有高度比 min 大的山峰,或者 A 通过 last 方向到 B 的途中没有高度比 min 大的山峰,认为 A 和 B 可以相互看见。

问题如下: 给定一个含有负数可能有重复值的数组 arr,请问有多少对山峰能够相互看见?

输入描述:
第一行给出一个整数 n,表示山峰的数量。
以下一行 n 个整数表示各个山峰的高度。
输出描述:
输出一行表示答案。

示例1

输入
5
3 1 2 4 5
输出
7

问题分析:

这道题第一眼看到就会想到,我们可见山峰,肯定是当前山峰左右有大于自己的山峰,所以肯定是使用单调栈的解法来解决(栈内存放山峰高度(对应索引也可以index)和当前高度山峰数量num)。

这道题的难点在于分析山峰的数量。在使用单调栈弹出栈顶元素时,对于栈顶元素来说,他的下面时左边比他高的山峰,当前要加入的山峰是其右边比他高的山峰。弹出栈顶元素时,当前栈顶元素对应山峰对应可见山峰对数为:num2 + C(2,num)(左右都有比当前高度山峰高的山峰,所以可见山峰对数为num2,此外,相同高度的山峰也是可见山峰,num个山峰随机组合)

遍历完所有山峰以后,栈内一定还会存在山峰,我们需要处理它。处理过程分为三类,一类是当前山峰既不是栈内倒数第二的山峰,也不是栈内最后一个元素,此时将其弹出时,计算可见山峰对数和遍历时相同,可见山峰对数量为:num*2 + C(2,num)(因为当前山峰不是最后两个元素,因为山峰是环形的,所以顺时针方向和逆时针方向一边可以看到最高的山峰一边可以看到比自己高的另一座山峰)第二类是为栈内倒数第二的山峰,此时可见山峰数量就与栈底山峰数量有关了。如果最后的山峰数量等于1,那么可见山峰对数量为:num + C(2,num),如果最后的山峰数量大于1,那么可见山峰对的数量为:num*2 + C(2,num) (因为如果最后的山峰如果只有一个的话,往两边看到的是同一座山峰,所以乘1,如果大于1,往两边看到的不是同一座山峰,那么可见山峰对乘2)第三类是栈内最后一个山峰,此时向两侧看见的山峰都是和自己高度一样的山峰,所以,可见的山峰对数量为:C(2,num)。将所有的可见山峰对数量加起来就是我们要的结果

解法:

单调栈

解题:

代码:
package cn.basic.algorithm;

import java.util.Scanner;
import java.util.Stack;

/**
 * @author FOLDN
 * 获得一列环形山峰中可见山峰的对数,山峰长度可重复
 */
public class VisiblePeakNum {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = scanner.nextInt();
        }
        int visibleNum = getVisibleNum(arr);
        System.out.println(visibleNum);
    }
    public static int getVisibleNoRepeatNum(int[] arr){
        if (arr == null || arr.length < 2){
            return 0;
        }
        // 山峰大于两座,除去最高的两座山峰以外,其他的每一座山峰都至少可以看见这两座山峰,所以每一座山峰都有2对,因此共有2*(arr.length-2),最高的两座山峰之间是一对可见山峰
        return (arr.length*2-3);
    }
    public static int getVisibleNum(int[] arr){
        // 不满足题目要求则返回0
        if (arr == null || arr.length < 2){
            return 0;
        }
        // 首先找到arr中的最高山峰,记录下索引,从该索引开始遍历
        int maxIndex = 0;
        for (int i = 0,size = arr.length; i < size; i++) {
            maxIndex = arr[maxIndex]>arr[i] ? maxIndex : i;
        }
        // 创建一个单调栈,将最高山峰的索引加入栈中,栈从栈底到栈顶为递减
        Stack<Record> stack = new Stack<>();
        stack.push(new Record(maxIndex,1));
        // 获得下一个山峰的索引
        int index = getNextIndex(maxIndex,arr.length);
        int res = 0;
        // 遍历环形山峰数组,因为我们不知道开始位置以及结束位置,我们使用while循环,当回到一开始的位置时停止循环
        while (index != maxIndex){
            // 开始进行单调栈的循环
            // 当加入的山峰高度会破坏栈的单调性,弹出栈顶元素
            while (arr[stack.peek().index] < arr[index]){
                // 弹出栈顶元素后,由于栈顶元素中的山峰数量不同会有不同的可见山峰对
                int num = stack.pop().num;
                res += 2*num + getPeakNum(num);
            }
            // 通过上面的while循环,此时我们要加入的山峰不会对栈的单调性造成影响,可以直接加入
            // 此时我们需要考虑的是此时加入的山峰高度是否和此时的栈顶山峰高度一样,如果一样我们就直接增加栈顶Record的num就可以
            if(arr[stack.peek().index] == arr[index]){
                // 山峰高度相同,则增加栈顶元素的num值
                stack.peek().num++;
            }else {
                // 不相同则向栈中添加一个新的Record记录
                stack.push(new Record(index,1));
            }
            // 向栈中添加完元素后,我们需要更新索引,开始下一个山峰的判断
            index = getNextIndex(index,arr.length);
        }
        // 遍历完成后,将栈中剩余的山峰进行处理,有三个阶段
        // 第一个阶段,该山峰既不是倒数第一也不是倒数第二个元素
        while (stack.size() > 2){
            int num = stack.pop().num;
            res += 2*num + getPeakNum(num);
        }
        // 第二个阶段,该山峰是倒数第二个元素
        if (stack.size() == 2){
            int num = stack.pop().num;
            res += getPeakNum(num) + (stack.peek().num == 1 ? num : 2 * num);
        }
        // 第三个阶段,该山峰是倒数第一个元素
        if (stack.size() == 1){
            int num = stack.pop().num;
            res += getPeakNum(num);
        }
        return res;
    }
    // 获得环形数组的下一个索引值
    public static int getNextIndex(int index,int size){
        return (index+1)%size;
    }
    // 通过Record的num值获得可见山峰对数(只算相同高度的山峰之间)
    public static int getPeakNum(int num){
        return num == 1 ? 0:(num*(num-1)/2);
    }
    static class Record{
        public int index;
        public int num;
        public Record(int index,int num){
            this.index = index;
            this.num = num;
        }
    }
}

代码解析:

我们首先创建了一个记录类,用于记录山峰和山峰数量,将这个记录存放在单调栈中。我们首先得到最高的山峰放入栈中,从当前最高山峰开始环形遍历山峰。关于下一个索引,我们只需要根据循环队列的求下标来得到就可以。(当前索引加1然后对总长度求模即可得到下一个索引)。我们开始遍历,当下一个索引为我们循环一开始的索引时,停止循环,此时表示已经遍历了整个山峰数组。然后就是对单调栈的处理,当此时加入的山峰会破坏栈的单调性,弹出栈顶元素,直到加入山峰不会破坏单调性,加入山峰(需要进行条件判断,如果山峰高度和栈顶山峰高度一样,就将栈顶记录中的山峰数量加1,如果高度不同,则新建一个山峰记录加入栈中)。每次弹出山峰计算可见山峰对数量(计算方式问题分析已经说过)遍历完所有山峰后,因为一开始向栈内加入了最大山峰,栈内至少还会有一个元素(更大可能是有多个),我们需要处理剩余的山峰。此时分为三类计算(计算方法在问题分析中),计算完所有山峰,将所有的可见山峰对加起来就是我们要的结果。

总结:

单调栈:栈内元素按照规则排序,如果压入的元素不符合规则,则弹出队尾元素,直到满足规则。(只能操作栈顶)

单调栈模板:

for(对数组进行遍历,也可以使用while循环){
    while(栈不为空 && 当前要加入的元素破坏栈的单调性(我们可以根据这个条件设置栈的单调性) ){
        // 弹出栈顶元素,对栈顶元素进行处理(一般是和题目要求相结合,可以得到摸个结果)
        // 如果我们在遍历栈之前向栈内添加了最大元素(或者最小元素),我们就可以不需要判断栈不为空了
    }
    // 通过上面的循环,此时将元素加入栈内不会影响栈的单调性,直接加入栈
}
// 遍历完数组,此时栈内可能还有剩余的元素,我们对其进行处理
while(栈不为空){
    //进行处理
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值