読者です 読者をやめる 読者になる 読者になる

naoya_t@hatenablog

いわゆるチラシノウラであります

2-SATと強連結成分分解

TopCoder Single Round Match Practice Room 過去問探訪 Algorithm グラフ理論

ふと2-SATの事が気になって、復習がてらPractice RoomでSRM464のDiv1 Medium問題を開いてみた。(ちなみにSRM464には出場している

SAT (充足可能性問題, SATisfiability problem) についてはここの読者の皆さんはご存知とは思います。NP完全問題でおなじみのあれです

(x_0 ∨ ¬x_1) ∧ (¬x_0 ∨ ¬x_3) ∧ ...
みたいな論理式(乗法標準形)を満たす真偽値 x_0, x_1, ... の組み合わせが存在するか否か。(存在する場合、その組み合わせを知りたかったりする)

上の例ではすべての節が(高々)2変数なので2-SATと呼ばれる。
2-SATは線形時間で解けるらしい)。

  • 論理和 x ∨ y が論理包含 ¬x⇒y (または ¬y⇒x)に書き換え可能なことを利用して、論理式を有向グラフに変換してみたり
  • 出来上がったdagを強連結成分分解してみる
  • 同じ強連結成分に含まれるノード(が表すリテラル、すなわちxなり¬yなり)は同じ真偽値をもつ。ということは、xと¬xが同じ強連結成分に含まれたらアウト。

2-SATも強連結成分分解も、蟻本§4-3「グラフマスターへの道」(p.285〜) で解説されている(のを以前読んだがすっかり忘れている。p.286のグラフをノートに書いてscc()を手動トレースして一度は納得したはずなのだが)。

Medium ("ColorfulDecoration", 550)

この問題で何が x ∨ y にあたるのか思いつかない…orz

http://d.hatena.ne.jp/komiyam/20110925/1316920692 を見たら、
「x と y が共存できない」¬(x ∧ y) が ¬x ∨ ¬y に等しいことを利用すればいいらしい。

するとこれは 「x⇒¬y かつ y⇒¬x」に書き直すことができる。

書いてみたコード

  • 強連結成分分解ではなく、x から ¬x に行く経路(とその逆)が存在するかをWFで求めている。O(n^3)ですけど。
  • passed system test
#include <vector>
using namespace std;

#define rep(var,n)  for(int var=0;var<(n);++var)
#define all(c)  (c).begin(),(c).end()
#define tr(i,c)  for(typeof((c).begin()) i=(c).begin(); i!=(c).end(); ++i)
#define mid(x,y) ((x)+((y)-(x))/2)
#define ev(v1,v2,n) for(int v1=0;v1<(n)-1;++v1)for(int v2=v1+1;v2<(n);++v2)

vector<int> xa_, ya_, xb_, yb_;
vector<vector<int> > arc;
int n;

inline bool intersects(int xi, int yi, int xj, int yj, int d){
  int dx = abs(xi - xj), dy = abs(yi - yj);
  return (dx < d && dy < d);
}

inline void add_edge(int i, int j) {
  // ¬(i ∧ j)
  // ¬i ∨ ¬j
  // (i ⇒ ¬j) かつ (j ⇒ ¬i)
  arc[i][j<n ? j+n : j-n] = 1;
  arc[j][i<n ? i+n : i-n] = 1;
}

bool po(int d){
  if (d == 0) return true;

  arc.assign(n*2, vector<int>(n*2, 10000));
  rep(i,n*2) arc[i][i] = 0;

  ev(i,j,n) {
    if (intersects(xa_[i],ya_[i], xa_[j],ya_[j], d)) { // i && j が駄目
      add_edge(i, j);
    }
    if (intersects(xa_[i],ya_[i], xb_[j],yb_[j], d)) { // i && ¬j が駄目
      add_edge(i, n+j);
    }
    if (intersects(xb_[i],yb_[i], xa_[j],ya_[j], d)) { // ¬i && j が駄目
      add_edge(n+i, j);
    }
    if (intersects(xb_[i],yb_[i], xb_[j],yb_[j], d)) { // ¬i && ¬j が駄目
      add_edge(n+i, n+j);
    }
  }

  rep(k,n*2) rep(i,n*2) rep(j,n*2){
    arc[i][j] = min(arc[i][j], arc[i][k] + arc[k][j]);
  }

  rep(i, n) {
    if (arc[i][n+i] < 10000 && arc[n+i][i] < 10000) return false;
  }

  return true;
}

class ColorfulDecoration {
 public:
  int getMaximum(vector<int> xa, vector<int> ya, vector<int> xb, vector<int> yb) {
    n = xa.size();
    xa_ = xa; ya_ = ya; xb_ = xb; yb_ = yb;

    int lo = 0, hi = INT_MAX;
    while (true) {
      int mi = mid(lo, hi);
      if (mi == lo) break;
      if (po(mi))
        lo = mi;
      else
        hi = mi;
    }
    return lo;
  }
};

さて。
強連結成分分解をちゃんとやってみよう。

dagの強連結な部分集合:「有向グラフの部分集合のうち、それに含まれる任意の2頂点u, vを取った時 uからvへ辿り着く有向路が存在する」
強連結成分:強連結な部分集合のうち、これ以上頂点を付け足したら強連結でなくなってしまうもの

最大クリーク(無向グラフで、あらゆる2つの頂点を繋ぐ辺が存在する集合(=クリーク)のうち、これ以上頂点を付け足したらクリークでなくなってしまうもの)に似ている。

vector<vector<int> > arc;
vector<vector<int> > arc_r;
vector<int> cmp; // 属する強連結成分の、グラフ全体におけるトポロジカル順序

vector<bool> visited;
vector<int> vs; // post-order

void dfs(int v) {
  visited[v] = true;
  tr(it, arc[v]) {
    if (!visited[*it]) dfs(*it);
  }
  vs.push_back(v); // 帰りがけにvを記録。グラフの末端に近いものから並ぶ
}
void rdfs(int v, int k) {
  visited[v] = true;
  cmp[v] = k;
  tr(it, arc_r[v]) {
    if (!visited[*it]) rdfs(*it, k);
  }
}

int scc() { // 強連結成分分解
  vs.clear();

  visited.assign(n*2, false);
  rep(i,n*2) {
    if (!visited[i]) dfs(i);
  }

  visited.assign(n*2, false);
  int k=0;
  reverse(all(vs)); // グラフの末端から遠いものから始めたい
  tr(it, vs) {
    if (!visited[*it]) rdfs(*it, k++);
  }
  return k;
}

bool po(int d){
  if (d == 0) return true;

  arc.assign(n*2, vector<int>());
  arc_r.assign(n*2, vector<int>()); // 追加
  cmp.assign(n*2, -1); // 追加

  ev(i,j,n) {
    if (intersects(xa_[i],ya_[i], xa_[j],ya_[j], d)) {
    ...
  }

  scc(); // 強連結成分分解

  rep(i, n) {
    // x と¬x が同じ強連結成分に属してしまったらアウト
    if (cmp[i] == cmp[n+i]) return false;
  }

  /* 
  // 式を満たす具体的な真偽値を求める方法。
  //「 x を含む強連結成分のトポロジカル順序 cmp[x] が、¬x を含む強連結成分のトポロジカル順序 cmp[¬x] よりも後ろ」⇔「xは真」
  vector<bool> ans(n);
  rep(i, n) {
    ans[i] = (cmp[i] > cmp[n+i]);
  }
  cout << ans << endl;
  */

  return true;
}