見出し画像

ABC197 C 解答

C - ORXOR (809)

問題文

長さ N の数列 A が与えられます。この数列を、1 つ以上の空でない連続した区間に分けます。その後、分けた各区間で、区間内の数のビット単位 OR を計算します。こうして得られた全ての値のビット単位 XOR
として考えられる最小値を求めてください。

制約

1 ≤ N ≤20
0 ≤ A_i < 2^30
入力に含まれる値は全て整数である

考察

まず、演算について簡単に説明します。ORとXORはどちらも2進数にした時の各ビット(桁)に対する演算です。例として1と2と5と見てみます。

1: 0 0 1
2 : 0 1 0
5 : 1 0 1

ORはその名前の通り、どちらかのbitが1であれば1を返す演算です。

1 OR 2 = 001 OR 010 = 011 = 4
2 OR 5 = 010 OR 101 = 111 = 7
1 OR 5= 001 OR 101 = 101 = 5

こんな感じです。 XORは比較する2つの数の該当のビットが等しければ0、異なれば1を返す演算です。もうちょっと言いますと、該当桁の1が偶数個であれば1を奇数個であれば0を返します。

1 XOR 2 = 001 XOR 010 = 011 = 4
2 XOR 5 = 010 XOR 101 = 111 = 7
1 XOR 5 = 001 XOR 101 = 100 = 4

ここで大事なのが、ORやXORの演算はどんな順番で行っても答えが変わらないということです(日本語が難しいですが、ORはOR同士であれば好きな順番でおこなっても良い、ということです、ORとXORを混ぜると答えは変わります。それに応えるのが本問題です)。

ですので、本問題を考える際には、どの組をORで演算するかのグループさえ決めてしまえば、答えはその演算順には依らないため、一意に答えが得られるというわけです。

ということで、どのようにグループ分けするかを考えましょう。これは本問題の制約が

1 ≤ N ≤20

と小さいことを利用します。BIT全探索というやつです。

例を出します。

1 2 3 4 5 6

としましょう。6個の数字がありますね。これをBIT全探索でいくつかのグループに分けます。まずは、6桁の0を用意しましょう。

0 0 0 0 0 0 = 0

これを、全部 1 になるまで全パターン行います。

1 1 1 1 1 1 = 63 

どうやら64パターンあるらしいです。これを使ってグループ分けをします。例えば、

0 0 1 0 1 1 = 11

であれば、連続した数字を同じグループであるとみなすことにより、

[1 2] [3] [4] [56]

とした4グループに分かれます。もう一つ見ましょう。

1 1 1 1 1 0 = 62
[1 2 3 4 5] [6]

ですね。連続の判定は様々なやり方があると思います。私はbitの左シフトを使って、

(1 1 1 1 1 0 & (1 << i) ) *2 == (1 1 1 1 1 0 & (1 << (i+1)))

とやりました。i = 3 としてみましょう。

(1 1 1 1 1 0 & (1 << 3)) * 2
= (1 1 1 1 1 0 & 0 0 1 0 0 0) * 2
= 0 0 1 0 0 0 * 2
= 8 * 2 = 16

(1 1 1 1 1 0 & (1 << (3+1)))
= (1 1 1 1 1 0 & 0 1 0 0 0 0)
= 0 1 0 0 0 0 = 16

です。 *2 はbitを一つ分左に移動する操作に対応します。これが一致していれば、00もしくは11ですので、同じグループに属します。01、10のときは違う値になりますので、グループを変えましょう(0のとき結果は0、1の時には0以外になります)。

グループ分けと書きましたが、実装の際にはどんどん OR 演算をしていき、その結果をある配列に詰め込んでいます。全てのグループ分けが終わった後に、配列にある値を前からXORすることで答えを求めています。

実装も少し大変なのでコメントで補足します。

実装


#include<bits/stdc++.h>
#define rep(i,n) for(int i=0;i<n;++i)
#define reps(i,s,n) for(int i=s;i<n;++i)
using namespace std;
using ll = long long;
using P = pair<int, int>;

int main()
{
   int n;
   cin >> n;
   vector<ll> a(n);
   rep(i,n) cin >> a[i];
   //ansには大きな値を入れておく
   ll ans = 1e18;
   //nまでで十分だが、最大ケースにしてある
   rep(i,1<<20)
   {
       ll bit = i;
       //はじめa[0]をいれる
       ll tmp = a[0];
       //グループ分けした数を入れる配列
       vector<ll> v;
       rep(j,n-1)
       {
           //a[1]から開始
           ll bit1 = bit & (1<<j);
           ll bit2 = bit & (1<<(j+1));

           if(bit1*2 == bit2)
           {
               //等しい場合にはORをとる
               tmp = tmp | a[j+1];
           }
           else
           {
               //異なる場合には配列にいれて、仕切り直し
               v.emplace_back(tmp);
               tmp = a[j+1];
           }
       }
       //最後の数も忘れずに入れる
       v.emplace_back(tmp);
       //0のXORは0である(0なら演算に影響を与えない)
       ll now = 0;
       //XOR演算
       for(auto vi:v) now = now ^ vi;
       //最小値であれば更新
       ans = min(ans,now);  
   }
   cout << ans << endl;
   return 0;
}

あとがき

連続する数の判定方法は様々なやり方があると思います。

3 (10) = 11 (2)

なのを利用して、i だけ右シフトして &3 %3 の結果で判定するなんて方法も教えていただきました。賢い。

3を i 個分左シフトして比較する 2 のべき乗(または 0)になっているかで判断するのも良さそうです。

とにかく、連続する数の判定というのが大切なところだったと思います。ですので、この問題はただのBIT全探索よりかはとっても苦戦しました。私はなんとか解くことはできましたが、相当に時間を費やし1WAのおまけもついてきました。

BIT全探索+αをするという、とても良い問題だったのかなと思います。実際コンテスト中に全探索だと理解してから、「連続部分どうしよう」と考えているときは非常に焦りましたが、楽しかったです。BIT全探索を一応かけるようになった。という方の次のステップにぴったりですね。

この記事が気に入ったらサポートをしてみませんか?