代表的な面接トピック

コーディング面接:区間加算と区間和のための遅延伝播セグメント木の構築方法

コーディング難しい
Offer.cc 編集チーム公開日 更新日

質問

オンラインの区間加算および区間和の操作を実装してください。遅延タグがいつpushされるか、計算量、および正当性を損なう境界ケースについて説明してください。

課題設定とスコープ

長さ n の整数配列が与えられたとき、閉区間 [l, r] 内のすべての値に delta を加算する操作と、[l, r] の総和を返す操作の2つのオンライン操作を処理します。目標は、O(n) の構築、操作あたり O(log n)、追加空間 O(n) です。区間は閉区間であり、delta は負の値を取り得ること、要求されない限り永続化はスコープ外であることを明記します。

面接官が見ているポイント

まず不変条件を定義します。tree[p] は常にそのノード区間の真の総和であり、lazy[p] はその総和にはすでに含まれているものの子ノードにはまだ適用されていない一様な加算値です。優れた回答では、完全にカバーされたノードのみを更新し、部分的な探索の前にpushを行い、加算値に対象区間の長さを乗算します。

コーディング前の確認事項

  1. 更新操作は加算、代入、あるいは区間最小値のいずれですか?これらは遅延タグの合成規則が異なります。
  2. 集約値は総和、最小値、最大値のどれですか?ノードのマージ方法とタグの計算式が変わります。
  3. 区間は閉区間ですか?この回答では閉区間 [l, r] を使用します。半開区間の場合は一貫した分割と長さの計算が必要です。
  4. 値はどの程度大きくなりますか?tree[p] + delta * length は32ビット整数をオーバーフローする可能性があるため、より広い型を選択します。

推奨解法と導出

暗黙的な二分木を配列内に保持します。ノード [lo, hi]mid[lo, mid][mid+1, hi] に分割されます。完全にカバーされた更新では、tree[p]delta * (hi-lo+1) を加算し、lazy[p]delta を累積します。子ノードの値は必要になるまでそのままにしておくことができます。

python
class LazySumTree:
    def __init__(self, values):
        self.n = len(values)
        self.tree = [0] * (4 * max(1, self.n))
        self.lazy = [0] * len(self.tree)
        if self.n:
            self._build(1, 0, self.n - 1, values)

    def _apply(self, p, lo, hi, delta):
        self.tree[p] += delta * (hi - lo + 1)
        self.lazy[p] += delta

    def _push(self, p, lo, hi):
        if self.lazy[p] == 0 or lo == hi:
            return
        mid = (lo + hi) // 2
        d = self.lazy[p]
        self._apply(p * 2, lo, mid, d)
        self._apply(p * 2 + 1, mid + 1, hi, d)
        self.lazy[p] = 0

同じ不変条件のもとで再帰的に addsum を完成させます。完全カバー時は _apply を呼び出し、部分カバー時はその前に _push を呼び出します。復帰後に子ノードから親ノードを再計算します。各レベルで訪問する境界ノードは定数個のみであるため、更新とクエリは O(log n) であり、記憶容量は O(n) です。

代替案とトレードオフ

1点更新とプレフィックス和のみであれば、Fenwick木の方がコードが短く定数倍も小さくなります。オフラインの区間加算とそれに続く1回の最終読み取りだけであれば、差分配列の方が単純です。遅延伝播セグメント木がその複雑さに見合う価値を発揮するのは、オンラインの区間更新と区間集約が共存する場合です。区間代入を行う場合は、「代入タグが存在するか」のフラグと明示的な優先順位規則が必要です。新しい代入は古い代入と加算タグを置き換え、その後の加算は代入の後に累積されます。

障害モード、境界条件、反例

  • 区間の長さを掛け忘れると、[2, 5] への3の加算が総和を12ではなく3しか増やさなくなります。
  • 完全カバー後に再帰を行うと遅延伝播が失われ、更新が重複して適用される可能性があります。
  • push後にタグをクリアし忘れると、次回の訪問時に同じ加算値が再度適用されてしまいます。
  • 部分更新後に親ノードを再計算しないと、その後の完全カバークエリで古い値が返されます。
  • 閉区間と半開区間を混同すると、1要素のズレや mid+1 のエラーが発生します。空配列、l > r、境界における n=0 で検証してください。

テストと検証のチェックリスト

愚直な配列をオラクルとして使用し、ランダムな更新とクエリを生成して、各操作後に比較します。1要素の区間、全体区間、両端の境界、負のデルタ、重複する範囲への更新、すべての値が等しいケースを含めます。再帰呼び出しの後に親が子の合計と等しくなっていることをアサートし、大きな入力に対して選択した整数型がオーバーフローしないことを確認します。非再帰(イテレーティブ)構成を実装する場合は、等価性テストを追加してください。

フォローアップの質問

区間代入と区間加算を共存させるにはどうすればよいですか?

ノードごとにオプションの代入タグと加算タグを保持します。新しい代入は古い代入と加算の両方を上書きし、新しい加算は代入の後に累積されます。まず代入をpushし、次に加算をpushします。この順序が正当性の規則となります。

区間最小値をサポートするにはどうすればよいですか?

総和の代わりに区間の最小値を保持します。区間加算はその最小値に依然として delta を加えるだけなので、遅延タグは単純なままです。区間 chmin や chmax をサポートするには、segment-tree beats などのより高度な不変条件が必要です。

過去の履歴バージョンを公開するにはどうすればよいですか?

永続セグメント木を使用します。更新パスに沿ったノードを複製し、変更のない部分木は共有します。各更新は約 O(log n) 個のノードを複製し、ルートポインタがバージョンを識別するため、メモリ空間は更新回数に応じて増加します。

公開情報ソース

関連する質問

関連面接ツール

コーディング問題にはスクリーンショットを使用

問題をキャプチャし、制約条件、解法アプローチ、コード、エッジケース、計算量の順に進めます。

ツールを見る