1. 程式人生 > 其它 >可見的山峰對數量(單調棧)

可見的山峰對數量(單調棧)

可見的山峰對數量(單調棧)

問題重述:

一個不含有負數的陣列可以代表一圈環形山,每個位置的值代表山的高度。比如,{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)(左右都有比當前高度山峰高的山峰,所以可見山峰對數為num

2,此外,相同高度的山峰也是可見山峰,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(棧不為空){
    //進行處理
}