[モノトーン スタック] 配列の左右で、自分よりも大きいか小さい最初の数字を見つける

1.「モノトーンスタック」のデータ構造について

単調なスタックとは、内部がスタックの下部からスタックの上部まで単調性を満たすスタック構造を指します。

実際、モノトニックスタックは「スタック+単調性を維持」です。

1.1 プッシュ操作

ここで、単調スタックは、スタックの一番下から一番上まで単調に減少するスタックであると仮定されます。不一致を避けるために、単調増加および単調減少は、スタックの最下部からスタックの最上位への順序を参照し、後で説明されません。

モノトニック スタックに要素を挿入する場合、要素をポップしてスタック内の単調性を確保する必要がある場合があります。

挿入する要素 x がスタックの一番上の要素よりも小さい場合、x はスタック内のどの要素よりも小さいことを意味するため、他の操作は必要なく、直接挿入するだけです。
挿入する要素 x がスタックの一番上にある要素よりも大きい場合、要素 x を挿入した後、スタックの単調性を確保するために一部の要素をポップする必要があることを意味します。
挿入操作の基本的なテンプレート コードは次のとおりです。

// insert element x in mono_stack
void insert(stack<int>& mono_stack, int x) {
    
    
  while (!mono_stack.empty() && mono_stack() > x) {
    
    
    // operations #1
    mono_stack.pop();
  }
  // operations #2
  mono_stack.push(x);
}

1.2 モノトニック スタックを維持するときに生成される要素の関係

上記のコードには、追加の操作を追加できる場所が 2 つあります (つまり、操作 #1 と操作 #2)。スタックの以前の挿入と使用が基本である場合、追加の 2 つの操作位置で生成される要素の関係は、モノトニック スタックの本質です

モノトニック スタックには、トラバースされた要素がスタックにプッシュされるとき (操作 #2 に対応) と、スタック内の要素がポップされるとき (操作 #1 に対応) の2 つの追加操作があります。この一連の操作は、実際には、要素がスタックにプッシュされるときと、要素がスタックからポップされるときの2 つの部分に分けることができますたとえば、要素をスタックにプッシュする場合、スタック内の単調性が満たされないことがわかり、一連の要素をポップしてからスタックにプッシュする必要があります。このプロセスには、要素のプッシュと他の要素のポップの両方が含まれます。分析を簡素化するために、2 つの操作を別々に検討し、2 つの操作が発生したときに含まれる要素の関係を分析します。

以下の 2 つのケースについて、単調減少スタックを作成する例で説明します。トラバースされた要素がスタックにプッシュされると:

ここに画像の説明を挿入

上の図のように、要素1をスタックに追加したい場合、要素1は単調性を崩さずに直接スタックにプッシュできるため、直接スタックにプッシュできます。1 がスタックにプッシュされる前に、スタックの最小要素はスタックの一番上にある 3 であることがわかります。そのため、元の配列位置の左側のスタックにプッシュされようとしている要素は、 4. または 6 ではなく 3 になります (3 はスタック上で最小であるため、4 と 6 は 3 で「切り捨て」られています)。

したがって、抽象化できます。要素がスタックにプッシュされた後、単調スタックの単調性を破壊しない場合、スタックの一番上の要素は、元の配列位置の左にある最初の要素であり、それ自体よりも大きくなります

この結論は、スタックが単調減少し、配列が左から右にトラバースされる場合にのみ存在することを再度強調する必要があります。

要素がスタックからポップされるとき:

ここに画像の説明を挿入

上図に示すように、要素 5 をスタックにプッシュしようとすると、スタックにプッシュされた後に単調性が満たされないことがわかります。スタックの一番上の 1 は 5 未満なので、一番上の要素 1 をポップする必要があります。ここで、ポップされる 1 とプッシュされる 5 にも関係が含まれています。 (ここでは 1 と 5 の間に要素はありません)、プッシュされる 5 は、ポップされる 1 の元の配列位置の右側にある、それ自体よりも大きい最初の要素です。

この関係を抽象化します。プッシュされる要素がスタックにプッシュされる場合、プッシュされる要素は、プッシュされる要素の右側にある最初の要素であり、それ自体よりも大きくなります

この結論は、スタックが単調減少し、配列が左から右にトラバースされる場合にのみ存在します。

上記の例では、1 がスタックからポップされると、3 と 4 もスタックからポップされて、5 がスタックにプッシュされるようにする必要があります (元の配列には、3 と 5 の間、および 4 と 4 の間に要素はありません)。単調性を打破するには 5)。5 は、3 と 4 の右側でそれ自体よりも大きい最初の値であることがわかります。次に 5 がスタックにプッシュされます. このとき、スタック内の要素 6 は、元の配列の左側にある 5 の最初の値であり、それ自体よりも大きい値です。これらは、上で抽象化された 2 つのプロパティも検証します。

1.3 まとめ

次に、モノトニック スタックのプロパティをまとめます。単調に減少するスタックを維持しながら、配列を左から右にトラバースすると、次のようになります。

  • 1 つ目は単調性です。スタック内の要素は単調性を満たしますが、これは大きな違いにはなりません。
  • 要素が単調スタックの単調性を破壊することなくスタックにプッシュされる場合、スタックの一番上にある要素は、プッシュされる元の配列の位置 [左] でそれ自体よりも大きい最初の要素です。
  • プッシュされる要素はスタックにプッシュされるため、要素がポップされると、プッシュされる要素は、ポップされた要素の [右側] の最初の要素になります。

上記はすべて単調に減少するスタックです. 単調に増加するスタックに直面した場合, それらは次のように直接拡張されます:

  • 要素が単調スタックの単調性を破壊することなくスタックにプッシュされる場合、スタックの一番上にある要素は、スタックにプッシュされる要素であり、位置 [左] 元の配列の。
  • プッシュされる要素はスタックにプッシュされるため、要素がポップされると、プッシュされる要素は、ポップされた要素の [右側] の最初の要素です。

上記のプロパティから、モノトニック スタックの役割は、最終的なスタック自体ではなく、スタックのプロセスで取得した情報を維持することであることがわかります。したがって、要件に応じて、操作 1 と操作 2 で異なる情報を取得できます。

第二に、モノトニックスタックを使用する際の基本的な問題

2.1 基本的な質問

質問1:

指定された整数配列 nums について、各要素の右側にあるそれ自体よりも大きい最初の数値の添え字を見つけます。そうでない場合は、-1 を埋めます

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[2, 2, 3, -1, 5, -1, -1]

分析:
先ほど得たモノトニック スタック機能によると、次のようになります。

  • 自分よりも大きな要素の添え字に関心があるため、単調減少する stackを維持します。
  • 右隣の要素の最初の添え字が自分よりも大きいのは、スタックをポップしたときにしか得られない情報なので、そのときにプッシュする要素の情報を取得すればよい。要素がポップされる前. これはポップされる要素です. それ自体よりも大きい右側の最初の要素.
  • 要素がスタックからポップされていない場合、それはそれ自体より大きい要素がないことを意味するため、初期化中に -1 を割り当てることができます。

上記の簡単な分析の後、答えは簡単に得られます。

コード:

#include <iostream>
#include <stack>
#include <vector>
using namespace std;

// 问题1
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] > nums[st.top()]) {
    
    
      ret[st.top()] = i;
      st.pop();
    }
    st.push(i);
  }
  return ret;
}

void printVec(vector<int> vec) {
    
    
  for (auto& x : vec) {
    
    
    cout << x << "  ";
  }
  cout << endl;
}

int main() {
    
    
  vector<int> nums = {
    
    2, 1, 5, 6, 2, 3, 1};
  auto ret = solve(nums);
  printVec(ret);
  return 0;
}

2.2 問題の拡張

「右側の一人称は自分よりも大きい」という問題が解けると、「右側の一人称は自分よりも小さい」「左側の一人称は自分よりも小さい」という 3 つのアナロジー問題を簡単に拡張できます。側は自分よりも大きい」、「左側の最初のものは自分よりも小さい」. 詳細は次のとおりです。

質問 2:
与えられた整数配列 nums について、各要素の右側にあるそれ自体より小さい最初の数値の添え字を見つけます。そうでない場合は、-1 を記入してください

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[1, -1, 4, 4, 6, 6, -1]

分析:

  • 自分よりも小さい要素の添え字に関心があるため、単調に増加するスタックを維持します。
  • 右側の要素の最初の添え字のうち、それよりも大きいものを見つけるには、これはスタックをポップするときにのみ取得できる情報であるため、要素をポップする場合は、要素の情報のみを取得する必要があります。これはポップされる要素です. それ自体よりも小さい右側の最初の要素.
    コード:

問題1と比較すると、popの判定を>から<に変更するだけです。

// 问题2
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] < nums[st.top()]) {
    
    
      ret[st.top()] = i;
      st.pop();
    }
    st.push(i);
  }
  return ret;
}

質問 3:

指定された整数配列 nums について、各要素の左側にある最初の数値の添字のうち、それ自体よりも大きいものを見つけます。そうでない場合は、-1 を入力します

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[-1, 0, -1, -1, 3, 3, 5]

分析:

  • 自分よりも大きな要素の添え字に関心があるため、単調に減少するスタックを維持します。
  • 左の最初の要素の自分よりも大きい添字を見つけるには、スタックにプッシュするときに取得できる情報です。したがって、要素をスタックに正常にプッシュできる場合は、プッシュする要素と、プッシュする前のスタックの最上位要素を取得できます。スタックの一番上の要素は、プッシュされた要素の左にある、それ自体よりも大きい最初の要素です。
  • 要素がプッシュされたときにスタックが空である場合、それはその要素より大きい添え字がないためです。

コード:

質問 1 のコードと比較すると、次の 3 つの変更点があります。

  • ポップ前の操作を削除します
  • ポップが配置されているループは >= に変更されます
  • プッシュの前に必要な操作を追加します (スタックが空にならないように注意してください)
// 问题3
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] >= nums[st.top()]) {
    
    
      st.pop();
    }
    ret[i] = st.empty() ? -1 : st.top();
    st.push(i);
  }
  return ret;
}

質問 4:

指定された整数配列 nums について、各要素の左側にあるそれ自体より小さい最初の数値の添字を見つけます。そうでない場合は、-1 を埋めます

example:
input:[2, 1, 5, 6, 2, 3, 1]
output:[-1, -1, 1, 2, 1, 4, 4]

分析:

  • 自分よりも小さい要素の添え字に関心があるため、単調に増加するスタックを維持します。
  • 左端の最初の要素の添え字のうち、自分より小さいものを見つけるには、スタックにプッシュするときに取得できる情報です。したがって、要素をスタックに正常にプッシュできる場合は、プッシュする要素と、プッシュする前のスタックの最上位要素を取得できます。スタックの一番上の要素は、プッシュされた要素の左にある、それ自体より小さい最初の要素です。
    コード:

問3と比べて、popが位置するループの判定を<=に変更するだけです。

// 问题4
vector<int> solve(vector<int>& nums) {
    
    
  int n = nums.size();
  vector<int> ret(n, -1);
  stack<int> st;
  for (int i = 0; i < n; ++i) {
    
    
    while (!st.empty() && nums[i] <= nums[st.top()]) {
    
    
      st.pop();
    }
    ret[i] = st.empty() ? -1 : st.top();
    st.push(i);
  }
  return ret;
}

上記の 4 つの非常に単純な質問を通じて、モノトニック スタックのコア動作を簡単に理解できます。

3. モノトニックスタックの使い方まとめ

これまでの説明と 4 つの質問を通して、モノトニック スタックの使用方法をまとめましょう。

ここに画像の説明を挿入

左右で自分よりも小さい要素を見つけ、単調に増加するスタックを使用し、スタックに置いたときに自分よりも小さい左側の要素を更新し、右側の最初の小さい要素を更新しますスタックから飛び出すときよりも。同様に、自分よりも大きい左右の要素を見つけるには、単調減少スタックを使用し、一番最近自分よりも大きい左側の要素をスタックに積んだときに更新し、更新します。スタックからポップアウトしたときに自分よりも大きい右側の最初の要素。

上記のルールの理由を理解するために、次のように考えることができます:要素のライフ サイクルは、要素がトラバースされるときにスタックにプッシュされ、スタックから飛び出す機会があるということです。他の要素にトラバースされるとき。要素がスタックにプッシュされたとき、要素の右側の情報は不明であるため、現在、左側の情報のみがわかっています (つまり、それ自体よりも大きい/小さい左側の最初の要素は、更新しました)。スタックをポップすると、右側の要素がスタックにプッシュされることが原因であることがわかっています。これは、右側の情報です (右側の最初の要素で、それ自体よりも大きい/小さいものを更新します) (スタックをポップするときに更新されない場合、チャンスはありません)。

上記はすべて左から右へのトラバースですが、右から左へのトラバースが許可されている場合、テーブルは次のようになります。

ここに画像の説明を挿入

元のテーブルを左から右にトラバースしただけで、スタッキングとポッピング操作の順序が変更されていることがわかります。この変化を理解するためには、トラバーサル方向と情報を取得する順序に注目する必要があります。右から左にトラバースする場合、トラバースされる要素については、要素とその右側の情報しかわかりません。したがって、このトラバースされた要素の場合、最初の操作がスタックをプッシュすることである場合、それ自体よりも大きいまたは小さい右側の最初の値が取得されます。2 番目の操作がスタックをポップする場合、要素の左側の最初の値のうち、それ自体よりも大きい値が取得されます (ポップは要素の左側の要素によって引き起こされ、この要素が最初の値です)それはそれ自身よりも大きい) 自身の大きな値または小さな値) .

  1. 参照
    [1] https://www.cnblogs.com/molinchn/p/14772025.html

おすすめ

転載: blog.csdn.net/jiaoyangwm/article/details/127455866