F - Spices Editorial by iica


掃き出し法によって基底を求めているのだと捉えるとコードが書きやすかったので、誰かの役に立てばと思いメモを貼っておきます。


\(N\) 桁の \(2\) 進数は、\(\mathbb F_2\) を成分とする \(N\) 次元のベクトルとみることができます。この見方をしたとき、本問は \((2^N-1)\) 本のベクトルから何本かを選んで、ベクトル空間の基底をつくる問題といえます。

ベクトル空間の生成元たちが与えられているとき、そこからいくつかのベクトルを選んで基底をつくる方法としては、掃き出し法が有名です。

詳細は略しますが、本問でいえば、\((2^N-1)\)\(N\) 列の行列を考えて、前進消去といわれる操作を行うことにより、すでに基底に採用されたベクトルから生成されるベクトルを消去していきます。 そして、最後まで消去されなかった \(N\) 本のベクトルが基底というわけです。

ところで、本問では基底であれば何でもよいというわけではなく、その中でも特に価格が最も安くなるような選び方をしなければなりません。

そのために、あらかじめ行の入れ替えを行い、価格の安いベクトルほど若い行番号となるようにソートしておきます。そして、若い行番号から順に貪欲に採用していけばよいというのは、公式解説などにあるとおりです。

このとき、上から順に処理していってある対角線上の成分が \(0\) になってしまっているときも、その行で右の方を見て \(0\) でない成分があれば、その行は採用しなければなりません。これは、掃き出し法の言葉で言えば、横方向のピボット選択をして、今採用した行でビットが立っている最も上位の桁をピボットとしていることになります。

これをコードにすると次のようになります(D言語)。 jが行列の行番号に相当し、ts[j]が初めにその行にあったベクトル、xs[j]が現在(前進消去の影響を受けて)その行に書いてあるベクトルです。

import std;
void main(){
  int n = readln.chomp.to!int;
  long[] cs = [0L] ~ readln.chomp.split.to!(long[]);
    // cs[t]: 番号tのスパイスの値段 ※0始まり
	
  int[] ts = (1<<n).iota.array;
  ts.sort!((a, b) => cs[a] < cs[b]);
    // ts[j]: j番目に安いスパイスの番号
	
  int[] xs = (1<<n).iota.array;
    // xs[t]: 番号tのスパイスが現在持っているビットパターン
	
  long ans;
  foreach(j, t; ts){
    if(xs[t] == 0) continue;
    ans += cs[t];
    foreach(t2; ts[j + 1 .. $]) xs[t2] = min(xs[t2], xs[t2] ^ xs[t]);
      // 掃き出し法の要領で,未検討のスパイスのビットパターンを更新する
    }
  ans.writeln;
}

https://atcoder.jp/contests/abc236/submissions/29327488

posted:
last update: