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

【C++】区間のXORと総和が一致する(l, r)ペアの個数を二分探索で効率的に求める方法

問題概要

N個の要素を持つ配列Aが与えられます。このとき、次の条件を満たす整数のペア(l, r)の個数を求めてください。

A[l] XOR A[l+1] XOR … XOR A[r] = A[l] + A[l+1] + … + A[r]

つまり、「区間内の要素をXORした結果」と「区間内の要素を足し合わせた結果」が一致するような区間を数え上げる問題です。

例えば、入力が A = [2, 5, 4, 6] の場合、出力は 5 になります。これは、(1,1)、(2,2)、(3,3)、(4,4)、(1,2) の5つのペアが条件を満たすためです。

解法のポイント

すべての(l, r)の組み合わせを素直に調べるとO(N²)の計算量が必要になり、Nが大きい場合は現実的ではありません。そこで、次の2つの性質を利用します。

  • 区間の総和は常にXOR以上になる: 加算とXORの違いは「繰り上がりが発生するかどうか」だけです。したがって、任意の区間について 総和 ≥ XOR が常に成り立ちます。
  • 条件には単調性がある: 左端lを固定したとき、右端rを伸ばしていって一度「総和 ≠ XOR」になったら、それ以降は二度と等号が成立しません。すでに総和がXORより大きい状態でさらに要素を追加しても、その差は決して埋まりません。

この単調性のおかげで、各lに対して「条件を満たす最大のr」を二分探索で求めることができます。条件を満たすrは連続した範囲になるため、見つかった最大のrに対して (r − l + 1) 個のペアが存在することになり、これを答えに加算していきます。

アルゴリズムの手順

  1. 1-indexedの配列aに元の配列Aの値をコピーします。
  2. s[i](先頭からi番目までの累積和)と sx[i](先頭からi番目までのXOR累積)を前計算しておきます。これにより、任意の区間の総和とXORをO(1)で取得できます。
  3. 各lについて二分探索を行い、s[mi] − s[l−1] と sx[mi] XOR sx[l−1] が一致する最大のmiをrとして求めます。
  4. res に (r − l + 1) を加算し、最後に res を返します。
n := size of A
Define some arrays of size (n + 1) each, a, s and sx
for initialize i := 1, when i <= n, update (increase i by 1), do:
    a[i] := A[i - 1]
    s[i] := s[i - 1] + a[i]
    sx[i] := sx[i - 1] XOR a[i]
res := 0
for initialize l := 1, when l <= n, update (increase l by 1), do:
    bg := l, en = n, r = l
    while bg <= en, do:
        mi := (bg + en) / 2
        if s[mi] - s[l - 1] is same as (sx[mi] XOR sx[l - 1]), then:
            r := mi
            bg := mi + 1
        Otherwise
            en := mi - 1
        res := res + (r - l + 1)
return res

C++実装例

それでは、実際のC++コードを見てみましょう。

#include <bits/stdc++.h>
using namespace std;

int solve(vector<int> A){
    int n = A.size();
    vector<int> a(n + 1), s(n + 1), sx(n + 1);
    for (int i = 1; i <= n; i++){
        a[i] = A[i - 1];
        s[i] = s[i - 1] + a[i];
        sx[i] = sx[i - 1] ^ a[i];
    }
    int res = 0;
    for (int l = 1; l <= n; l++){
        int bg = l, en = n, r = l;
        while (bg <= en){
            int mi = (bg + en) / 2;
            if (s[mi] - s[l - 1] == (sx[mi] ^ sx[l - 1])){
                r = mi;
                bg = mi + 1;
            }
            else
                en = mi - 1;
        }
        res += (r - l + 1);
    }
    return res;
}
int main(){
    vector<int> A = { 2, 5, 4, 6 };
    cout << solve(A) << endl;
}

計算量

前計算にO(N)、各lに対する二分探索にO(log N)かかるため、全体の時間計算量は O(N log N) です。空間計算量は累積配列の分の O(N) となります。素朴なO(N²)の全探索と比べて大幅に高速化できていることが分かります。

入力

{ 2, 5, 4, 6 }

出力

5
  1. C++で数の奇数の約数(奇因子)の合計を求めるプログラム

    正の整数が与えられたとき、その数の奇数の約数(奇因子)をすべて求め、それらの合計を計算するのが本プログラムの目的です。 例 入力: number = 20 出力: 奇数の約数の合計は: 6 入力: number = 18 出力: 奇数の約数の合計は: 13 例えば number = 20 の場合、約数は 1, 2, 4, 5, 10, 20 ですが、このうち奇数は 1 と 5 のみです。したがって、結果 = 1 + 5 = 6 となります。 プログラムで使用するアプローチ 奇数の約数の合計を計算する対象の数を入力する 偶数の約数を除外するため、まず数を2で割り切れる限り2で割り続け、奇数の部

  2. C++で16進数を10進数に変換するプログラムの作成方法

    16進数が入力として与えられたとき、その16進数を10進数に変換するのが本記事のテーマです。 コンピュータにおける16進数は基数16で表現され、10進数は基数10で表現されます。10進数では0〜9の数字のみを使用しますが、16進数では0〜15までの値を扱うことができ、10以上の値はそれぞれA(10)、B(11)、C(12)、D(13)、E(14)、F(15)という英字で表されます。 16進数から10進数への変換手順 16進数を10進数に変換するには、以下の手順に従います。 右端の桁から順に数字を取り出し、0から始まるべき乗を掛けていきます。指数は「桁数 − 1」になるまで1ずつ増やします。