見出し画像

ABC201 E 解答

E - Xor Distances(1694)

問題

問題文

N 頂点の重み付き木があります。i 本目の辺は頂点 ui と頂点 vi を双方向に結んでいて、その重みは wi です。頂点の組 ( x, y ) について、dist ( x, y ) を以下のように定めます。

・x から y への最短パスに含まれる辺全ての重みの XOR

1 ≤ i < j ≤ N を満たす全ての組 ( i, j ) について dist( i, j ) を求め、その総和を 
( 10^9+7 ) で割った余りを出力してください。

制約

2 ≤ N ≤ 2×10^5
1 ≤ ui < vi ≤ N
0 ≤ wi < 2^60
与えられるグラフは木
入力は全て整数

考察

まずはxor演算の性質を見ていきます。(10進数と2進数を混ぜてます。すみません)

6 xor 3
= 110 xor 011
= 101
= 5

となります。もうちょっと見てみます。

6 xor 3 xor 5 
=(6 xor 3) xor 5
= 101 xor 101
= 0

これを縦に書いてみます。

6 : 110
3 : 011
5 : 101
     000

です。なんか言えそうなことをまとめます。

1)xor演算は各桁ごとに独立してる
2)その値は1の数の偶数奇数で決定される
3)同じもののxorは0

です。例が少なくてこじつけみたいになってますが、今一度確認してみてください。

問題に移ります。本問題は木の任意の2点を選択してその経路のxor演算の総和を求める問題です。木の具体例を示します。ついでに2進数にしておきます。

画像1

このままだと、見通しが立たないので、先の性質を使います。2)です。xor 演算は各桁ごとに独立して考えられるので、桁を分けてしまいましょう。1桁目に注目すると次のようなグラフになります。試しに2つに頂点を選んでxorを取ってみます。

画像3

となります。これを全ての頂点の組みでやったら大変なことになりそうです。

ですので、一つ基準を決めてあげましょう。その基準から各頂点への xor 演算を計算します。
その後再び、先ほどと同じ頂点のxorを考えてみます。頂点を0と置いておきましょうか。1と置いても答えは変わりません。

画像3

するとどうでしょうか。基準からのxorの値のxor(赤い経路)は、

先程の経路+共通の経路

というように表現されてます。ここで性質の3)です。同じもののxorは0になりますね。0 xor X = Xとなるように、0とのxorはその値が返ります。ですので、共通経路が無視されて任意の2点間のxorが取得できるわけです(水色と緑でかぶっているところは無視できる)。実際に計算してみると

基準から左下:1 xor 1 xor 0 = 0
基準から右下:1 xor 1 = 0

で1つ目の1が共通ですので、これは無視できます。とすると

1 xor 0 xor 1 = 0

となって一致します。

これを全頂点間で試して1となる組を数えれば良さそうです。が、これだと計算量はO(N^2)です。頂点数の2乗だけかかります。残念ながら間に合いません。

少し考え方を変えてみましょう。頂点を2つ選んだ時に起こりうる組み合わせを列挙します。

0 0 : 0
0 1 : 1
1 0 : 1
1 1 : 0

の4つです。このうち1となるのは10か01の場合です。ここでxorの2)です。xorは1の数の偶数奇数により決定されます。順番も関係ありませんね。

ということで、xorが1となる頂点の組みは0の頂点から1個、1の頂点から1個選んだ場合に限られます。その組の総数は、

総数=0の頂点数*1の頂点数

で求まります。各頂点が0、1のどちらか調べるだけですので、O(N)で計算できます。上の図の場合ですと、

5 * 1 = 5 通り

でしょうか。

これで1桁目が1となる総数が求まりました。2進数の1桁目は1です。1が総数個存在することになりますので、この総数が答えに加算されます。

2桁目以降も同様に求めることが可能です。ただし、最後に答えに加算する時には注意が必要です。2進数の2桁目は2ですので、総数に2をかけたものを足しましょう。2が総数個存在するということです。3桁目なら4ですね。

以上の計算により答えを求めることが可能です。

実装について少しだけ説明します。木グラフにおいて各値のxorを求めるのはBFSやDFSでやるのが良さそうです。制約を見ますと桁は60桁あるので、BFSを60回やると全桁求まります。しかもどうやらこれが間に合うらしいです。詳しくは確かめてないです。

ですが、60回もやる必要はなく、例えば各頂点に対して60個分配列を用意して、探索の際にはビットシフトで桁をずらしてあげれば1回のBFSやDFSで探索が完了します。

あとはソースコードで補足します。

実装

 #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<ll, ll>;

const int mod = 1e9 + 7;
class mint
{
public:
   long long x;
   mint(long long x = 0) :x((x% mod + mod) % mod) {}
   mint operator -() const { return mint(-x); }
   mint& operator +=(const mint a)
   {
       if ((x += a.x) >= mod) x -= mod;
       return *this;
   }
   mint& operator -=(const mint a)
   {
       if ((x += -a.x + mod) >= mod) x -= mod;
       return *this;
   }
   mint& operator *=(const mint a)
   {
       (x *= a.x) %= mod;
       return *this;
   }
   mint operator+(const mint a)
   {
       mint res(*this);
       return res += a;
   }
   mint operator-(const mint a)
   {
       mint res(*this);
       return res -= a;
   }
   mint operator*(const mint a)
   {
       mint res(*this);
       return res *= a;
   }
   mint pow(long long t) const
   {
       if (!t) return 1;
       mint a = pow(t >> 1);
       a *= a;
       if (t & 1) a *= *this;
       return a;
   }
   //for only prime number
   //Fermat's little theorem
   mint inv() const
   {
       return pow(mod - 2);
   }
   mint& operator/=(const mint a)
   {
       return (*this) *= a.inv();
   }
   mint operator/(const mint a)
   {
       mint res(*this);
       return res /= a;
   }
};
vector<vector<pair<int, ll>>> g;

vector<int> used;
vector<vector<int>> x;
void dfs(int s)
{
   for (auto [e, w] : g[s])
   {
       if (used[e]) continue;
       used[e] = 1;
       //各桁で計算する
       rep(i, 60)
       {
           //各辺のそれまでの値とwの各桁のxor
           x[e][i] = x[s][i] ^ (w >> i & 1);
       }
       dfs(e);
   }

}

int main()
{
   int n;
   cin >> n;
   g.resize(n);
   used.resize(n);
   x.resize(n);
   rep(i, n) x[i].resize(60, 0);
   rep(i, n - 1)
   {
       int u, v;
       ll w;
       cin >> u >> v >> w;
       --u, --v;
       g[u].emplace_back(v, w);
       g[v].emplace_back(u, w);
   }
      
   //頂点0を基準にする
   used[0] = 1;
   dfs(0);
   mint ans = 0;
   rep(i, 60)
   {
       int zero = 0;
       int one = 0;
       rep(j, n)
       {
           //0と1の数をカウントする
           if (x[j][i] == 0) ++zero;
           else ++one;
       }
       //1桁目なら1,
       //2桁目なら2
       //3桁目なら4。
       mint bit = 1LL << i;
       //オーバーフローに注意
       ans += bit * zero * one;
   }
   cout << ans.x << endl;


   return 0;
}

あとがき

xorの性質を生かした問題でした。この問題のポイントは

xorは各桁ごとに計算できること
同じ数字のxorは0となること

だと思います。この性質を上手く使うことで、木構造の探索を効率よく行っています。

この問題は解説を見れば、なるほど、となりますがなかなかコンテスト中には気がつきませんね。どんな練習をすれば解けるようになるのでしょうか。

ただ、各要素を分解していくと見たことあるものも多いです。xorは各桁で計算、交換法則がある(これにより基準からのxorで計算)、同じものは0、などなど、グラフと合わさると見えにくくなります。

考えれば考えるほど、2つの要素がいい感じに組み合わさった素敵な問題だと思います。ぜひ挑戦してみてください!

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