(旧) kano-e no memo

こっちは更新してません

部分和問題をSQLで解く

サービス終了のお知らせ - Yahoo!ジオシティーズ
部分和問題

部分和問題とは、任意の数の整数の集合から適当な部分集合を選んで、その部分集合の和が与えられた数 N に等しくなるような組み合わせが存在するか調べる問題。

リンク先の例だと、1以上5以下の整数の集合から、1つ以上選んだ整数の合計が7になる組合せをあげる、というものでした。
解答について、詳しくは解答ページを見ていただくとして、

しかし、n 個の要素を含む集合について n 回クエリを発行せねばならないというのも大変です。うまく一つの SQL で解く方法はないでしょうか?

と、あったもので、気になって実際に自分でSQLを書いてみました。

組合せについて考える

そもそも、クエリをn回発行するのは、「1個以上の整数を選択する」と考えるから1個の場合、2個の場合、3個の場合。。。。。。と場合わけをしなければいけないのであって、「n個のうち、選択される整数と選択されない整数の組合せ」と考えれば、一回のクエリで全部の組合せがとれるんじゃないか、と思ったわけです。
わかりにくいですよね、自分でも何言ってんだかよくわかんないです。
ともかく、SQLを書いてみました。
あ、そうだ。今回、MySQLでしか確認してないんで、MySQLでしか使えないものとか使ってる可能性あります。

下準備

まず、選択する・しないはbool値なので、0と1の数を用意する。
ついでに、1から5までの集合のテーブルもつくっておく。

CREATE TEBLE bool(id int not null);
INSERT INTO bool VALUES (1), (0);
CREATE TEBLE num5(id int not null);
INSERT INTO num5 VALUES (1), (2), (3), (4), (5);

「n個のうち、選択される整数と選択されない整数の組合せ」

次に、今回の問題に合わせて、5個の組合せを作る。

SELECT * FROM bool a, bool b, bool c, bool d, bool e;
+----+----+----+----+----+
| id | id | id | id | id |
+----+----+----+----+----+
|  0 |  0 |  0 |  0 |  0 | 
|  1 |  0 |  0 |  0 |  0 | 
|  0 |  1 |  0 |  0 |  0 | 
|  1 |  1 |  0 |  0 |  0 | 
|  0 |  0 |  1 |  0 |  0 | 
(中略)
|  0 |  0 |  1 |  1 |  1 | 
|  1 |  0 |  1 |  1 |  1 | 
|  0 |  1 |  1 |  1 |  1 | 
|  1 |  1 |  1 |  1 |  1 | 
+----+----+----+----+----+
32 rows in set (0.10 sec)

さて、ここから、どうするか。
このidの縦列に左から1,2,3,4,5を割り振らなくてはいけない。

組合せと実際の数字を関連付ける

さっきのクエリを元に、こんな感じにjoinする。

SELECT *
  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 *
      FROM num5
      WHERE id = 1
    ) as n1
    ON a = 1
  LEFT JOIN (
    SELECT *
      FROM num5
      WHERE id = 2
    ) as n2
    ON b = 1
  LEFT JOIN (
    SELECT *
      FROM num5
      WHERE id = 3
    ) as n3
    ON c = 1
  LEFT JOIN (
    SELECT *
      FROM num5
      WHERE id = 4
    ) as n4
    ON d = 1
  LEFT JOIN (
    SELECT *
      FROM num5
      WHERE id = 5
    ) as n5
    ON e = 1
;

+---+---+---+---+---+------+------+------+------+------+
| a | b | c | d | e | id   | id   | id   | id   | id   |
+---+---+---+---+---+------+------+------+------+------+
| 0 | 0 | 0 | 0 | 0 | NULL | NULL | NULL | NULL | NULL | 
| 1 | 0 | 0 | 0 | 0 |    1 | NULL | NULL | NULL | NULL | 
| 0 | 1 | 0 | 0 | 0 | NULL |    2 | NULL | NULL | NULL | 
| 1 | 1 | 0 | 0 | 0 |    1 |    2 | NULL | NULL | NULL | 
| 0 | 0 | 1 | 0 | 0 | NULL | NULL |    3 | NULL | NULL | 
(中略)
| 1 | 1 | 0 | 1 | 1 |    1 |    2 | NULL |    4 |    5 | 
| 0 | 0 | 1 | 1 | 1 | NULL | NULL |    3 |    4 |    5 | 
| 1 | 0 | 1 | 1 | 1 |    1 | NULL |    3 |    4 |    5 | 
| 0 | 1 | 1 | 1 | 1 | NULL |    2 |    3 |    4 |    5 | 
| 1 | 1 | 1 | 1 | 1 |    1 |    2 |    3 |    4 |    5 | 
+---+---+---+---+---+------+------+------+------+------+
32 rows in set (0.00 sec)

大分目的に近付いている気がする。
さらに、この後足し算をすることを考えると、NULLは邪魔なので0になおしておく。

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 (...(以下同じ)

+---+---+---+---+---+------+------+------+------+------+
| a | b | c | d | e | n1   | n2   | n3   | n4   | n5   |
+---+---+---+---+---+------+------+------+------+------+
| 0 | 0 | 0 | 0 | 0 |    0 |    0 |    0 |    0 |    0 | 
| 1 | 0 | 0 | 0 | 0 |    1 |    0 |    0 |    0 |    0 | 
| 0 | 1 | 0 | 0 | 0 |    0 |    2 |    0 |    0 |    0 | 
| 1 | 1 | 0 | 0 | 0 |    1 |    2 |    0 |    0 |    0 | 
| 0 | 0 | 1 | 0 | 0 |    0 |    0 |    3 |    0 |    0 | 
(中略)
| 1 | 1 | 0 | 1 | 1 |    1 |    2 |    0 |    4 |    5 | 
| 0 | 0 | 1 | 1 | 1 |    0 |    0 |    3 |    4 |    5 | 
| 1 | 0 | 1 | 1 | 1 |    1 |    0 |    3 |    4 |    5 | 
| 0 | 1 | 1 | 1 | 1 |    0 |    2 |    3 |    4 |    5 | 
| 1 | 1 | 1 | 1 | 1 |    1 |    2 |    3 |    4 |    5 | 
+---+---+---+---+---+------+------+------+------+------+
32 rows in set (0.00 sec)

いい感じ。

仕上げ

後は、n1からn5までを足し算して、7になる組合せを見付ければ良いだけ。

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 *
          FROM num5
          WHERE id = 1
        ) as n1
        ON a = 1
      LEFT JOIN (
        SELECT *
        FROM num5
        WHERE id = 2
      ) as n2
      ON b = 1
      LEFT JOIN (
        SELECT *
          FROM num5
          WHERE id = 3
        ) as n3
        ON c = 1
      LEFT JOIN (
        SELECT *
          FROM num5
          WHERE id = 4
        ) as n4
        ON d = 1
      LEFT JOIN (
        SELECT *
          FROM num5
          WHERE id = 5
        ) as n5
        ON e = 1
  ) as ssp
  WHERE
    n1 + n2 + n3 + n4 + n5 = 7
;

+------+------+------+------+------+
| n1   | n2   | n3   | n4   | n5   |
+------+------+------+------+------+
|    1 |    2 |    0 |    4 |    0 | 
|    0 |    0 |    3 |    4 |    0 | 
|    0 |    2 |    0 |    0 |    5 | 
+------+------+------+------+------+
3 rows in set (0.01 sec)

できたーっ!

蛇足

本当言うと、もうちょっとすっきりと書けるんじゃないかなーと思っています。

  LEFT JOIN (
    SELECT *
      FROM num5
      WHERE id = 1
    ) as n1
    ON a = 1

これを5回書いている辺り。
でも、今回はこの方法しか思い付かなかったです。うーん。

さらに蛇足

ちなみに、今回、この思考にたどり着くまでに、どんなクエリを書いたかって言うと。

SELECT n1.id, n2.id, n3.id, n4.id, n5.id
  FROM num5 n1, num5 n2, num5 n3, num5 n4, num5 n5
  WHERE
    coalesce(n1.id,-99) < coalesce(n2.id,-98)
  AND
    coalesce(n2.id,-98) < coalesce(n3.id,-97)
  AND
    coalesce(n3.id,-97) < coalesce(n4.id,-96)
  AND
    coalesce(n4.id,-96) < coalesce(n5.id,-95)
  AND
    ( coalesce(n1.id,0)
    + coalesce(n2.id,0)
    + coalesce(n3.id,0)
    + coalesce(n4.id,0)
    + coalesce(n5.id,0) ) = 7;

coalesceだらけ。
一目見ただけで、思考の迷宮をさまよっていたことが伺えるひどいSQLです。
確かに、coalesceは便利だけれどもさ、この使い方ってどうよ。

追記

  LEFT JOIN (
    SELECT *
      FROM num5
      WHERE id = 1
    ) as n1
    ON a = 1

この部分は

  LEFT JOIN (
    SELECT 1 as id
    ) as n1
    ON a = 1

にしちゃえば良いんだ。
それと、蛇足であげたcoalesceだらけのSQLですが、あれの前提になっているnum5テーブルは、今回使用したnum5テーブルとちょっと違っています。

CREATE TABLE num5(id int);
INSERT INTO num5 VALUES (NULL), (1), (2), (3), (4), (5);

という具合に、NULLがあるのでした。
1から5までと、どの数字も選択しない、というのを現すとこうなるかなーと思って、そこからあのcoalesceだらけが生まれたのでした。
今まであやふやだったのに、coalesceの綴り覚えちゃった。