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

JavaScriptで指定範囲内の区間和(Range Sum)を持つペアをカウントする方法


範囲和(Range Sum)とは?

範囲和 rangeSum(i, j) とは、配列のインデックス i から j(i ≤ j、両端を含む)までの要素の合計のことです。

問題の概要

整数の配列 arr を第1引数に、2つの数値 upper と lower を第2・第3引数として受け取るJavaScript関数を作成します。

この関数は、範囲 [lower, upper](両端を含む)に収まる区間和の個数を返す必要があります。

入力例

const arr = [1, 4, 3];
const upper = 5;
const lower = 2;

出力例

この入力に対して、期待される出力は次のとおりです。

const output = 3;

すべての区間和を列挙して確認してみましょう。

  • rangeSum(0, 0) = 1 … 範囲外
  • rangeSum(0, 1) = 5 … 範囲内 ○
  • rangeSum(0, 2) = 8 … 範囲外
  • rangeSum(1, 1) = 4 … 範囲内 ○
  • rangeSum(1, 2) = 7 … 範囲外
  • rangeSum(2, 2) = 3 … 範囲内 ○

条件を満たすのは3つなので、答えは 3 になります。

解法のポイント:累積和と二分探索

すべての区間 (i, j) を素直に調べると O(n²) の計算量が必要になり、配列が長い場合には非効率です。そこで、次の2つのテクニックを組み合わせます。

  1. 累積和の利用: 区間和 S(i, j) は「位置 j までの累積和 − 位置 i−1 までの累積和」と表せます。つまり本問題は、「ある累積和と別の累積和の差が [lower, upper] に入る組み合わせを数える」問題に言い換えられます。
  2. 二分探索: それまでに現れた累積和を常にソート済みの状態で保持しておけば、条件を満たす要素の個数を二分探索で O(log n) で求められます。

コード例

const arr = [1, 4, 3];
const upper = 5;
const lower = 2;
const countRangeSum = (arr = [], lower, upper) => {
   const sums = [0];
   let res = 0;
   let last = 0;
   let firstge = value => {
      let l = 0, r = sums.length, m;
      do {
         m = Math.floor((r + l) / 2);
         sums[m] < value ? l = m : r = m;
      } while (r >= l + 2);
      while (r > 0 && sums[r - 1] >= value ) {
         r -= 1;
      }
      return r;
   };
   arr.forEach(num => {
      last += num;
      res += firstge(last - lower + 1) - firstge(last - upper);
      sums.splice(firstge(last), 0, last);
   });
   return res;
};
console.log(countRangeSum(arr, lower, upper));

実行結果

コンソールには次のように出力されます。

3

コードの解説

  • sums:それまでに登場した累積和を昇順に保持する配列です。初期値を [0] にすることで、「配列の先頭から始まる区間」も正しく数えられるようになります。
  • firstge(value):二分探索により「value 以上の要素が最初に現れる位置」、すなわち「value 未満の要素の個数」を返すヘルパー関数です。
  • last:現在の位置までの累積和を保持する変数です。
  • res += firstge(last - lower + 1) - firstge(last - upper):「last − 過去の累積和 ≥ lower」を満たす個数から「last − 過去の累積和 > upper」を満たす個数を差し引くことで、lower ≤ 区間和 ≤ upper を満たす組み合わせだけを数えています。
  • sums.splice(firstge(last), 0, last):現在の累積和を適切な位置に挿入し、sums を常にソート済みの状態に保ちます。

計算量の目安

二分探索そのものは O(log n) ですが、splice による挿入は最大 O(n) のコストがかかるため、この実装全体の計算量は O(n²) になります。より大きな配列を扱いたい場合は、マージソートの併合処理の中で条件を満たすペアを数える手法(計算量 O(n log n))を採用すると効果的です。


  1. JavaScriptで一致する部分文字列(部分列)の数を効率的に数える方法

    問題JavaScriptで、第1引数に文字列 str、第2引数に文字列の配列 arr を受け取る関数を作成します。この関数は、配列内の各要素 arr[i] のうち、文字列 str の「部分列」となっているものの個数を数えて返す必要があります。ここでの「部分列」とは、元の文字列から0個以上の文字を削除して得られる文字列であり、必ずしも連続している必要はない点に注意してください。これは「部分文字列」とは異なる概念です。例として、次のような入力が与えられた場合を考えてみましょう。入力const str = klmnop; const arr = [k, ll, klp, klo];出力const o

  2. JavaScriptでMapSumクラスを実装する:トライ木を使った接頭辞の合計計算

    問題 本記事では、insert メソッドと sum メソッドを持つ MapSum クラスを実装していきます。 insert(key, val): キー(文字列)と値(整数)のペアを受け取ります。同じキーがすでに存在する場合は、既存のキーと値のペアが新しい値で上書きされます。 sum(prefix): 接頭辞(プレフィックス)を表す文字列を受け取り、その接頭辞で始まるすべてのキーに対応する値の合計を返します。 解決のアプローチ:トライ木(接頭辞木) この問題はトライ木(Trie/接頭辞木)というデータ構造を使うことで効率的に解くことができます。各ノードは1文字を保持し、子ノードへの参照をオ