challenge 水の移し替えパズル

A, B, Cの容器があり,それぞれ水が4L, 2L, 10L入っている. ここで次の操作を繰り返す.

(*)「A, B, Cのどれか二つの容器から水を1Lずつくみ上げ,残りの容器に移す.」

たとえばA, Bから1Lずつくみ上げて移せばA=3L, B=1L, C=12Lとなる. くみ上げる前の容器には必ず水が入っているとする.

(*)を繰り返してどれか一つの容器にのみ水がはいっている状態にする最小手数を求めよ.

可能ならA=827392L,B=65536L,C=122880Lのときも求めよ.


このお題は光成さんの投稿を元に作成しました。ご協力ありがとうございます。

Posted feedbacks - Nested

Flatten Hidden
あってるか自信なし。
愚直な方法でやっています。

どこのカップに水を入れるかで、無限ループするケースがあるので、
並列にまわして、一番初めに答えを出したやつを帰すというクソ仕様。

ちなみに、[3,1,12]は12回
[827392,65536,122880]は、827392回
が最小(?)手数でした。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import copy
import threading
import time

class calcThread(threading.Thread):
    def __init__(self, cups, result):
        self.cups = cups
        self.result = result
        threading.Thread.__init__(self)
    def run(self):
        count = 0
        cups = self.cups
        while(cups[1]!=0 or cups[2] !=0):
            count+=1

            if cups[1] and cups[2] :
                cups[0] += 2
                cups[1] -= 1
                cups[2] -= 1
                continue

            if cups[1] == 0:
                cups[1] += 2
                cups[0] -= 1
                cups[2] -= 1
                continue

            if cups[2] == 0:
                cups[2] += 2
                cups[0] -= 1
                cups[1] -= 1
                continue
        self.result[0] = count

def water_move(cups):
    count = [[0],[0],[0]]
    threadholder = []
    for x in xrange(3):
        c = copy.copy(cups)
        c[0], c[x] = c[x], c[0]
        threadholder.append(calcThread(c, count[x]))
        threadholder[-1].setDaemon(True)
        threadholder[-1].start()

    while (1):
        for x in range(3):
            if not threadholder[x].isAlive():
                return count[x][0]
        

#print water_move([3,1,12])
print water_move([827392,65536,122880])
ちなみに、[3,1,12]は12回
↑答えのペアはあってるんですが要求されてる答えとは違いますね。
[4,2,10]はbに水を集めて、10回でした。
water_move([1,1,4]) で 4 が返ってきません?
スレッドを三つ同時に走らせて、一番速く帰ってきた奴を採用しているんですが、
処理時間が短すぎて、スレッドの終了チェック前にすべてのスレッドが終了しているようです。
そのため、一番目のスレッド(カップaに水を集める)の答えが採用されてしまっているようです。

45行目から下のコードにすることで、1がちゃんと帰ってきます。

一番乗り目指して愚直にやったのが、仇になったなぁ。
1
2
3
4
5
6
7
8
9
    time.sleep(0.1)
    result = []
    while (1):
        for x in range(3):
            if not threadholder[x].isAlive():
                result.append(count[x][0])
        if len(result) :
            break
    return min(result)
水の量を均一にすればよいのだから最小公倍数とか使えそうなんだけど
バカにはよくわからないので一つ一つでカウントしていった。
一番多いところ-二番目に多いところの差を
半分ずつわければもうちょっと計算減らせそうだけど。。。

4L,2L,10L ではBがのこって 10回?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <stdint.h>

#define BAKET_SWAP(a,b) { uint32_t *work = *(a); *(a) = *(b); *(b) = work; }

void baket_sort( uint32_t **baket[] )
{
  int is_changed = 1;
  while( is_changed == 1 )
  {
    is_changed = 0;
    if( (*(baket[0])) < (*(baket[2])) ){ BAKET_SWAP(baket[0], baket[2]); is_changed = 1;}
    if( (*(baket[0])) < (*(baket[1])) ){ BAKET_SWAP(baket[0], baket[1]); is_changed = 1;}
    if( (*(baket[1])) < (*(baket[2])) ){ BAKET_SWAP(baket[1], baket[2]); is_changed = 1;}
  }
}

uint64_t play_game(uint32_t A, uint32_t B, uint32_t C)
{
  uint32_t *baket[] = {&A, &B, &C};
  uint64_t count = 0;

  while( 1 )
  {
    /* 水の量が均一になれば終わり */
    if( A == B )
    {
      printf("のこるのは C\n");
      count += A;
      break;
    }
    if( A == C )
    {
      printf("のこるのは B\n");
      count += A;
      break;
    }
    if( B == C )
    {
      printf("のこるのは B\n");
      count += B;
      break;
    }
    /* 昇順にバブルソート */
    baket_sort( &baket );
    (*(baket[0])) --; (*(baket[1])) --;
    (*(baket[2])) += 2;
    count ++;
  }
  return count;
}

int main(int argc, char *argv[])
{
  printf("ans = %lld\n", play_game(4,2,10));
  printf("ans = %lld\n", play_game(3,1,12));
  return 0;
}
ちょこちょことバグかあるので修正
それにしても、よくcoreをはかず動いていたものです^;;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--- doukaku73.c.org    2007-10-29 22:26:48.875000000 +0900
+++ doukaku73_2.c    2007-10-30 20:51:49.375000000 +0900
@@ -9,9 +9,9 @@
   while( is_changed == 1 )
   {
     is_changed = 0;
-    if( (*(baket[0])) < (*(baket[2])) ){ BAKET_SWAP(baket[0], baket[2]); is_changed = 1;}
-    if( (*(baket[0])) < (*(baket[1])) ){ BAKET_SWAP(baket[0], baket[1]); is_changed = 1;}
-    if( (*(baket[1])) < (*(baket[2])) ){ BAKET_SWAP(baket[1], baket[2]); is_changed = 1;}
+    if( *(*(baket[0])) < *(*(baket[2])) ){ BAKET_SWAP(*(baket[0]), *(baket[2])); is_changed = 1;}
+    if( *(*(baket[0])) < *(*(baket[1])) ){ BAKET_SWAP(*(baket[0]), *(baket[1])); is_changed = 1;}
+    if( *(*(baket[1])) < *(*(baket[2])) ){ BAKET_SWAP(*(baket[1]), *(baket[2])); is_changed = 1;}
   }
 }
 
@@ -37,7 +37,7 @@
     }
     if( B == C )
     {
-      printf("のこるのは B\n");
+      printf("のこるのは A\n");
       count += B;
       break;
     }
あれこれ考えていたらこれで解が出るような気がしました。
探索を書かせるのが趣旨だったらごめんなさい。
1
2
3
(defun water-puzzle (a b c)
  (loop for (x y) in `((,a ,b) (,b ,c) (,c ,a))
    if (zerop (mod (- x y) 3)) minimize (max x y)))
差が全部3の倍数であるときの挙動が(僕がCommonLispを読めないため)よくわかりませんでした。
その場合
(min (max a b) (max b c) (max c a))
と同じになりますから、a, b, c のうち二番目に大きいものが返るはずです。
 愚直に数えてみたら時間がかかりすぎたり無限ループしたりとロクなことが無かったので,kozimaさんの(#3557)を丸写し。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function doukaku73_bad(a, b, c){
  var f = function(x, y){ return x - y }, v = [a, b, c].sort(f), i = 0, d;
  for(; v[0] < v[1] && v[1] < v[2]; i++){
    v[0] += 2, v[1]--, v[2]--;
    if(v[0] > v[1]) v.sort(f);
  }
  return i + v[1];
}
/// ↓ ///

function doukaku73(a, b, c){
  for(var v = arguments, r = [], i = 3, j; i--;)
    r[i] = (v[i] - v[j = (i + 1) % 3]) % 3 ? Infinity : Math.max(v[i], v[j]);
  return Math.min.apply(null, r);
}
Squeak Smalltalk で。kozima さんの #3557、そのまんまです。幅優先探索のも書きましたが、こちらのほうがずっとシンプルでいいですね。
1
2
3
4
5
6
| start result |
start := #(827392 65536 122880).
result := start max.
start combinations: 2 atATimeDo: [:pair |
    (pair first - pair second isDivisibleBy: 3) ifTrue: [result := result min: pair max]].
^result
愚直に一リットルずつ動かして全可能性を網羅しています(ただし、往復の移動は避けています)。

4L, 2L, 10L の時は10でした。
827392L,65536L,122880Lのときはスタックが溢れて求められません。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Sample {
    private static int[] volume = {4, 2, 10};
    private static boolean[] dFlag = new boolean[3];
    private static int minStep = Integer.MAX_VALUE;
    public static void move(int dest, int step) {
        boolean so = dFlag[dest];
        try {
            volume[dest] += 2;
            volume[(dest + 1) % 3]--;
            volume[(dest + 2) % 3]--;
            dFlag[dest] = true;
            int emptyCount = 0;
            int destCount = 0;
            for (int i = 0; i < 3; i++) {
                if (volume[i] < 0) {
                    return;
                } else if (volume[i] == 0) {
                    emptyCount++;
                }
                if (dFlag[i]) destCount++;
            }
            if (destCount == 3) return;
            if (emptyCount == 2) {
                if (minStep > step + 1)
                    minStep = step + 1;
            } else {
                move(0, step + 1);
                move(1, step + 1);
                move(2, step + 1);
            }
        } finally {
            dFlag[dest] = so;
            volume[dest] -= 2;
            volume[(dest + 1) % 3]++;
            volume[(dest + 2) % 3]++;
        }

    }
    public static void main(String[] args) {
        move(0, 0);
        move(1, 0);
        move(2, 0);
        System.out.println(minStep);
    }
}
うーん、最小であることの証明が出来ていないのですが、この手数で出来ることは間違い有りません。

結果:
 $ gosh 73.scm
10
827392
 $ fg
1
2
3
4
5
6
7
(define (wp n . l)
  (let1 sl (sort l)
    (cond ((= (caddr sl) (cadr sl)) (+ n (cadr sl)))
          (else (wp (+ 1 n) (+ (car sl) 2) (- (cadr sl) 1) (- (caddr sl) 1))))))

(print (wp 0 4 2 10))
(print (wp 0 827392 65536 122880))
自己フォロー
1 2 3
みたいな数列だと、先ほどのコードだと停止しません。
答えがないので仕方ないとも言えますが、悔しいので、過程の再出現の時点で「n/a」と返すコードを書いてみました。
全ての変遷の履歴を持ち、チェックする為、2番目の例だとなかなか計算が終わりません。

実行結果:
 $ gosh 73.scm
(4 2 10) -> 10
(1 2 3) -> n/a
 $
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(define (wpz . l)
  (display #`",l -> ,(apply wp () l)\n"))

(define (wp n . l)
  (let1 sl (sort l)
    (cond ((= (caddr sl) (cadr sl)) (+ (length n) (cadr sl)))
          ((member sl n) "n/a")
          (else (wp (cons sl n) (+ (car sl) 2) (- (cadr sl) 1) (- (caddr sl) 1))))))

(define (main args)
  (wpz 4 2 10)
  (wpz 1 2 3))
#3557 見ちゃうと、この解じゃ駄目な感じ。最小手数の証明も出来てないし。
自分でマイナス評価です。
幅優先探索です。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module Main(main) where

import List(any, findIndex)

type Youki = (Int, Int, Int)

main :: IO ()
main = print $ shallowestLeafDepth (4, 2, 10)

shallowestLeafDepth :: Youki -> Int
shallowestLeafDepth y = maybe 0 id $ findIndex (any isLeaf) $ bfs branch [y]

isLeaf :: Youki  -> Bool
isLeaf (_, 0, 0) = True
isLeaf (0, _, 0) = True
isLeaf (0, 0, _) = True
isLeaf _ = False

branch :: Youki -> [Youki]
branch (w1, 0, 0) = []
branch (0, w2, 0) = []
branch (0, 0, w3) = []
branch (0, w2, w3) = [(2, w2-1, w3-1)]
branch (w1, 0, w3) = [(w1-1, 2, w3-1)]
branch (w1, w2, 0) = [(w1-1, w2-1, 2)]
branch (w1, w2, w3) = [(w1+2, w2-1, w3-1), (w1-1, w2+2, w3-1), (w1-1, w2-1, w3+2)]

bfs :: (a -> [a]) -> [a] -> [[a]]
bfs f [] = []
bfs f xs = xs : bfs f (concatMap f xs)
とりあえず愚直に。まだだれもperlで投稿してないのでテスト。 3つの要素を比較して 1番小さな要素を足していきます。 1問目は $VAR1 = { '1' => 3, '3' => 9, '2' => 4 }; $VAR1 = { '1' => 5, '3' => 8, '2' => 3 }; $VAR1 = { '1' => 4, '3' => 7, '2' => 5 }; $VAR1 = { '1' => 6, '3' => 6, '2' => 4 }; 1と3のバケツが6と6になったので  以下略。で 10手 2問目は大きすぎるのではしょって ...一番最後 $VAR1 = { '1' => 338603, '3' => 338602, '2' => 338603 }; 827392手みたいです。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
use Data::Dumper;

my %hash = (
    1 => 827392,
    2 => 65536,
    3 => 122880,
);

my $calc_cnt = 0;

until ( any_eqv( \%hash ) ) {

    ++$calc_cnt;

    my $mini_key = get_mini_key( \%hash );
    $hash{$mini_key} += 2;

    @other_key = get_other_key($mini_key);
    for (@other_key) {
        $hash{$_} -= 1;
    }
}

print Dumper( \%hash );

$nokori = get_same_val( \%hash );
print "The answer is " . ( $calc_cnt + $nokori );

sub any_eqv {
    my $hash_ref = shift;
    my %hash     = %{$hash_ref};

    if (   $hash{1} == $hash{2}
        or $hash{2} == $hash{3}
        or $hash{3} == $hash{1} )
    {
        return 1;
    }
    else {
        return 0;
    }
}

sub get_mini_key {
    my $hash_ref = shift;
    my %hash     = %{$hash_ref};
    my $mini     = $hash{1};
    my $mini_key;
    for ( 1 .. 3 ) {
        $mini = $hash{$_} xor $mini_key = $_
            if ( $mini >= $hash{$_} );

    }
    return $mini_key;
}

sub get_other_key {

    my $target = shift;
    my $list   = '123';
    $list =~ s/$target//;
    return split( //, $list );
}

sub get_same_val {

    my $hash_ref = shift;
    my %hash     = %{$hash_ref};

    if ( $hash{1} == $hash{2} ) {
        return $hash{1};
    }
    elsif ( $hash{2} == $hash{3} ) {
        return $hash{2};
    }
    elsif ( $hash{3} == $hash{1} ) {
        return $hash{3};
    }
}
一箇所に水が集まるということは、その前の段階では残り二箇所は同じ数字になっている。
ならば、どうやって同じ数字にするか、と考えると2箇所から1を引いて残り一箇所に2を足すと考えると、3ずつしか差分は動かない。
よって差が3の倍数である組に注目する。
手順としてはその組の大きい数字を0にする作業となるので差が3の倍数である組の最大値が回答になる。
差が3の倍数の組が二つある場合は小さいほうの組でよい。

ってことで、こうなりました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>

long water_move(long a,long b,long c){
    long ret=-1;
    
    if((a-b)%3==0)
        ret=max(a,b);
    if((a-c)%3==0)
        if(ret==-1) ret=max(a,c);
        else ret=min(ret,max(a,c));
    if((b-c)%3==0)
        if(ret==-1) ret=max(b,c);
        else ret=min(ret,max(b,c));
    
    return ret;
}

void put_water_move(long a,long b, long c){
    int ret;
    
    ret=water_move(a,b,c);
    printf("A=%ld B=%ld C=%ld : ");
    if (ret==-1)
        printf("出来ません\n");
    else
        printf("%ld\n",ret);
}

int main(){
    put_water_move(4,2,10);
    put_water_move(827392,65536,122880);
    put_water_move(4,2,6);
    put_water_move(4,1,10);
    
    return 0;
}
gccだとmax、minが未定義って怒られました。
あとA、B、C、をprintfしてる所の引数が足りなくないですか?
max、minマクロってstdlibで定義されてるもんじゃなかったんでしたっけ?

引数は・・・書き忘れてます^^;; 
自分とこではたまたま、スタック上に残ってて動いたようですw
vc++のclだとコンパイルできました。

max,minは少なくともANSI Cにはないと思います。
vcの独自拡張かMS-DOS近辺の文化かどっちかなのかなあ。
math.h にもないかな?
#gcc だとwin32api/windef.h にあるらしい

まぁなければ作ればよいという事で差分。。。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
--- b.c.org    2007-10-30 13:52:04.546875000 +0900
+++ b.c    2007-10-30 13:52:41.421875000 +0900
@@ -1,5 +1,7 @@
 #include <stdio.h>
-#include <stdlib.h>
+
+#define max(a,b) ((a)>(b)?(a):(b))
+#define min(a,b) ((a)<(b)?(a):(b))
 
 long water_move(long a,long b,long c){
     long ret=-1;
@@ -20,7 +22,7 @@
     int ret;
     
     ret=water_move(a,b,c);
-    printf("A=%ld B=%ld C=%ld : ");
+    printf("A=%ld B=%ld C=%ld : ", a,b,c);
     if (ret==-1)
         printf("出来ません\n");
     else
IDをはじめて取得してみました。

かなり変態的に。
解が無いもの

”要素の合計が3で割り切れ
且つ 要素が互いに異なるもの”

に対しては むりぽ と叫んでdieします。

一問目は 10
二問目は 827392

 (m,n,l)の配列で

m != n !=  l
の時の最短の手法は 一番大きな要素になります。

* 一番大きな要素を1ずつ削っていくため。

m と n または
n と l または
m と l  が同じ値の時、
同じ値の数値が最短の手法になります。

* 同じ容器から1ずつ削っていくため。 

m と n と l が同じ値の時
は m = n= l 回が最短の手法です。

* 任意の2つの容器から1ずつ削っていくため。
1
2
3
4
5
6
7
8
@list = sort { $b <=> $a } qw(827392 65536 122880);
die "むりぽ"
    if (!( map { $sum += $_; $sum } @list ) % 3
    and !grep { $_ ne $x xor( $x = $_ ) } @list );

@list = sort { $a <=> $b } @list
    if ( grep { $_ ne $x xor( $x = $_ ) } @list );
print my ($min) = @list;
間違ってます。(本人
こっちが正解でつ
1
2
3
4
5
6
7
8
@list = sort { $b <=> $a } qw(3 2 1);
my ($s) = grep { $x ne $_ xor $x = $_ } @list;
print $s if ($s);
die "だめぽ"
    if ( ( $list[0] - $list[1] )  
    * ( $list[1] - $list[2] ) 
    * ( $list[0] - $list[2] ) % 3 );
print $list[0] if ( !$s );
解くプロセスを Emacs で表示してみました。
もっときれいに書けるかも……
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(eval-when-compile (require 'cl))

(defun water-puzzle (a b c)
  (let ((cups (list a b c)))
    (destructuring-bind ((i p) (j q) (k r))
        (sort `((0 ,a) (1 ,b) (2 ,c))
              (lambda (x y) (< (cadr x) (cadr y))))
      (cond ((zerop (mod (- p q) 3)) (show-strategy cups i j k))
            ((zerop (mod (- p r) 3)) (show-strategy cups i k j))
            ((zerop (mod (- q r) 3)) (show-strategy cups j k i))
            (t (message "Cannot solve!"))))))

(defun show-strategy (cups i j k)
  (let ((buf (get-buffer-create "*water puzzle*"))
        (mover (lambda (di dj dk)
                 (incf (nth i cups) di) (redraw-cup i di)
                 (incf (nth j cups) dj) (redraw-cup j dj)
                 (incf (nth k cups) dk) (redraw-cup k dk)))
        (counter 0))
    (switch-to-buffer buf)
    (show-init-state cups)
    (while (> (nth j cups) 0)
      (sit-for 1)
      (apply mover (if (zerop (nth i cups)) '(2 -1 -1) '(-1 -1 2)))
      (incf counter))
    (message "Solved in %d step%s."
             counter (if (= counter 1) "" "s"))))

(defun show-init-state (cups)
  (delete-region (point-min) (point-max))
  (redraw-cup 0 (car cups))   (insert ?\n)
  (redraw-cup 1 (cadr cups))  (insert ?\n)
  (redraw-cup 2 (caddr cups)) (insert ?\n))

(defun redraw-cup (i d)
  (goto-line (1+ i))
  (if (plusp d) (dotimes (x d) (insert ?|)) (delete-char (- d))))

;;; test
(water-puzzle 4 2 10)
A<=B<=Cとしたとき、

・AとBから、Aが空になるまで汲み続け、
・BとCからAに汲み、AとBからCに汲む、を繰り返す

と、できると思ったんですが、最短なのかどうかよくわかりません。
正解はどこかにあります?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
    # A, B, C を cups[0], cups[1], cups[2] とする
    cups[0] = $1
    cups[1] = $2
    cups[2] = $3

    # A <= B <= C にそろえる
    sort(cups)

    total = 0
    # A と B からとり、C に移す。A が空になるまで
    count = cups[0]
    cups[0] = 0
    cups[1] -= count
    cups[2] += count * 2
    total += count

    # B が空になるまで、以下を繰り返す。
    # B と C から取って A に移し、A と B からとって C に移す。
    count = cups[1]
    cups[0] = count % 2
    cups[1] -= count
    cups[2] += count * 2
    total += count

    if(cups[0] != 0) {
        print "N/A"