D - XXOR

はじめに

競技プログラミングAtCoder Beginner Contest 117
D - XXOR
言語はC#
何かあればTwitter→@pirorirori_n712まで

問題

問題文

N個の非負整数A1,A2,...,ANおよび非負整数 Kが与えられます。
0以上K以下の整数 Xに対して、f(X)=(X XOR A1)+(X XOR A2)+...+(X XOR AN)とします。
ここで、非負整数 a,bに対して a XOR bは aとbのビットごとの排他的論理和を表します。
fの最大値を求めてください。

XORとは?

整数A,Bのビットごとの排他的論理和Xは、以下のように定義されます。
Xを二進表記した際の 2k(k≥0) の位の数は、
A,Bを二進表記した際の2kの位の数のうち一方のみが1であれば1、そうでなければ0である。
例えば、3 XOR 5=6となります (二進数表記すると:011 XOR 101=110)。

制約

入力は全て整数である
・1≤N≤10^5
・0≤K≤10^12
・0≤Ai≤10^12

入力

入力は以下の形式で標準入力から与えられる。

N K
A1 A2 ... AN

出力

fの最大値を出力せよ。

解き方

こちらの解き方を参考にしています。 goo.gl

①fを最大にするXを考える

fは、
N個の非負整数A1,A2,...,ANと
0以上K以下の整数 Xの
ビットごとの排他的論理和
和である。

そのため
N個の非負整数A1,A2,...,ANと
ビットごとの排他的論理和をとったときに
全体の和が最大になるような値を選ぶ必要がある。

ビットごとの排他的論理和をとるので
Xの値はN個の非負整数A1,A2,...,ANを
二進数になおした値で考えていく必要がある。

例えば、入力例1を二進数になおすと以下のようになる。
入力例1

3 7
1 6 3

二進数

1→001
6→110
3→011

ここからXに何をとれば
各数字のXORの和が最大になるかを考える。

XORは0に1をあてると1にひっくり返るので
0が多い桁を1にすると
XORをとったときに大きい値を得ることができる。

0 XOR 1 = 1

また、2進数は10進数になおすと左に進むごとに2Nの意味をもつ。

2^2 2^1 2^0
  0   0   1

これをもとにあらためて
2進数になおした上の数字を見ていくと以下のようになる。

2^2 2^1 2^0
  0   0   1
  1   1   0
  0   1   1

左の桁から考えていくと、
22の桁に0が2つあるので
Xの22の桁を1にすれば
XORをとったとき
22×2 数が得られる。

21の桁は
0が1つしかないので
Xの21の桁を1にしても
21×1しか数が得られない。

逆に
1が2つあるので
Xの21の桁を0にすれば
21×2を得ることができる。

同じように20の桁についても考える。
こちらも21の桁と同じように
0が1つしかなく1が2つあるので
Xの20桁は0をとったほうが
XORで大きい値を得られる。

2^2 2^1 2^0
  0   0   1
  1   1   0
  0   1   1

X 1   0   0

この際、
Xは0以上K以下という条件に注意しなければならない。
KよりXが大きくなりそうであれば
見ている桁を1にすることをやめる必要がある。

この部分のコードを書くと以下のようになる。
・各桁で1になっている箇所を保存

        //各桁で1になっている箇所の数を保存する
        //XはKよりも大きくはならないので、Kの最大の桁までを保存する
        var length = (long)Math.Log(K, 2);
        var count = new int[length + 1];
        var bit = 1L;
        for (int i = 0; i <= length; ++i)
        {
            for (long j = 0; j < N; ++j) if ((bit & A[j]) != 0) count[i]++;
            bit <<= 1;
        }

・Xを求める

        bit = 1L;
        //大きい桁から考える
        bit <<= (int)length;
        //X
        var X = 0L;
        var temp = 0L;
        //大きい桁から順番に1にするかどうかを決める
        for (long i = length; 0 <= i; --i)
        {
            //A全体の数の半分以上が0であればビットを立てる
            if (count[i] < ((N + 1) / 2)) temp |= bit;
            //K以下であればXがとれる値なのでXに代入
            if (temp <= K) X = temp;
            //Kよりも大きければXはとれない値なのでK以下の状態に戻す
            else temp = X;
            //次の桁に進む
            bit >>= 1;
        }

A全体でビットが立っている(=1である)数が
半分より少ない場合にXのビットを立てる(=1)としている。
(この考え方でACが出るが、
下のビットを立てたほうが有利なパターンが全くないのか
それとも、たまたま今回は
これでも大丈夫だったのかは分かりませんでした。)

また桁が大きいほうから考えているのは、
K=8
A={7,3,3}
というようなケースで上の桁から決めないと
Xで間違った値が出てきてしまうからである。

②求めたXとAをXORした和を求める

Xと各AiをXORした和を求める

        var ans = 0L;
        for (int i = 0; i < N; ++i) ans += (X ^ A[i]);

全体のコード

using System;
using System.Linq;
using System.Collections.Generic;

class Dmondai
{
    static void Main()
    {
        var line = Console.ReadLine().Split(' ').Select(x => Int64.Parse(x)).ToArray();
        var N = line[0];
        var K = line[1];
        var A = Console.ReadLine().Split(' ').Select(x => Int64.Parse(x)).ToArray();
        //Kが0のときはそののまま和を出力
        if (K == 0)
        {
            Console.WriteLine(A.Sum(x => x));
            return;
        }
        //各桁で1になっている箇所の数を保存する
        //XはKよりも大きくはならないので、Kの最大の桁までを保存する
        var length = (long)Math.Log(K, 2);
        var count = new int[length + 1];
        var bit = 1L;
        for (int i = 0; i <= length; ++i)
        {
            //ビットが立っているものをカウント
            for (long j = 0; j < N; ++j) if ((bit & A[j]) != 0) count[i]++;
            //次の桁に進む
            bit <<= 1;
        }
        bit = 1L;
        //大きい桁から考える
        bit <<= (int)length;
        //X
        var X = 0L;
        var temp = 0L;
        //大きい桁から順番に1にするかどうかを決める
        for (long i = length; 0 <= i; --i)
        {
            //A全体の数の半分以上が0であればビットを立てる
            if (count[i] < ((N + 1) / 2)) temp |= bit;
            //K以下であればXがとれる値なのでXに代入
            if (temp <= K) X = temp;
            //Kよりも大きければXはとれない値なのでK以下の状態に戻す
            else temp = X;
            //次の桁に進む
            bit >>= 1;
        }
        //Xと各AiをXORした和を求める
        var ans = 0L;
        for (int i = 0; i < N; ++i) ans += (X ^ A[i]);
        Console.WriteLine(ans);
    }
}