C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++でデータストリームを互いに素な区間として要約する方法

整数からなるデータストリーム a1, a2, ..., an, ... が順番に入力される状況を想定しましょう。この問題では、それまでに入力されたすべての数値を、互いに重なり合わない「互いに素な区間」のリストとして要約する必要があります。

たとえば、入力される整数が 1, 3, 8, 2, 7, ... である場合、各時点での要約結果は以下のように変化していきます。

  • [1, 1]
  • [1, 1], [3, 3]
  • [1, 1], [3, 3], [8, 8]
  • [1, 3], [8, 8](2 が追加され、[1,1] と [3,3] が連結される)
  • [1, 3], [7, 8](7 が追加され、[8,8] が [7,8] に拡張される)

解法のアプローチ

この問題は、C++ の std::set(自動的に昇順ソートされる集合)を利用することで効率的に解決できます。set によって数値が常に整列された状態で保持されるため、隣接する数値(= 連続した区間)を容易に検出できるのがポイントです。具体的な手順は以下の通りです。

  • nums という名前の int 型 set を用意する
  • コンストラクタで low := -inf、high := +inf として初期化する
  • addNum(num): 引数で受け取った num を set nums に挿入する
  • getIntervals(): 以下の処理を実行する
    • 2次元配列(ベクター)ret を定義する
    • it を set nums の先頭要素を指すイテレータとする
    • it が有効な要素を指している間、以下を繰り返す
      • x := it が指す値
      • ret が空であるか、「ret の最後の区間の終点 + 1 < x」が成り立つ場合は、区間 {x, x} を ret の末尾に追加する
      • それ以外の場合(x が直前の区間に連続している場合)は、ret の最後の区間の終点を 1 増やす
      • it を次の要素へ進める
    • 最後に ret を返す

C++ 実装例

より深く理解するために、実際の実装コードを見てみましょう。

#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<vector<auto>> v){
   cout << "[";
   for(int i = 0; i<v.size(); i++){
      cout << "[";
      for(int j = 0; j <v[i].size(); j++){
         cout << v[i][j] << ", ";
      }
      cout << "],";
   }
   cout << "]"<<endl;
}
class SummaryRanges {
   public:
   set <int> nums;
   int low, high;
   SummaryRanges() {
      low = INT_MAX;
      high = INT_MIN;
   }
   void addNum(int val) {
      nums.insert(val);
   }
   vector<vector<int>> getIntervals() {
      vector < vector <int> > ret;
      set <int> :: iterator it = nums.begin();
      while(it != nums.end()){
         int x = *it;
         if(ret.empty() || ret.back()[1] + 1 < x){
            ret.push_back({x, x});
         } else {
            ret.back()[1]++;
         }
         it++;
      }
      return ret;
   }
};
main(){
   SummaryRanges ob;
   ob.addNum(1);
   print_vector(ob.getIntervals());
   ob.addNum(3);
   print_vector(ob.getIntervals());
   ob.addNum(8);
   print_vector(ob.getIntervals());
   ob.addNum(2);
   print_vector(ob.getIntervals());
   ob.addNum(7);
   print_vector(ob.getIntervals());
}

入力

クラスを初期化した後、要素を 1 つずつ addNum で追加し、その都度 getIntervals を呼び出して区間の状態を確認します。今回追加される要素は [1, 3, 8, 2, 7] です。

出力

[[1, 1]]
[[1, 1],[3, 3]]
[[1, 1],[3, 3],[8, 8]]
[[1, 3],[8, 8]]
[[1, 3],[7, 8]]

計算量とポイント

std::set は平衡二分探索木(赤黒木など)で実装されているため、addNum(挿入)は O(log n)、getIntervals(全要素の走査)は O(n) の計算量で動作します(ここで n はそれまでに追加された要素の総数)。また、set は要素を自動的に一意化しソートしてくれるため、同じ値が何度追加されても正しく動作する点も、この設計の大きな利点といえます。

  1. C++である区間が別の区間を含むかどうかを判定するアルゴリズム

    問題概要2次元の区間リストが与えられます。各区間は [start, end](開始値と終了値)の2つの値で表されます。このリストの中に、別の区間を完全に含んでいる区間が存在するかどうかを判定するのがこの問題の目的です。たとえば、入力が [[2,4],[5,11],[5,9],[10,10]] の場合を見てみましょう。[5,11] という区間が [5,9] を含んでいるため、出力は true(真) となります。解決のためのアプローチこの問題は、区間を適切にソートしてから線形走査を行うことで効率的に解くことができます。具体的な手順は以下の通りです。まず、配列 v を「終了値の昇順」でソートします(

  2. C++で解く「ジャンプゲームV」:メモ化再帰による最大訪問インデックス数の求め方

    問題の概要整数型の配列 arr と整数 d が与えられます。1ステップごとに、インデックス i から次の場所へジャンプできます。右方向: i + x(ただし i + x < n、かつ x は 1 以上 d 以下)左方向: i - x(ただし i - x >= 0、かつ x は 1 以上 d 以下)ここで n は配列のサイズです。さらに重要な制約として、インデックス i から j へジャンプできるのは、arr[i] > arr[j] であり、かつ i と j の間にあるすべてのインデックス k に対して arr[i] > arr[k] を満たす場合のみです。つまり、より低