Leetcode:單調棧_可見山峰問題

一隻老風鈴發表於2020-12-12

題目描述


一個不含有負數的陣列可以代表一圈環形山,每個位置的值代表山的高度。比如,{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,請問有多少對山峰能夠相互看見?

輸入
5
3 1 2 4 5
輸出
7

思路

樸素的做法是,研究任意兩個點之間是否可以互相看見,需要順時針、逆時針的研究中間的點是否遮擋。

採取單調棧的思路,維護一個遞減的單調棧,初始化最大的元素入棧,從下一個元素開始,遍歷一圈(假設所有的點不重複):

  • 如果新來的元素比當前棧頂更小,那麼直接入棧。
  • 如果新來的元素比當前棧頂更大,那麼彈出棧頂,直到棧頂比當前元素大。

彈出過程,每彈出一個k,容易知道k與新來的元素一定可以相互看見(中間元素更小或者沒有元素),同時k與彈出k後的棧頂一定可以相互看見(直接相鄰)。因此count++

在這裡插入圖片描述

遍歷結束後,棧中仍然剩餘一定的元素:
若剩餘2個以上,那麼從第三大開始的每個元素,至少與最大,次大元素相互可見。
若只剩餘2個,那麼這兩個相互可見。

在這裡插入圖片描述

進階:存在重複的點

若存在重複的點,那麼需要注意的問題:棧中每個元素不再是值,而是pair<值,數量>

①第一階段:每個元素入棧

同樣的首先,最大值入棧 stack.push(pair(maxvalue,1));

如果新來的元素與棧頂相等:那麼棧頂元素的個數+1
如果新來的元素比棧頂更小:那麼壓入棧頂stack.push(pair(newvalue,1))
如果新來的元素比棧頂更大:那麼不斷彈出棧頂中比新來元素更小的值

因為彈出的一個數值value的數可能存在重複,假設重複的個數有k個,那麼這k個可以望見的點總數:
K2+K(K-1)/2 即:

  • 每個元素可以與新來元素相互看見,可以與彈出value後棧頂元素相互看見
  • 重複的元素相互之間兩兩可以相互看見,即組合數
    在這裡插入圖片描述

注意,刪除元素後,新來的元素可能與棧頂相等,那麼top.count++

②第二階段:清算棧剩餘元素

棧中可能剩餘一定的元素(至少最大值一定還在其中),那麼可能有3大型別:

第三大及往後元素:每個元素一定與其棧中左右相鄰元素相互看見(最小那個元素順時針可以與最大值相互看見)

第二大元素:
若最大值元素個數超過2:那麼每個元素都可以與2個不同的最大值相互看見(順時針和逆時針)
若最大值元素個數為1:那麼每個元素順時針,逆時針看見的最大值為同一個

在這裡插入圖片描述

最大值元素,只能相互之間相互看見(因為採取的小看大的計數方式:否則計數重複)

另外每種型別內部,可以兩兩相互看見:組合數計算

程式碼實現

int main()
{
    int n;
    cin>>n;
    vector<int> arr(n);
    stack<pair<int,int>> stk;
    int _max=-1;
    int _index=-1;
    for(int i=0;i<n;i++)
    {
        cin>>arr[i];
        if(arr[i]>_max)
        {
            _max=arr[i];
            _index=i;
        }
    }
    //開始從index開始向next方向遍歷一圈
    long long count=0;
    stk.push(pair<int,int>(arr[_index],1));//將(最大值,1)入棧
    for(int i=1;i<n;i++) //遍歷一圈
    {
        int index=(_index+i)%n;//當前的研究節點
         while(stk.top().first<arr[index])
        {
                int num=stk.top().second;//彈出的這個中間值的數量
                count+=2*num+num*(num-1)/2;//每個元素都可以和左、右 相互看見+它們內部兩兩相互看見
                stk.pop();//彈出它們(因為被兩個更大的包圍=》不可能在與兩堵牆之外的相望)
               
        }
        if(stk.top().first==arr[index]) //新來的值和棧頂的值相等
        {
            stk.top().second++;
            
        }
        else  //新來的值更大 那麼彈出中間那些節點
        {
           stk.push(pair<int,int>(arr[index],1)); 
        }
    }
    //結束之後  可能還有剩餘 (至少最大的還包含在其中)
    while(stk.size()>2)  //在兩個較大值之間還有較小值
    {
         int num=stk.top().second;//彈出的這個中間值的數量
         count+=2*num+num*(num-1)/2;//每個元素都可以和左、右 相互看見+它們內部兩兩相互看見
         stk.pop();//彈出它們 
    }
    if(stk.size()==2) //還剩兩個
    {
        int num=stk.top().second;
        stk.pop();
        int num2=stk.top().second;
        if(num2>1) //存在兩個以上的最大值  
            count+=2*num+num*(num-1)/2;
        else
        {
            count+=num+num*(num-1)/2;
        } 
    }
    if(stk.size()>0)
    {
        int num=stk.top().second;
        count+=num*(num-1)/2;
    } 
    cout<<count<<endl;      
}

相關文章