ARMERIA

Rubyと競技プログラミングの話 AtCoderやCodeforcesの問題解説記事が多め。

Codeforces Round #458 (Div. 1 + Div. 2, combined) E. Palindromes in a Tree

お題箱より。

Problem - E - Codeforces

問題概要

 n 頂点の木が与えられ、各頂点には a から t までのうち1文字のアルファベットが書かれている。

この木の2点間を結ぶパスであって、「通過する頂点のアルファベットを並び替えることで回文にすることができる」ようなものをpalindromicなパスと呼ぶ。

各頂点  i について、頂点  i を通るpalindromicなパスの数を求めよ。ここで始点と終点が同一であるパス  (a, a) はカウントし、始点と終点を入れ替えたパス  (a, b), (b, a) は1つのみカウントするものとする。

制約

  •  1 \le n \le 2\times 10^{5}
  • 各頂点に書かれたアルファベットは a 以上 t 以下
  • グラフは木である

解説の前に

この問題は重心分解そのものを学ぶ問題としてはあまり適さないと思います。重心分解の実装や使い方については、まず

ツリーの重心分解 (木の重心分解) の図解 - Qiita

こちらの記事で実装について理解し、

競技プログラミングにおける重心分解問題まとめ - はまやんはまやんはまやん

こちらの記事の(入門)と書かれている問題などで使い方を習得することをおすすめします。

解法

パスの数え上げ→重心分解を考えてみる

「木について、ある条件を満たすパスの個数を求めよ」というタイプの問題でよく使うのが重心分解です。重心分解でパスの本数を数える問題は、

まず木の重心を求め、その重心を通るようなパスだけについて条件を満たすものを数えた後、その重心を取り除くことでできる部分木に対して同じ処理を再帰的に行う

という構造になっています。

重心分解でパスの数え上げが上手くいくのは、数えたいパスが以下のような性質を持っている場合です。

各頂点  v と重心を結ぶパスについて何らかの特徴量  B_{v} が計算できて、重心を挟む2頂点間のパス  (v, u) がカウント対象となる条件が  B_{v}, B_{u} で表現できる。特に、一方の端点の特徴量  B_{v} を固定した時にもう一方の端点の特徴量  B_{u} が満たすべき条件が容易に定まる。

どういうことなのか、今回の問題で具体的に説明していきましょう。

並び替えて回文にできるかどうか判定するので、パスの特徴として必要なのは各アルファベットの登場回数の偶奇だけです。アルファベットの種類は20個なので、これは長さ20のビット列として表現できます。パスがpalindromicである条件は、このビット列において立っているビットが0個または1個であることです。

重心を決め、重心のアルファベットに対応するビットだけを立てたビット列を  C とします。そして重心以外の頂点  v について「(重心を除いて)重心からその頂点までのパスに対応するビット列」  B_{v} が求められたとします。

このとき以下の図のようにパスの片方の端点  v を固定すると、もう片方の端点  u についての条件は「 B_{v} \oplus C \oplus B_{u} の立っているビットが0個または1個であること」と表現できます。ここで  \oplus はXORです。

f:id:betrue12:20200302230923p:plain

長さ20のビット列において立っているビットが0個または1個であるものは21個なので、 B_{u} の候補は21個に定まります。このように片方の頂点が持つ値  B_{v} から、ぺアになれるもう片方の頂点が持つ値  B_{u} の条件が決まるという点が重要です。

この性質を利用すると、重心分解を用いたパスの数え上げアルゴリズムは大まかには以下のようになります。

  1. まず重心からDFSを行い、全ての頂点  v について  B_{v} の値を求め、その頻度表を管理しておく。
  2.  B_{v} について、ペアとなれるような  B_{u} を持つ頂点の個数を合計する。

ただしこの方法では、重心から見て同じ側にあるような2頂点を選んでしまう可能性があります。それを防ぐためには、手順2.において見ている頂点と同じ側の頂点が持つ値  B_{v} を頻度表から除いておく必要があります。

また重心自身もパスの端点となり得るので、そこも考慮する必要があります。重心そのものの  B_{v} の値を  0...0 として頻度表に含めてしまうと良いでしょう。

単にパスを数え上げる問題であればここまで考慮すればOKなことが多いです。ですが今回の問題では「各頂点ごとにそれを通るpalindromicなパスの数を求めよ」というオマケ付きです。これを何とかします。

各頂点ごとに数え上げる方法

まず重心からDFSを行い、全ての頂点  v について  B_{v} の値を求め、その頻度表を管理します。

その上で、以下のような処理をします。

f:id:betrue12:20200302235851p:plain

ちょっと図が複雑ですが、

  1. 重心から再度DFSをする。このとき、見に行く方向にある頂点の値は頻度表から除くようにする。
  2. DFSで訪れた頂点を一方の端点  v とみなし、ペアになれるもう一方の端点の個数を頻度表から計算する。
  3. そこで得られた値を、 v から重心までのパス上の頂点全ての答えに足す。実際には再帰で戻るときに伝播すると良い。

という処理をします。こうすると

  • 重心以外の頂点については、その頂点を通るpalindromicなパスが1回分足される。
  • 重心については、その頂点を通るpalindromicなパスが2回分足される。

ことになります。重心をまたぐ各パスについて見ている頂点がどちらである時にも重心には必ず加算されるので、2回分足されるという仕組みです。

そのため重心については加算分の値を2で割ることで、各頂点について必要な値が求められます。一方の端点が重心であるようなパスや、始点終点がともに重心であるようなパスについても2回分足されるようにして辻褄を合わせる必要があるため、ここは上手く調整しましょう。

これで当初の目的である、

木の重心を通るようなパスだけについて条件を満たすものを考え、各頂点ごとにその頂点を通るパスの本数を答えに足す

という処理が達成されました。あとはこれを再帰的に適用していくことで全体の答えを求めることができます。

ACコード

Submission #70034325 - Codeforces