部分和問題を解くアルゴリズムをプログラミングしてみる

部分和問題

ちょうど部分和問題について触れることがあったので,久しぶりにプログラミングの練習.

部分和問題とは,

与えられた n 個の整数 a1,...,an から部分集合をうまく選んで、その集合内の数の和が与えられた数 N に等しくなるようにできるかどうかを判定する問題

という問題.(部分和問題 - Wikipediaより)

例えば,{1,2,3,4,5,6}のうちいくつかを足して21にできるかどうか,という問題.

これを解くアルゴリズムとして,

  • {1,2,3,4,5,6}の一つ一つについて選ぶか選ばないかを決め,
  • そのそれぞれの選び方で選んだ数の和のうち,21になるものがあればYESと判定
  • 一つも21になるものがなければNOと判定

を採用.

選ぶか選ばないか2通りの選択が,{1,2,3,4,5,6,7}の集合の要素数分だけあるから,
素数が増えれば計算量は指数関数的に増えていく...

PHPでプログラミング

整数の集合は配列(変数$S)で,与えられた数は変数$tで宣言.

まずはアルゴリズムの通りに実装したいので,再帰関数を使用.
集合から一つ数を取り出して,その数を選ぶ場合と選ばない場合に再帰を用いて分岐.

選ぶ数は別の配列に保存して,集合が空になったときに選んだ数を足し合わせて判定.

<?php
$S = array(1, 2, 3, 4, 5);
$t = 21;

bubun($S, array());
print "NO\n";

function bubun($S, $a){
	global $t;
	if(empty($S)){
		$i = array_sum($a);
		if($i == $t){
			var_dump($a);
			exit();
		}
	}else{
		$i = array_pop($S);
		bubun($S, $a);
		array_push($a, $i);
		bubun($S, $a);
	}
}
?>

問題としては,選んだ数を表示しなくてもいいから(判定するだけでよい),
変数$tから値を引く・引かないで選ぶ・選ばないを分岐させて,

<?php
$S = array(1, 2, 3, 4, 5, 6);
$t = 21;

bubun($S, $t);
print "NO\n";

function bubun($S, $t){
	if($t == 0){
		print "YES\n";
		exit();
	}elseif(empty($S)){
		return;
	}
	$i = array_pop($S);
	bubun($S, $t);
	bubun($S, $t-$i);
}
?>

再帰ではなくて,繰り返しでできないか,ということで,,
新たに,足し合わせた数を格納する配列を用意して,

  • 集合から一つずつ値を取り出す
    • 配列をコピー
    • 一方には取り出した値を足し合わせる
    • 配列をくっつける

という感じで,足す・足さないで選ぶ・選ばないを実現

<?php
$S = array(1, 2, 3, 4, 5, 6, 7);
$t = 21;
$sum = array();

for($temp = $S; !empty($S); $temp = $sum){
	$a = array_pop($S);
	for($i=0; $i<count($sum); $i++) $sum[$i] = $sum[$i] + $a;
	$sum = array_merge($sum, $temp);
}

array_search($t, $sum) ? print "YES\n": print "NO\n";
?>

SQLで実現

部分和問題でググってたら,SQLで解くという話がおもしろかった.

サービス終了のお知らせ - Yahoo!ジオシティーズ
http://d.hatena.ne.jp/kano-e/20070809/1186672517

CやPHPと違って構造化プログラミングができないから,解くのが難しい.
上記のリンクのkano-e no memoさんの方法が一番良いんじゃないかと.

  • 0と1が入ったbool表を用意
  • bool表を集合の要素回だけ結合(直積して要素数桁のすべてのビットパターンを生成)
  • 1が現れた場所に各要素を外部結合
  • 選んだ要素を足し合わせて,目的の行を選択

って感じかな.

外部結合部分で少し削れそうな部分があったので,いじってみる.
集合の各要素はSQL文の中に書いちゃって,それを保存しておくテーブルはいらなくなるかも.

SELECT n1, n2, n3, n4, n5
  FROM (
    SELECT
      a, b, c, d, e,
      coalesce(n1.id, 0) n1,
      coalesce(n2.id, 0) n2,
      coalesce(n3.id, 0) n3,
      coalesce(n4.id, 0) n4,
      coalesce(n5.id, 0) n5
      FROM (
        SELECT a.id a, b.id b, c.id c, d.id d, e.id e
          FROM bool a, bool b, bool c, bool d, bool e
      ) as f
      LEFT JOIN ( SELECT 1 id ) as n1 ON a = 1
      LEFT JOIN ( SELECT 2 id ) as n2 ON b = 1
      LEFT JOIN ( SELECT 3 id ) as n3 ON c = 1
      LEFT JOIN ( SELECT 4 id ) as n4 ON d = 1
      LEFT JOIN ( SELECT 5 id ) as n5 ON e = 1
  ) as ssp
  WHERE
    n1 + n2 + n3 + n4 + n5 = 7
;