Tournamentの最小道被覆問題 (Minimum Path Cover Problem)
情報工学工房の問題で面白かったのでまとめます.
POJ : 1776 -- Task Sequences
問題(意訳)
単純有向グラフ G=(V, A) が与えられる.ただし,Gの任意の2頂点 に対して, または を満たす(どちらも満たさない場合はない).
Gの最小道被覆 (Minimum Path Cover) を求めよ.
入力は の隣接行列で与えられる.成分(i, j)は0か1で,頂点iとjの間に弧があれば1,無ければ0である.
- 制約:
方針
いくつか用語の説明をします.
[最小道被覆(Minimum Path Cover)]
G = (V, A) の道被覆(Path Cover)とは点素パスの集合PでVの各頂点がPに属するちょうど1つの道に属するという性質を満たすもの.
点素パスとは頂点の重複がない道.
[Tournament]
有向グラフがTournamentであるとは無向完全グラフのすべての辺に向き付けをおこなったもの.
Tournament (graph theory) - Wikipedia
一般グラフの最小道被覆問題はNP困難なのですがTournamentでは多項式時間アルゴリズムが存在します.また,この問題で与えられるグラフはTournamentの拡大グラフなので解くことができます.競技プログラミングではDAGの最小道被覆問題を解くアルゴリズムがよく知られていると思います.
この問題で与えられるグラフの両方向に弧が存在する頂点間に対して,どちらか片方のみに弧があると考えてtournamentとして考えます.tournamentに対して次の定理が成り立つのでそれを用いて問題を解きます.
[定理 (Redei 1934)]
Tournamentにはハミルトン路が存在する
証明:
頂点数に関する帰納法で証明を行う.
(基底段階 |V| = 1, 2)
グラフを全て列挙することによって正しさが確認できる.
(帰納段階)
で成り立つと仮定してk+1の場合を証明する.
任意の頂点数k+1のTournament G=(A,V)を考える.
任意に頂点 を取り除いたグラフ G'を考えるとG'は頂点数kなので帰納法の仮定からハミルトン路 が存在する.
Pに を加えてGのハミルトン路Pを構成する.
1. の場合
P'の先頭に を加えて とする.
2. の場合
P'の後ろに を加えて とする.
3. それ以外
P'の先頭から添字の最も小さい頂点で からの弧が存在する頂点を とする.
仮定から 2 < k かつ であるからそのような頂点は存在する.
このとき, の選び方から である(最小性論法).
したがって, とする. □
最小道被覆問題はパスの数の最小化でしたのでハミルトン路が存在すればそれが最適解となります.したがって,アルゴリズムとしては上の証明のように頂点を1つずつハミルトン路を保つように加えていきます.
計算時間は です.より高速なアルゴリズムとして Bar-NoyとNor(1990)の のものが存在します(未読).
ソースコード
#include <iostream> #include <vector> using namespace std; int main() { cin.tie(0); ios::sync_with_stdio(false); int n; string s; while (cin >> n) { cin.ignore(); // Input Graph vector<vector<bool> > g(n, vector<bool>(n, false)); for (int i = 0; i < n; ++i) { getline(cin, s); for (int j = 0; j < n; ++j) { if (s[j << 1] == '1') g[i][j] = true; } } // Find a hamiltonian path in a like tournament g // n == 1 if (n == 1) { cout << "1\n1\n1\n"; continue; } // 2 <= n vector<int> next(n, -1); int head = 0, tail = 0; // add v in a path (head -> ... -> tail) for (int v = 1; v < n; ++v) { if (g[v][head]) { // v -> head -> ... -> tail next[v] = head; head = v; } else if (g[tail][v]) { // head -> ... -> tail -> v next[tail] = v; tail = v; } else { // head -> ... -> v_cur-1 -> v -> v_cur -> ... -> tail for (int cur = head; ; cur = next[cur]) { if (g[cur][v] && g[v][next[cur]]) { next[v] = next[cur]; next[cur] = v; break; } } } } // Output cout << "1\n" << n << '\n'; for (int cur = head; cur != -1; cur = next[cur]) cout << cur + 1 << (cur == tail ? '\n' : ' '); } return 0; }