木と計算量 前編 〜O(N^2)とO(NK)の木DP〜

この記事はCompetitive Programming Advent Calendar 2018の46日目の記事として書かれました(嘘)

最近、木上のアルゴリズムの面白い計算量解析が2つ話題になったのでまとめておきます。

予備知識

まず、https://web.archive.org/web/20150819082918/https://topcoder.g.hatena.ne.jp/iwiwi/20120428/1335635594 について復習します。
iwiさんのブログとは違う、より直感的な解析方法も紹介します

以下の問題を考えます。

N 頂点の木が与えられる。
頂点 1 を含む頂点数 K の根付き木の個数を求めよ。
制約:1 ≦ K ≦ N ≦ 3000

典型的な木DPの問題です。
解法は以下の通りです。(解法の細かい説明は本題ではないので追わなくて大丈夫です)

頂点 1 を根とした根付き木にして以下のようなdpテーブルをボトムアップに計算していく。
・dp[v][i] = 頂点 v を根とする頂点数 i の根付き木の個数

例えばpythonで書くと以下の通りです。

def dfs(v):
  sz[v] = 1
  dp[v] = [0]*(sz[v]+1)
  dp[v][1] = 1
  for u in to[v]:
    dfs(u)
    merged = [0]*(sz[v]+sz[u]+1)
    for i in range(sz[v]+1):
      for j in range(sz[u]+1):
        merged[i+j] += dp[v][i]*dp[u][j]%mod
    sz[v] += sz[u]
    dp[v] = merged
  dp[v][0] = 1

(変数宣言、入出力などは省略しています)
このアルゴリズムの計算量について考えていきます。

最も重い部分は i,j に関する二重ループの部分です。
ここの計算量は O(sz[v] * sz[u]) です。
つまり、サイズがそれぞれ a, b のdpテーブルをマージするときに O(ab) の計算量がかかっています。
これらを合計すると一見 O(N^3) の計算量が掛かるように思えます。
しかし、実は全体で O(N^2) になっているのです。

この計算量を解析するために以下のような問題を考えます。

N 個のグループがあり、初めは各グループに頂点が1つずつ含まれています。
これらをマージしていき、最終的に1つのグループにしたいです。
グループ A,B をマージするとき、A に含まれる頂点と B に含まれる頂点の間を結ぶような辺を全て追加します。(つまり、|A|*|B| 本の辺を追加します)
マージの順番を工夫したとき、追加する辺の本数は最大で何本でしょうか?

例えば以下のような流れになります。

答えは「どのような順番でマージしても完全グラフになるので、N*(N-1)/2」でした。
(どの2頂点間の辺についてもちょうど1回ずつ追加されるため)

この問題が計算量解析にどう関係しているかは、以下の2つを比較すれば分かるでしょう。

  • サイズがそれぞれ a, b のdpテーブルをマージするときに O(ab) の計算量がかかる
  • サイズがそれぞれ a, b のグループをマージするときに ab 本の辺を追加する

辺の本数がそのまま計算量を表しているのです。
というわけで先ほどのアルゴリズムの計算量は O(N^2) なのでした。

本編

予備知識だけでもそれなりのボリュームでしたが続けます。

以下のような問題を考えます。

N 頂点の木が与えられる。
頂点 1 を含む頂点数 K の根付き木の個数を求めよ。
制約:1 ≦ N ≦ 10^5, 1 ≦ K ≦ 500

先ほどと同じようなDPをpythonで書くと以下のようになります。

def dfs(v):
  global ans
  sz[v] = 1
  dp[v] = [0]*(sz[v]+1)
  dp[v][1] = 1
  for u in to[v]:
    dfs(u)
    merged = [0]*(sz[v]+sz[u]+1)
    for i in range(sz[v]+1):
      for j in range(sz[u]+1):
        merged[i+j] += dp[v][i]*dp[u][j]%mod
    sz[v] += sz[u]
    dp[v] = merged
    if sz[v] > K:
      sz[v] = K
      dp[v] = dp[v][:K+1]
  if sz[v] >= K:
    ans += dp[v][K]
    ans %= mod
  dp[v][0] = 1

要点は、dpテーブルのサイズが K を超えたら K になるようにカットしている点です。
このアルゴリズムの計算量を解析するために、簡略化した以下の問題を考えます。

N 個の集合があり、初めは各集合のサイズが1です。
これらをマージしていき、最終的に1つの集合にしたいです。
グループ A,B をマージするとき、min(|A|, K) * min(|B|, K) のコストがかかります。
マージの順番を工夫したとき、コストの合計は最大でいくらになるでしょうか?

追記:下の方により簡潔な解析方法を書きました。

サイズが K 未満の集合を「小」、K 以上の集合を「大」と表すことにして場合分けをします。

小と大のマージ

小と大のマージコストは「(小のサイズ) * K」です。
(集合の要素)に注目すると、各元が小-大マージの小の元として選ばれる回数は高々1回です
また、元の個数は N なので Σ(小のサイズ) は N で抑えられます。
よって、小と大のマージコストの合計が O(NK) であることが言えました。

大と大のマージ

大と大のマージコストは K^2 です。
大と大を x 回マージするためには、大の集合が x+1 個必要です。
大の集合は高々 N/K 個しか作れないので、大と大のマージは高々 N/K 回です。
K^2 * (N/K)\ =\ NK なので、大と大のマージコストの合計が O(NK) であることが言えました。

小と小のマージ

小と小のマージだけを行うことを考えます。
各集合のサイズは 2K 以上にはなりえません。
また、各集合での合計コストは O(集合サイズの二乗) です。(予備知識参照)

つまり、小と小のマージコストの合計の最大値は 以下の式で上から抑えられます。

  • max(\sum A_i^2\ |\ A_i \leq K,\ \sum A_i = N)(A を正整数列とします)(定数倍は無視しています)

これは Ai が全て K の場合に最大となりK^2 * (N/K)\ =\ NK となります
これで、小と小のマージコストの合計が O(NK) であることが言えました。


これらをまとめると、この問題の答えは O(NK) となります。
これで、上記のアルゴリズムの計算量は O(NK) であることが言えました。

追記

より直感的で場合分けもない解析方法を知ったので書いておきます。
予備知識のところで使った手法に似ています。

マージの過程を二分木のような形で表します。

で、マージするときに左のグループ内の右からK個と右のグループ内の左からK個の頂点の間を張ることを考えます。

  • 各頂点間には高々1回しか辺が張られない
  • 距離が2K以上離れた頂点との間には辺は張られない

ということが言えるので、辺の本数の合計はO(NK)となります。

あとがき

結構長く競プロをやってるつもりだけど、この計算量今まで知らなかった。面白い。
場合分けをしない良い感じの解析方法を見つけたりしたら教えてください。(上の追記参照)

うむ,やっぱあまりに汎用性が高いので僕が知らなかっただけで常識なのではないかと思い始めた.

ちなみにこれを知ったきっかけはHello 2019 Gでした。
(※ そのままdpしてもダメなので式変形などで工夫をする必要はあります)

後編はこちら

最大マッチングを貪欲する問題の証明典型テク

ARC092C - 2D Plane 2N Pointsの証明が話題になってたっぽいので問題を見てみたら、最大マッチング系貪欲の証明テクが詰まった問題だなぁと思ったので書いてみた。

問題概要

  • 平面上に赤い点と青い点がある。
  • 赤い点が青い点の左下である(つまりxもyも小さい)とき、それらはペアにできる
  • 何ペア作れるか

解法

青い点をxの昇順に見て、マッチングできる赤い点があればそのうちyが最大のものとマッチングさせていく貪欲。

証明テク1:マッチングできるときはしてもよい

つまり、あえてマッチングさせないことによって答えが増えることはない。

なぜなら、ある青い点 A と赤い点 X をマッチングできるとき、

  • マッチングさせる:残りの点集合から A と X が消える
    • メリット:答えが 1 増える
  • マッチングさせない:残りの点集合から A が消える
    • メリット:X を残せる

で、「X を残せる」ことによって答えは高々 1 しか増やせないため、(マッチングさせたときの答え) < (マッチングさせないときの答え) となることはない。

ただし、A と別の赤い点 Y をマッチングさせる方が答えが良くなるという可能性はある点に注意。

このテクは結構出てくる。
使えるのは以下のようなケースのとき。

  • 次数1の頂点がある:木でのマッチングなど
  • マッチング相手に全順序がある:後述

例題:最近だとこれとか

証明テクテク2:マッチング相手に最弱の頂点があれば、それとマッチングさせてよい

以下のような状況を例に取って説明します。

f:id:snuke:20190103234249p:plain

緑の線の接続関係は同じとします。(灰色は何でもいい)

今、A にマッチングできる点は X と Y です。
X, Y とマッチングさせられる頂点集合を n(X), n(Y) とすると、n(X) ⊆ n(Y) です。
このとき、A と X はマッチングさせてもいいということです。
(少なくとも X か Y のどちらかとマッチングさせてもいいというのはテク1から言えますね)

なぜなら、

  • Y とマッチングさせる:このとき残る辺集合を E とする
  • X とマッチングさせる:このとき残る辺集合は(X と Y を区別せずに見ると) E + 辺(Y,α) となる

となり、辺が多い方が当然答えは大きくなるので X とマッチングさせても良いため。

マッチング相手が3頂点以上あった場合でも、その中で n(v) が最弱の頂点があればそれとマッチングさせて良い(つまり、どの他の頂点 u についても n(v) ⊆ n(u) が成り立つ v があれば)
大抵の問題ではマッチング相手の中に全順序があるが、一応そこまでは必要ない。

相性の良い競プロテク1:平面走査で変数を1つ消す

この問題の最後のピースです。
このままだと2変数あるので全順序とかも成り立ってないので工夫が必要です。

"A" として試す青い点を x の昇順で見ていくことによって x を消して1変数にすることができます
すると y の大小だけの全順序が成り立つようになるわけです。

もう少し詳しく言うと、

  1. A の x 座標 x_A は残っている青い点のうち最小である
  2. 以降は x 座標が x_A 以下の赤い点は全部 x 座標が -∞ だと思っても答えは変わらない
  3. A とマッチングできる頂点は x 座標が -∞ の赤い点のうち y 座標が y_A 以下のものである
  4. x 座標が -∞ の赤い点の中では、y 座標は大きければ大きいほど "弱い"

ということから、最初に書いたような解法の正当性が示せる。

あとがき

貪欲法の問題は、正しいと確信できる程度の証明までして初めて解けた言えると思うんですが、
証明方法にも典型パターンというものは存在して、そういうのに積極的に触れていけばその内できるようになっていくと思います。

Xmas Contest 2018 ギャラリー編

XmasContest終了後にA問題の解答画像をtwitterに上げてくれてる人がたくさんいて嬉しかったので、展覧会を開くことにしました。

まずは画像一覧をどうぞ。
photos.app.goo.gl
ダウンロードするとファイル名がユーザ名になってます。


これだけだと芸がないので、Dendrogram描いてみました。

配色

まずは配色でクラスタリング

f:id:snuke:20181226232046p:plain

Dendrogramの簡単な説明

木上で距離の近い頂点どうしは似ている、遠い頂点どうしは似てない、ということを表しています。
plotlyを使って描画しました。
plotlyのdendrogram機能には距離行列を入力すればいいです。

配色の距離の計算方法

4色×4色で重み付き二部マッチングをしました。
色どうしの距離は、RGB値をHSV値に変換して、H(色相)の差*1.0 + S(彩度)の差*0.2 + V(明度)の差*0.1 として計算しました。
色相の差は円環上の距離なので max(|A-B|, 1-|A-B|) のように計算しないといけない点に注意。

観察

jken_ullさんの作品armeria_betrueさんの作品は配色が (赤#F00, 緑#0F0, 青#00F, 黄#FF0)で完全に一致しています。
RGB値の差はあれど、この配色が多かったです。
黄色ではなくオレンジを使った人も結構いました。

1人明らかに様子がおかしい人がいるのはおいといて、259_Momoneさんの作品は結構独特な配色で、唯一灰色を使用しています。赤・緑のクリスマスカラーに白・灰と、かなり配色センスを感じました。
あと、Feunard_Luneさんの作品の配色は、なんというかパステルカラーでかわいい感じでいいですね。AtCoderユーザ名の顔文字もかわいくて好きです)

塗り分け方

次に配色を無視して、塗り分け方でクラスタリング

f:id:snuke:20181226232110p:plain

距離の計算方法

各領域を頂点とした完全グラフで、各辺に端点どうしの色が同じかどうか(bool)を書き込んだ行列を作ります。
で、行列どうしのハミング距離を距離としました。

観察

ceni1055さんの作品ant2357さんの作品が驚異のシンクロ率でした。色の割り当てまで同じです(驚) もはや間違い探しですね。(1箇所だけ違います)
ceni1055さんは昔自作されたソフトを使って解かれたようです。
ant2357さんは恐らくこのサイトを使われたのだと思います。
プログラムで解くとこういう感じになるんですね。


あと特徴が分かりやすそうなのは、文字の部分の塗り方でしょうか。
X以外が同じ色:Py2K4さんsemiexpさんtatyam_primeさん(、と自分)
"Xmas"と"Contest 2018"をそれぞれ同じ色:kakira9618さんskkytkstexhkさん
"Xmas"と"Contest"と"2018"をそれぞれ同じ色(未遂):jken_ullさん(後で自分もやってみようとしましたが、下の方が不可能なことを証明できたので諦めました)

ビジュアル

最後に、色も塗り方も考慮した感じのクラスタリング

f:id:snuke:20181226232305p:plain

距離の計算方法

各領域ごとに色の距離を計算してその和を取った。

観察

たしかに似てるような。(それ以外特に感想もない)
まぁ、おまけですね。

Xmas Contest 2018

Xmas Contest 2018 今年も開催しました。
コンテスト密度の高い中、ご参加いただいた皆さまありがとうございました。
楽しんでいただけたでしょうか?(よろしければアンケートお願いします)

今年の運営は4年連続のじゃっぷるさんと、2010~2014の運営の本家本元hosさんとやりました。
hosさんが数学的に面白くて奥深い問題とかをどんどん出していてすごかったです。さすが。
特にxorのやつはまじですごいのでおすすめです。クッソむずいですが。
diceの問題も楽しかったです。(楽しくなって作った謎ゲーム

自分が原案の問題の解説を書いておきます。

続きを読む

Mail.Ru Cup 2018 Round 1

久しぶりにCFに参加した。(開始10分後くらいに偶然TLを見て気づいた)

A

問題文が難しい><

B

まぁ。

C

 ans_i=n-l_i-r_i として、正しいかチェック。

D

累積xorをとって長さn+1の数列Xを作る。
Xから同じ数を2つ取ってきてその間を選ぶとxorが0になる。
つまり、 cnt_i = X内のiの個数とすると、 \sum (cnt_i\ choose\ 2)を最小化すればいい。
操作はある場所以降をflipに対応してるので、組み合わせると最初以外の要素を好きにflipできる。
 cnt_i + cnt_{\overline{i}} は一定だが、その範疇では好きに配分できる。半分ずつくらいに配分すると最小になる。

E

「1行目に0を、2行目に1を集める」というのを標準形とする。
任意の状態←→標準形も標準形←→標準形もs回の操作で変換できるので、3s回でできる。

F

とりあえず各点を長さ0の線分で覆えば達成はできる。
隣り合う線分の間を繋ぐと答えが1減る。
"間"の線分を交差しないようにできるだけたくさん選ぶ問題になる。
縦と横の二部グラフで最大独立集合を取る問題になってあとは復元とか頑張って。
二部グラフの最小点被覆、最大安定集合 (最大独立集合)、最小辺被覆を総整理!

G

葉から順に消していく。

  • ビット数=1のコミュニティは消す。
  • 残りのコミュニティで常にs[i][v]=1,s[i][u]=0になってるiが無ければvをuにくっついてる葉としてvを消す。

コミュニティを全部消せればYES。(連結になってなかったら適当に繋ぐ)
実装はbitsetを使ってO(2000^3/64)

  • a[i] = s[i]
  • d[i] = a[j][i]=1であるようなa[j]の&を取ったもの(iビット目は0にする)

を持って、

  • a[i].count() == 1があったらa[i]を消して、立ってるbitをjとしてd[j]だけ再計算
  • d[i] != 0があったらiを葉にする

って感じでやる

lewin氏の別解頭良さそう。
完全グラフを作って辺uvの重みはs[i][u]=s[i][v]=1になってるiの個数にする。そのグラフでMST(minじゃなくてmaxだけど)を求めてそれをチェックすればいい。
重みを全部計算するのは長さMのbitsetで&とpopcountをすれば/64できる。(CFだと/32なんだっけ...)



感想

普通に楽しかったしCFもっと出ていきたい。(とりあえずIGM,LGMに戻すぞー、Nutellaになりたい)

Knapsack And Queries - JAG夏合宿2018 day2 D

D - Knapsack And Queries
良く出来てるなぁ。

queueをstack2つで実現できるっていう知る人ぞ知るテクがある。
純粋関数型データ構造界隈とかで有名らしい。

どうやるかというと、

  • queueにpush
    • 単にstack2にpushする
  • queueからpop
    • stack1が空でない:stack1からpop
    • stack1が空:「stack2からpopしてstack1にpush」をstack2が空になるまでやる

計算量は、各要素が高々2回ずつしかpushされないことから、均しで線形であることがわかる。

f:id:snuke:20180918164151p:plain
[図1: 2つのstackからqueueを作る様子]

これをそのまま使うと例えば以下の問題が解ける。

  • 正整数の集合Sがあり、最初は空である。Sに対する以下のクエリを処理せよ。
    • push x:Sにxを追加する
    • pop:Sの要素のうち最も古いものを削除
    • gcd:Sの要素のgcdを答える
  • 計算量は O(クエリ数*gcdの計算量)

ただ、これだとsegtreeを使ったりするのと区別ができないのでコンテストで出題するのは難しい。

元の問題の解法に話を戻すと、あとはdpテーブル2つから答えを計算できればいいけど、これは片方のテーブルを全部舐めてもう片方のテーブルでスライド最小値をすれば良い。
なるほど、このテクでは2つの要素をマージできる必要はなくて、2つの要素から答えを計算できれば十分なんだなぁ。
作問が上手すぎる。

ちなみに、dequeもstack2つで実現できる。(償却計算量は同様に線形)

www.slideshare.net

CodeForces #485 Div1 C AND Graph

Um_nik回のCが面白かったので。
あと、自分の解法が想定解と違ったっぽいので。

問題概要

n ビットの二進数 a_im 個与えられる。
a_i \& a_j = 0 のとき頂点 i,j に辺を張ったグラフの連結成分数を求めよ。

制約

  • 1 \leq n \leq 22
  • 1 \leq m \leq 2^n
  • 0 \leq a_i \lt 2^n
  • a_i はdistinct
続きを読む