最大クリークの途中で補グラフが出てくる話
kousuke.hatenablog.com
前回、ワーストケースでになると書いた。
FPTでの幅はほぼ極大グラフの数でよいとおもっているけど、それをなぜ補グラフでやろうとしているか?
簡単に言うと補グラフに辺があれば、極大集合が2つ以上あるから。
最大クリーク判定の式をおさらいする。は自身を除く全点に接続している頂点。
3番目をA式、4番目をB式と呼ぶ。
A式では全点につながった辺はまとめて処理でき、ここは分岐する必要がない。また、この処理で辺は一気に減っているけど、補グラフの辺は実は減ってない。このことからも極大クリークを示すのに適当そうな感じがする。またA式の処理により、B式を処理するときには、すべての頂点は必ずどこかの頂点とつながっていない。そのため、補グラフに辺がある。ということでここから補グラフで展開していく。
補グラフの辺の数をwとする。となる。
D式では元のグラフの辺を選んでいて、すべての頂点は必ず補グラフの辺を1本以上含んでいるので、D式処理後、必ず補グラフの辺は2つ以上減る。
計算量はざっとが想定される。
とはいえワーストケースがk/2で収まるので、準指数でも大きそう。
なのでP=NPか、P≠NPでも、比較的小さいところでより小さくなる関数じゃないかとおもっている。もしかすると虚数がでてきて、k/2とどちらか速い方の可能性もある。
最小頂点被覆の計算量でが示せればよいとしてCを求めるやつだと、そのままwに置き換えるとになるんでか、1ならP=NPかもしれない。
最大クリークの計算量とか(続き)
kousuke.hatenablog.com
大事なことに気が付いたので、前回の修正。
単純な無向グラフに対し、をサイズkのクリークが含まれるかどうかを判定する関数とする。
グラフGに自身を除く全頂点に接続する頂点がある場合、それらは必ず最大クリークに含まれる。そのような頂点集合をとすると、下記が成り立つ。
の場合を考える。
頂点vに隣接している頂点の集合をとし、に対し、とする。ならば、eを含むサイズ3のクリークが存在し、誘導部分グラフの中にeを含むクリークが全て含まれる。よって次式が成り立つ。
の頂点はすべてeに接続しているので、にサイズk-2のクリークがあればよいので、さらに次式のように変形できる。
終了条件も含め、下記のようになる。
またにサイズkのクリークが含まれない場合、Gからeを取り除いても最大クリークには影響しない。
は辺を評価していると見ることができる。とする。
であれば、即座にその辺を除去する前処理を行う。この前処理の結果、少なくともサイズ3のクリークをk-2個以上含む辺のみが残り、各頂点の次数はk-1以上になる。
また次数がk-1未満になる頂点はその時点で除去できる。そのため、前処理後のグラフに対し、すべての辺を評価しなくともよい。
評価する辺の集合が最小のものをとし、を取り除いたグラフをとする。
下式よりおよその辺を評価すれば良いことがわかる。
最悪でも深さk/2の有界探索木だとおもうのでの目がある。
結局FPTなんだけど、そのパラメーターは極大クリークの数に依存する。上記のワーストケースは常にの場合で、どんなにクリークサイズが大きくても、完全グラフ-数辺みたいのだとあっという間に終わる。
前回は全点に接続する頂点を書いてなくて、終了条件に完全グラフって書いてたけど、これでぐっと見通しがよくなった。
次は補グラフでの最大独立集合を考えることにする。
を使った前処理の実装例
#!/usr/bin/env ruby require 'optparse' def shave_by_clique(g, eset, c) eset.reject {|u,v| (g[v] & g[u]).size < c } end def build_graph(g,vset,eset) vset.each {|v| g[v] = [] } eset.each {|u,v| g[v] << u; g[u] << v } end c = 3 opt = OptionParser.new opt.on('-c C', 'lower number bounds of 3-cliques.'){|v| c = v.to_i } opt.parse!(ARGV) n, m = gets.split.map(&:to_i) V = n.times.to_a E = m.times.map { gets.split.map(&:to_i) } G = {} eset = E build_graph(G,V,E) loop do shaved = shave_by_clique(G, eset, c - 2) if eset.size == shaved.size break end G.clear build_graph(G, V, shaved) eset = shaved end vset = eset.flatten.uniq.sort index = {} vset.each_with_index{|v,i| index[v] = i } puts "#{vset.size} #{eset.size}" puts eset.map{|e| e.map{|v| index[v] }}.map{|e| e.join(' ')} puts "# #{vset.join(' ')}"
グラフと補グラフの関係
n頂点の完全グラフを、独立グラフ(頂点のみで辺がないグラフ)をとする。
n頂点のグラフがある。
このときからへひとつづつ辺を消していく順序集合のうち、を含む順序の集合を定めることができる。
このときのどの順序を通ってもの補グラフはない。
そのためのような構造を考えることができる。
の辺の数はでと位相がずれていて、でに戻るので、上の構造はと同相と考えることができそう。
同様にも位相が離れているとみることができそう。
また最大クリーク問題を考えると頂点がなくなる方向への順序集合(こちらは頂点集合)もある。
最大クリーク問題の解はとの方向が一致することができる最大のnとみることができる。
なんとなくグラフを複素数の多項式として表せるんじゃないかとおもっていて、上の式を頂点方向と辺方向で全微分とかすると、その極大値が最大クリークにならないだろうかとかおもっていたりする。
またイメージとしてはに対し、への自己ループの輪()とを結ぶ輪が通っており、が縮もうとするときの緊張関係のように考えることができそう。
追記
ないとおもったけど、同型ならあるんだよね。とか考えると同型ならのなかにあるわ。
P=NPだと夢がある。
kousuke.hatenablog.com
頂点に関しては極大グラフが1つになるまでは(底を小さくするという意味ではもう少し改善はあるのだろうけど)、たぶん最速だとおもう。
あとは補グラフの辺数wをなるべく少なく消しながら、進めばよい。
これは一緒に補グラフの最大独立問題をやってるに等しい。
少なくとも、NP完全問題が難しいのは、頂点に関するものと辺に関するものを両方一緒にやるからというのが現在の認識。
全部辺の問題に還元しながらやっていくのがたぶんよい。
追記
下記の資料をみていて、葉最大全域木でなんかインスピレーション湧いた。
補グラフの辺はなんで、これを0に近づけていくと良いし、葉最大で葉を指定するのは難しいけど、次数使って辺をうまく重みづけできそうだし、その最小全域木に沿って処理できれば、Dinkelbach algorithmで結構高速にできそう。
追記2
最大クリーク問題である条件下で、多項式時間で計算量一気に短縮できる処理があって、それを常に続けられると多項式時間になるのになぁとおもっている。
最大クリークの計算量とか
修正しました。
kousuke.hatenablog.com
(以下、古い内容)
をサイズkのクリークが含まれるかどうかを判定する関数とする。(0: 含まない or 1 : 含む)
最大サイズkのクリークを含む、単純な無向グラフがあるとする。頂点vに隣接している頂点の集合をとし、に対し、とする。ならば、eを含むサイズ3のクリークが存在し、誘導部分グラフの中にeを含むクリークが全て含まれる。よって次式が成り立つ。
の頂点はすべてeに接続しているので、にサイズk-2のクリークがあればよいので、さらに次式のように変形できる。
前式によりは再帰的に求めることができることがわかる。
先程のの表記を拡張して、単にサイズcのクリークをと表記し、その全点から接続している頂点の集合をと書くことにすると、再帰の深さtを用いて次のように変形できる。
にサイズkのクリークが含まれない場合、eを消しても最大クリークには影響しない。Gからi個の辺を除去したものをとすると次式のように変形できる。iはに含まれる辺の最小のインデックスの意。
終了条件も合わせて下記のようになる。
深さk/2の有界探索木だとおもうので、そうするとだとおもう。
これに加えてサイズkのクリークはk-1以上の次数を必ず持つので次数k-1未満の点を除去していってもなくならない。この処理をとすると下記が成り立つ*1。
この前処理の結果、次数がk-1未満の頂点は全てなくなる。特にkが最大クリークサイズよりそれなりに大きい場合はとなり、前処理の中で完全に解けることになる(FPT?)。
また辺を評価する処理が進むにつれて辺はなくなっていき次数がk-1未満になる点はその時点で除去できる。
そのため、すべての辺を評価しなくとも、各頂点で個の辺だけ評価すればよい。
もうひとつパラメーターがあって、それが極大クリークの数になる。これをwとするとtに対し、w'≦wなんだけどこれを上手く処理できると多項式の目が出てくるかなと思っている。
補グラフの辺を変わりに評価対象にすればよいだろうか。
*1:これ自体は彩色問題でも成立する
続々々 最大クリーク問題(2)
すこしコードを変更している。興味ある人はpaiza.ioで動かしてみてほしい。
https://paiza.io/projects/14TYw6RR2KIPBLRjSNCPrg
V=1000、E=9988、20彩色可能、最大次数 69、次数の分布具合は下記のグラフを参考。
最大クリークは8。列挙しているわけではないので、複数ある可能性もある。
続々々 最大クリーク問題
基本的には前書いた内容。
kousuke.hatenablog.com
1000頂点20彩色可能なグラフ(|V|=1000,|E|=9988)から最大8クリーク探すのにpaiza.ioで1.85秒たぶん正しいとはおもうんだけど。
動作
0. 頂点数、辺数を確認して、0,1,2は確定させる。あとは3から最大次数で二分探索する。
1. contain_clique?で求めるクリークのサイズcを指定する。
2. critical_subgraph_by_degreeで頂点の次数が全てc-1以上となるような部分グラフを探す。なければこの時点で終わり
3. super_clique_exist?で指定したクリーク(最初は辺e)を含む、サイズcの拡大クリークを再帰的に探していく。
3-1. 指定したクリークが求めるサイズのクリークならtrueを返して終了。
3-2. +1サイズの拡大クリークがなければ、それは極大クリークで求めるサイズのクリークでないのでfalseを返して終了。
3-3. +1サイズの拡大クリークの数 + 現在のクリークサイズが、求めるクリークサイズc未満なら、その時点でサイズcのクリークには含まれないのでfalseを返して終了。
3-4. +1サイズの拡大クリークについて3を再帰的に行って、サイズcのクリークを探す。
4. 辺eを削除する
説明
サイズcのクリークは必ず全頂点がc-1の次数をもっているので、次数c-1未満の点を削除するというのを繰り返してもそのクリークはなくならない。このグラフをG'とする。
次にG'に含まれる辺eに対し、eを含むクリーク全体を評価していく。
e = {u,v}とし、u,vが接続している頂点の集合とする。
のどの頂点をeに加えてもサイズ3のクリークになり、これはサイズ3以上のクリークでも同様。
また辺eを含むクリーク全体はのべき集合の中にある。
基本的には計算量は極大クリークの数とその重複度合による。
ある辺eがサイズc未満の極大クリーク1つだけに含まれる場合、super_clique_exist?内で、3-2のケースにあたり再帰なしに枝刈される。同様に複数の極大クリークに含まれていて、再帰が深くなっていっても、極大クリーク1つに確定できるようになった時点でサイズc未満なら枝刈できる。
他にもの3つのクリークに含まれる場合(すべてサイズはc未満)でもならば、に含まれない頂点を選択した時点で枝刈出来る。3つ以上の極大クリークに含まれる場合も同様。
最終的にサイズcのクリークがなければ、辺eを消し*1、次の辺e'を含む評価に進む。
辺eを含んでいたクリークはだんだん細かく分断されていくけど、辺eを含んでいたクリーク全体は評価が終わっているので、他の辺e'を含むクリーク全体を評価する際に求めるサイズのクリークを小さくしたりはしない。また元々辺eを含んでいた極大クリークは小さく分断されていくことで他の極大グラフに含有されば、別の辺での計算量は次第に減っていくことになる。
最悪ケースでは全部のクリークを探すとかになるかもしれないけど、まあまあ速いとおもう。
もうすこし改良できる点。
二分探索をやるなかで、枝刈りしたところを何度も確認することになるので、枝刈時点のクリークと可能性のある最大サイズをキャッシュしておいて探すクリークのサイズが変更になったらそこから継続するとかすれば、もうすこし早くなりそう。
(追記)
各頂点毎にd(v)-c+2個の辺を評価して目的サイズのクリークがなければ、次数はc-1未満になり、その頂点は削除できるので上手く実装できれば少し速くなりそう。
コード
class Graph def initialize(vset, eset) @v = vset.to_a @e = eset.to_a @g = nil recreate_graph! end def recreate_graph! @g = {} @v.each{|v| @g[v] = []} @e.each{|u,v| @g[u] << v; @g[v] << u } nil end def _super_clique_exist?(clq, limit = clq.size + 1, cache = {}, &found) clqs = clq.sort.join(' ') if cache[clqs] return false else cache[clqs] = true end if clq.size == limit found && found.call(clq) return clq end vset = clq.inject(verteces) {|s, v| s & self[v] } if vset.size >= limit - clq.size vset.any? {|v| _super_clique_exist?(clq + [v], limit, cache, &found) } else false end end protected :_super_clique_exist? def contain_clique?(c, &found) g = critical_subgraph_by_degree(c - 1) g.edges.find do |e| u,v = *e g[u].delete(v) g[v].delete(u) g._super_clique_exist?(e, c, {}, &found) end end def empty? verteces.empty? end def critical_subgraph_by_degree(d) g = self[verteces {|v| degree(v) >= d }] loop do prev_size = g.verteces.size g.verteces.keep_if {|v| g.degree(v) >= d } g.edges.keep_if {|e| e.all? {|v| g.verteces.include?(v)} } g.recreate_graph! if g.verteces.size == prev_size break elsif g.verteces.empty? break end end g end def verteces(&block) block.nil? ? @v : @v.select(&block) end def edges(&block) block.nil? ? @e : @e.select(&block) end def degree(i) @g[i].size end def adjacents(v) @g[v] end def [](v_or_vset) if v_or_vset.is_a?(Array) induced_subgraph(v_or_vset) elsif v_or_vset.is_a?(Integer) adjacents(v_or_vset) end end def induced_subgraph(vset) eset = edges {|e| e.all?{|v| vset.include?(v)} } self.class.new(vset, eset) end def inspect @g.inspect end end n, m = gets.split.map(&:to_i) V = n.times.to_a E = m.times.map { gets.split.map(&:to_i) } G = Graph.new(V,E) max_degree = G.verteces.map {|v| G.degree(v) }.max max_clique = nil if G.empty? max_clique = [] elsif G.edges.empty? max_clique = [G.verteces.first] else max_clique = G.edges.first (3 .. max_degree).bsearch do |k| !G.contain_clique?(k) do |clq| max_clique = clq if clq.size > max_clique.size end end end puts max_clique.size puts max_clique.sort.inspect
*1:ちなみに実装としては4は3の前に行っている