18

I got one puzzle and I want to solve it using Python.

Puzzle:

A merchant has a 40 kg weight which he used in his shop. Once, it fell from his hands and was broken into 4 pieces. But surprisingly, now he can weigh any weight between 1 kg to 40 kg with the combination of these 4 pieces.

So question is, what are weights of those 4 pieces?

Now I wanted to solve this in Python.

The only constraint i got from the puzzle is that sum of 4 pieces is 40. With that I could filter all the set of 4 values whose sum is 40.

import itertools as it

weight = 40
full = range(1,41)
comb = [x for x in it.combinations(full,4) if sum(x)==40]

length of comb = 297

Now I need to check each set of values in comb and try all the combination of operations.

Eg if (a,b,c,d) is the first set of values in comb, I need to check a,b,c,d,a+b,a-b, .................a+b+c-d,a-b+c+d........ and so on.

I tried a lot, but i am stuck at this stage, ie how to check all these combination of calculations to each set of 4 values.

Question :

1) I think i need to get a list all possible combination of [a,b,c,d] and [+,-].

2) does anyone have a better idea and tell me how to go forward from here?

Also, I want to do it completely without help of any external libraries, need to use only standard libraries of python.

EDIT : Sorry for the late info. Its answer is (1,3,9,27), which I found a few years back. I have checked and verified the answer.

EDIT : At present, fraxel's answer works perfect with time = 0.16 ms. A better and faster approach is always welcome.

Regards

ARK

Morgoth
  • 4,139
  • 5
  • 35
  • 53
Abid Rahman K
  • 48,289
  • 27
  • 140
  • 153
  • 5
    The puzzle is trickier than that; I'm not sure you can easily brute-force it. The trick is that, to measure certain weights, he may need to add pieces of the weight to both sides of the scale. Think about a simpler version: break a 4-kg weight into 2 pieces that can measure any weight up to 4 kg. The answer is a 1 kg piece and a 3 kg piece. To measure 2 kg, you have to put one of the pieces on each side of the scale. – Jacob Mattison Apr 30 '12 at 17:50
  • @JacobM has the best way to go: start with a simpler problem and see if you can't find a pattern that allows you to solve the more complex problem. Also, be aware that, unless you are sure that each weight is unique, combinations won't give you what you want. (to see this, try changing weight to 10 and full to range(1,10). easier to play around with what it does.) – Jeff Tratner Apr 30 '12 at 17:53
  • @ JacobM...yeah.. of course.. ie the question. You can put weights on both sides of scale to get desired weight. ie i mentioned `negative sign` in question. ie `a-b, a-b+c-d ....`. `minus` denotes that weight put in other scale. I think i have to explain it in question. Thanks for notifying. – Abid Rahman K Apr 30 '12 at 17:56
  • Read the question andrew, he wants to solve it using Python, and who are we to stop him! – jgritty Apr 30 '12 at 19:05
  • ok, i deleted my comment in case it's an unwanted "spoiler" (sorry). if you want to know how to just write down the answer, email me. – andrew cooke Apr 30 '12 at 19:21

9 Answers9

24

Earlier walk-through anwswer:

We know a*A + b*B + c*C + d*D = x for all x between 0 and 40, and a, b, c, d are confined to -1, 0, 1. Clearly A + B + C + D = 40. The next case is x = 39, so clearly the smallest move is to remove an element (it is the only possible move that could result in successfully balancing against 39):

A + B + C = 39, so D = 1, by neccessity.

next:

A + B + C - D = 38

next:

A + B + D = 37, so C = 3

then:

A + B = 36

then:

A + B - D = 35

A + B - C + D = 34

A + B - C = 33

A + B - C - D = 32

A + C + D = 31, so A = 9

Therefore B = 27

So the weights are 1, 3, 9, 27

Really this can be deduced immediately from the fact that they must all be multiples of 3.

Interesting Update:

So here is some python code to find a minimum set of weights for any dropped weight that will span the space:

def find_weights(W):
    weights = []
    i = 0
    while sum(weights) < W:
        weights.append(3 ** i)
        i += 1
    weights.pop()
    weights.append(W - sum(weights))
    return weights

print find_weights(40)
#output:
[1, 3, 9, 27]

To further illustrate this explaination, one can consider the problem as the minimum number of weights to span the number space [0, 40]. It is evident that the number of things you can do with each weight is trinary /ternary (add weight, remove weight, put weight on other side). So if we write our (unknown) weights (A, B, C, D) in descending order, our moves can be summarised as:

    ABCD:   Ternary:
40: ++++     0000
39: +++0     0001
38: +++-     0002
37: ++0+     0010
36: ++00     0011
35: ++0-     0012
34: ++-+     0020
33: ++-0     0021
32: ++--     0022
31: +0++     0100
etc.

I have put ternary counting from 0 to 9 alongside, to illustrate that we are effectively in a trinary number system (base 3). Our solution can always be written as:

3**0 + 3**1 +3**2 +...+ 3**N >= Weight

For the minimum N that this holds true. The minimum solution will ALWAYS be of this form.

Furthermore, we can easily solve the problem for large weights and find the minimum number of pieces to span the space:

A man drops a known weight W, it breaks into pieces. His new weights allow him to weigh any weight up to W. How many weights are there, and what are they?

#what if the dropped weight was a million Kg:
print find_weights(1000000)
#output:
[1, 3, 9, 27, 81, 243, 729, 2187, 6561, 19683, 59049, 177147, 531441, 202839]

Try using permutations for a large weight and unknown number of pieces!!

fraxel
  • 31,038
  • 11
  • 87
  • 96
  • A + B + C = 39, so D = 1, by necessity. I wasn't so sure about that. What if you had weights of 14 and 15, you could do 1 using those 2, one on each side, as 15-14 = 1. So not sure necessity is the right word. – jgritty Apr 30 '12 at 19:10
  • @jgritty - but you can't get 39! If you are trying to weigh something that weighs 39, you're gonna need 3 elements for that :) – fraxel Apr 30 '12 at 19:13
  • That's OK, I have 4! However, you obviously will run into a problem eventually. – jgritty Apr 30 '12 at 19:16
  • @jgritty - er no, because the above method solves the problem: 1,3,9,27 :) – fraxel Apr 30 '12 at 19:19
  • Sorry, I used the wrong pronoun, I meant the guy who started with 14 and 15 will run into a problem. – jgritty Apr 30 '12 at 19:22
  • @F.J and jgritty - I'll try and clarify, we have 4 weights so to weigh some fifth weight that is `39` the `only` possibility is to remove one of our 4 weights (there is no other legitimate move that could do it). We know we are able to weigh 39, so one of our four weights must be `1`. – fraxel Apr 30 '12 at 19:53
  • +1 - Your method was awesome (although not python implementation). I understood all steps, but some confusion plays in my mind, i don't get what. I hadn't a systematic way when i solved it and was not a method with explanation. But now i got a method from your answer. So thank you. – Abid Rahman K May 01 '12 at 05:17
  • 1
    @Abid Rahman K - Cheers, :) I am going to provide a very interesting update to this answer with some code and more explanaition. It is a very fun problem. Using this method allows you to easily solve the problem: `A man drops a 1000000 Kg weight, his new weights allow him to weigh any weight up to 1000000 Kg! What are they, and how many are there? – fraxel May 01 '12 at 05:25
  • Oh my god... i am eagerly waiting for that. – Abid Rahman K May 01 '12 at 05:29
  • Wowwwww... it is pretty fast.. `t = 0.016 ms`.. Thanks – Abid Rahman K May 01 '12 at 08:06
  • How did you find this is base 3 operation? I never thought this kind of logic behind this question. Why my teacher pose such a question to 14 year old students including me? – Abid Rahman K May 01 '12 at 08:12
  • @Abid Rahman K - I just thought it through as written above. Sounds like a fun teacher! Try doing the timing comparisons for other unique answers [121 Kg (5 pieces), 364 Kg (6 pieces), 1093 Kg (7) etc.], see how quickly the combinations approach runs into trouble... – fraxel May 02 '12 at 06:52
  • Nice answer! This brought this comic(http://calamitiesofnature.com/archive/?c=548) to mind... – Jaime Jan 17 '13 at 20:54
  • @Jaime - ha, that is ridiculous :) – fraxel Jan 17 '13 at 21:07
  • @Jaime The comic's gone... >. – frederick99 Mar 10 '18 at 03:04
8

Here is a brute-force itertools solution:

import itertools as it

def merchant_puzzle(weight, pieces):
    full = range(1, weight+1)
    all_nums = set(full)
    comb = [x for x in it.combinations(full, pieces) if sum(x)==weight]
    funcs = (lambda x: 0, lambda x: x, lambda x: -x)
    for c in comb:
        sums = set()
        for fmap in it.product(funcs, repeat=pieces):
            s = sum(f(x) for x, f in zip(c, fmap))
            if s > 0:
                sums.add(s)
                if sums == all_nums:
                    return c

>>> merchant_puzzle(40, 4)
(1, 3, 9, 27)

For an explanation of how it works, check out the answer Avaris gave, this is an implementation of the same algorithm.

Community
  • 1
  • 1
Andrew Clark
  • 180,281
  • 26
  • 249
  • 286
4

You are close, very close :).

Since this is a puzzle you want to solve, I'll just give pointers. For this part:

Eg if (a,b,c,d) is the first set of values in comb, i need to check a,b,c,d,a+b,a-b, .................a+b+c-d,a-b+c+d........ and so on.

Consider this: Each weight can be put to one scale, the other or neither. So for the case of a, this can be represented as [a, -a, 0]. Same with the other three. Now you need all possible pairings with these 3 possibilities for each weight (hint: itertools.product). Then, a possible measuring of a pairing (lets say: (a, -b, c, 0)) is merely the sum of these (a-b+c+0).

All that is left is just checking if you could 'measure' all the required weights. set might come handy here.

PS: As it was stated in the comments, for the general case, it might not be necessary that these divided weights should be distinct (for this problem it is). You might reconsider itertools.combinations.

Avaris
  • 32,127
  • 6
  • 70
  • 67
  • +1 - `this can be represented as [a, -a, 0]`. This trick didn't come to my mind. That one single statement is the turning point. Thank you. – Abid Rahman K May 01 '12 at 05:22
2

I brute forced the hell out of the second part.

Do not click this if you don't want to see the answer. Obviously, if I was better at permutations, this would have required a lot less cut/paste search/replace:

http://pastebin.com/4y2bHCVr

jgritty
  • 10,684
  • 3
  • 33
  • 58
0

I don't know Python syntax, but maybe you can decode this Scala code; start with the 2nd for-loop:

def setTo40 (a: Int, b: Int, c: Int, d: Int) = {

val vec = for (
  fa <- List (0, 1, -1);
  fb <- List (0, 1, -1);
  fc <- List (0, 1, -1);
  fd <- List (0, 1, -1);
  prod = fa * a + fb * b + fc * c + fd * d;
  if (prod > 0)
  ) yield (prod)

  vec.toSet
}

for (a <- (1 to 9);
  b <- (a to 14);
  c <- (b to 20);
  d = 40-(a+b+c)
  if (d > 0)) { 
    if (setTo40 (a, b, c, d).size > 39)
      println (a + " " + b + " " + c + " " + d)
  }
user unknown
  • 32,929
  • 11
  • 72
  • 115
0

With weights [2, 5, 15, 18] you can also measure all objects between 1 and 40kg, although some of them will need to be measured indirectly. For example, to measure an object weighting 39kg, you would first compare it with 40kg and the balance would pend to the 40kg side (because 39 < 40), but then if you remove the 2kg weight it would pend to the other side (because 39 > 38) and thus you can conclude the object weights 39kg.

More interestingly, with weights [2, 5, 15, 45] you can measure all objects up to 67kg.

Luís Pureza
  • 781
  • 1
  • 6
  • 13
  • Hi, thank you for your answer. My question was not about finding its answer, but solving it programmatically, especially using python. – Abid Rahman K Mar 22 '13 at 13:37
  • That's ok, but please note that writing a program to find this answer is slightly more complicated, because you have to take into account the indirect measurements. – Luís Pureza Mar 22 '13 at 15:14
0

If anyone doesn't want to import a library to import combos/perms, this will generate all possible 4-move strategies...

# generates permutations of repeated values
def permutationsWithRepeats(n, v):
    perms = []
    value = [0] * n
    N = n - 1
    i = n - 1

    while i > -1:
        perms.append(list(value))

        if value[N] < v:
            value[N] += 1
        else:
            while (i > -1) and (value[i] == v):
                value[i] = 0
                i -= 1

            if i > -1:
                value[i] += 1
                i = N

    return perms

# generates the all possible permutations of 4 ternary moves
def strategy():
    move = ['-', '0', '+']
    perms = permutationsWithRepeats(4, 2)

    for i in range(len(perms)):
        s = ''

        for j in range(4):
            s += move[perms[i][j]]

        print s

# execute
strategy()
0

My solution as follows:

    #!/usr/bin/env python3

weight = 40
parts = 4
part=[0] * parts

def test_solution(p, weight,show_result=False):
    cv=[0,0,0,0]
    for check_weight in range(1,weight+1):
        sum_ok = False
        for parts_used in range(2 ** parts):
            for options in range(2 ** parts):
                for pos in range(parts):
                    pos_neg = int('{0:0{1}b}'.format(options,parts)[pos]) * 2 - 1
                    use = int('{0:0{1}b}'.format(parts_used,parts)[pos])
                    cv[pos] = p[pos] * pos_neg * use
                if sum(cv) == check_weight:
                    if show_result:
                        print("{} = sum of:{}".format(check_weight, cv))
                    sum_ok = True
                    break
        if sum_ok:
            continue
        else:
            return False
    return True

for part[0] in range(1,weight-parts):
    for part[1] in range(part[0]+1, weight - part[0]):
        for part[2] in range( part[1] + 1 , weight - sum(part[0:2])):
            part[3] = weight - sum(part[0:3])
            if test_solution(part,weight):
                print(part)
                test_solution(part,weight,True)
                exit()

It gives you all the solutions for the given weights

Jeromba6
  • 93
  • 1
  • 5
0

More dynamic than my previous answer, so it also works with other numbers. But breaking up into 5 peaces takes some time:

#!/usr/bin/env python3

weight = 121
nr_of_parts = 5

# weight = 40
# nr_of_parts = 4

weight = 13
nr_of_parts = 3


part=[0] * nr_of_parts

def test_solution(p, weight,show_result=False):
    cv=[0] * nr_of_parts
    for check_weight in range(1,weight+1):
        sum_ok = False
        for nr_of_parts_used in range(2 ** nr_of_parts):
            for options in range(2 ** nr_of_parts):
                for pos in range(nr_of_parts):
                    pos_neg = int('{0:0{1}b}'.format(options,nr_of_parts)[pos]) * 2 - 1
                    use = int('{0:0{1}b}'.format(nr_of_parts_used,nr_of_parts)[pos])
                    cv[pos] = p[pos] * pos_neg * use
                if sum(cv) == check_weight:
                    if show_result:
                        print("{} = sum of:{}".format(check_weight, cv))
                    sum_ok = True
                    break
        if sum_ok:
            continue
        else:
            return False
    return True

def set_parts(part,position, nr_of_parts, weight):
    if position == 0:
        part[position] = 1
        part, valid = set_parts(part,position+1,nr_of_parts,weight)
        return part, valid
    if position == nr_of_parts - 1:
        part[position] = weight - sum(part)
        if part[position -1] >= part[position]:
            return part, False
        return part, True
    part[position]=max(part[position-1]+1,part[position])
    part, valid = set_parts(part, position + 1, nr_of_parts, weight)
    if not valid:
        part[position]=max(part[position-1]+1,part[position]+1)
        part=part[0:position+1] + [0] * (nr_of_parts - position - 1)
        part, valid = set_parts(part, position + 1, nr_of_parts, weight)
    return part, valid

while True:
    part, valid = set_parts(part, 0, nr_of_parts, weight)
    if not valid:
        print(part)
        print ('No solution posible')
        exit()
    if test_solution(part,weight):
        print(part,'    ')
        test_solution(part,weight,True)
        exit()
    else:
        print(part,'   ', end='\r')
Jeromba6
  • 93
  • 1
  • 5