Atcoder Regular Contest 138 (大和証券プログラミングコンテスト 2022 Spring) - ARC 138 D - Differ by K bits の解法です. 公式解説 を読んで,きれいに解けるのにびっくりしてしまいました. ここに書くのはもっと泥臭いです.

問題概要

整数 $N$, $K$ が与えられる. $0, 1, \ldots, 2^N - 1$ の順列 $P_0, P_1, \ldots, P_{2^N - 1}$ で, 隣接項が 2進表記でちょうど$K$桁だけ異なるものが存在するかどうか判定し, 存在する場合には1つ構成せよ.

制約: $1 \leq K \leq N \leq 18$

問題へのリンク

解法

$N = 1$ の場合には,$K = 1$ であり,$P_i = i$ という解がある. 以下,$N \geq 2$ とする.

$K = N$ の場合は,あきらかに解はない. また,$K$ が偶数の場合には,ビットカウントが奇数になる数が作れないので, 解はない. $K < N$ かつ $K$ が奇数の場合には, $n = K + 1, K + 2, \ldots, N$ の順に解を構成することができる.

まず,$n = K + 1$ とする. グレイコード を1つおきに反転すれば良い.具体的には, $Q_i := i \oplus \textrm{sftR}(i)$ とすると,$(Q_i)_i$ は順列になり,隣接項は2進表記で1桁だけ異なる. ただし,$\oplus$ は XOR で,sftR は右ビットシフト. したがって,

$$ P_i := \begin{cases} Q_i & \text{ $i$ が偶数の時}\\ \textrm{flip}(Q_i) & \text{ $i$ が奇数の時} \end{cases} $$

が解となる.ただし,flip は,末尾nビットのビット反転.

あとは,$n$ を1つずつ増やしていく.

  • 順列の前半 ($i < 2^n$) は,1つ前の数列をそのまま用いる.
  • 後半の先頭 ($i = 2^n$) は, 前半の最後の数の $K-1$ ビットを反転させた上で, 最上位ビットを立てる.
  • 後半の残りは,前半の対応する場所と同じビットを反転する.

具体的には,$(Q_i)_i$ を $n$ に対する解であるとして, $n + 1$ に対する解 $(P_i)_i$ は次のようになる.

$$ P_i := \begin{cases} Q_i & (i < 2^n) \\ Q_{2^n - 1} \oplus (2^{K - 1} - 1) \oplus 2^n & (i = 2^n) \\ Q_{i - 1} \oplus (Q_{i - 2^n - 1} \oplus Q_{i - 2^n}) & (i > 2^n) \end{cases} $$

実装

  ll N, K; cin >> N >> K;
  if (N == 1) { cout << "Yes\n0 1\n"; return 0; }
  if (K == N or K % 2 == 0) { cout << "No\n"; return 0; }
  vector<ll> ans(1LL << N);
  ll n = K + 1;
  ll mask = (1LL << n) - 1;
  REP(i, 1LL << n) {
    ll x = i ^ (i >> 1);
    ans[i] = i % 2 == 0 ? x : (x ^ mask);
  }
  for (; n < N; n++) {
    ll j0 = 1LL << n;
    ans[j0] = ans[j0 - 1] ^ ((1LL << (K - 1)) - 1) ^ j0;
    REP(i, j0 - 1) ans[j0 + i + 1] = ans[j0 + i] ^ ans[i] ^ ans[i + 1];
  }
  cout << "Yes\n";
  for (ll a : ans) cout << a << " ";
  cout << endl;

ACコード