18
\$\begingroup\$

You are given a bunch of weights, and your task is to build a small balanced mobile using those weights.

The input is a list of integer weights in the range 1 through 9, inclusive. There may be duplicates.

The output is an ascii picture of a mobile that, when hung, would balance. Perhaps best shown by example:

input

3 8 9 7 5

possible output

         |
   +-----+---------+
   |               |
+--+-+        +----+------+
|    |        |           |
8   ++--+     7           5
    |   |
    9   3

You must use the ascii characters as shown. The horizontal and vertical segments may be of any length. No part of the mobile may touch (horizontally or vertically) another unconnected part of the mobile. All weights must be hung from a vertical segment of length at least 1, and there must be a vertical segment from which the whole mobile is hung.

The size of a mobile is the total number of +,-,and | characters required to build it. Lower sizes are better.

You may put as many connections on a segment as you would like. For example:

input

2 3 3 5 3 9

possible output

           |
   +---+---+-----------+
   |   |               |
+--+-+ 5               9
|  | |
2  | 3
   |
  +++
  | |
  3 3

The winning program is the one that can generate the lowest average of mobile sizes for a test set of inputs. The real test is super-secret to prevent hard-coding, but it will be something like this:

8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7
1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 7 7
3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7
\$\endgroup\$
  • \$\begingroup\$ Physics also involved? \$\endgroup\$ – YOU Apr 28 '11 at 5:35
  • 1
    \$\begingroup\$ @S.Mark: I guess you could say that. For the physically impaired, the sum of total_weight_hung_from_point * distance_of_point_from_pivot must be the same on both sides of the pivot point. \$\endgroup\$ – Keith Randall Apr 28 '11 at 5:38
  • \$\begingroup\$ Perhaps to make examining the diagrams easier, make it so that one bar is equal to about two hyphens? As it stands, your diagrams look out of balance. \$\endgroup\$ – Thomas O Apr 28 '11 at 14:48
5
\$\begingroup\$

Python 2.

I'm cheating a little bit:

  • I only construct mobiles with one horizontal. I have a feeling (but I haven't proven it) that the optimal mobile under the given conditions actually always does only have one horizontal. Edit: Not always true; with 2 2 9 1 Nabb has found a counter-example in the comments below:

    Size 18:                Size 16:
       |                        |
    +-++--+-----+            +--++-+
    | |   |     |            |   | |
    2 9   2     1           -+-  9 1
                            | |
                            2 2
    
  • I just do stupid brute-forcing:

    1. The given weights are shuffled randomly.
    2. Two weights at a time are placed on the mobile in the best positions such that it stays balanced.
    3. If the resulting mobile is better than any that we had before, remember it.
    4. Rinse and repeat, until a pre-defined number of seconds is up.

My results for your sample inputs; each was run for 5 seconds (I'm aware that this is ridiculous for the small ones – just going through all possible permutations would be faster). Note that since there's a random element, subsequent runs may find better or worse results.

3 8 9 7 5
Tested 107887 mobiles, smallest size 20:
        |
+-+-----+-+--+
| |     | |  |
5 3     7 9  8

2 3 3 5 3 9
Tested 57915 mobiles, smallest size 23:
      |
+--+-++--+-+---+
|  | |   | |   |
3  5 9   3 3   2

8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7
Tested 11992 mobiles, smallest size 50:
                |
+-+-+-+--+-+-+-+++-+-+--+-+-+-+-+
| | | |  | | | | | | |  | | | | |
8 8 8 8  8 8 8 8 8 8 8  7 8 8 8 8

1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 7 7
Tested 11119 mobiles, smallest size 62:
                    |
+-+-+-+-+-+--+-+-+-+++-+-+-+--+-+-+-+-+-+
| | | | | |  | | | | | | | |  | | | | | |
2 7 5 6 6 8  3 2 3 7 9 7 8 1  1 7 9 5 4 4

3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7
Tested 16301 mobiles, smallest size 51:
                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | |
4 6 5 7 7 4 6 5 3 5 6 4 7 6 7 5 4

The code (verbose, as this isn't code golf):

import time, random

def gcd(a, b):
    while b > 0:
        a, b = b, a % b
    return a

class Mobile(object):
    def __init__(self):
        self.contents = [None];
        self.pivot = 0;

    def addWeights(self, w1, w2):
        g = gcd(w1, w2)
        m1 = w2 / g
        m2 = w1 / g
        mul = 0
        p1 = -1
        while True:
            if p1 < 0:
                mul += 1
                p1 = mul * m1
                p2 = -mul * m2
            else:
                p1 *= -1
                p2 *= -1
            if self.free(p1) and self.free(p2):
                self.add(w1, p1)
                self.add(w2, p2)
                return

    def add(self, w, pos):
        listindex = self.pivot - pos 
        if listindex < 0:
            self.contents = [w] + (abs(listindex) - 1) * [None] + self.contents
            self.pivot += abs(listindex)
        elif listindex >= len(self.contents):
            self.contents += (listindex - len(self.contents)) * [None] + [w]
        else:
            self.contents[listindex] = w

    def at(self, pos):
        listindex = self.pivot - pos
        if 0 <= listindex < len(self.contents):
            return self.contents[listindex]
        return None

    def free(self, pos):
        return all(self.at(pos + d) is None for d in (-1, 0, 1))

    def score(self):
        return 1 + 2 * len(self.contents) - self.contents.count(None)

    def draw(self):
        print self.pivot * " " + "|"
        print "".join("+" if c is not None or i == self.pivot else "-" for i, c in enumerate(self.contents))
        print "".join("|" if c is not None else " " for c in self.contents)
        print "".join(str(c) if c is not None else " " for c in self.contents)

    def assertBalance(self):
        assert sum((i - self.pivot) * (c or 0) for i, c in enumerate(self.contents)) == 0


weights = map(int, raw_input().split())

best = None
count = 0

# change the 5 to the number of seconds that are acceptable
until = time.time() + 5

while time.time() < until:
    count += 1
    m = Mobile()

    # create a random permutation of the weights
    perm = list(weights)
    random.shuffle(perm)

    if len(perm) % 2:
        # uneven number of weights -- place one in the middle
        m.add(perm.pop(), 0)

    while perm:
        m.addWeights(perm.pop(), perm.pop())

    m.assertBalance() # just to prove the algorithm is correct :)
    s = m.score()
    if best is None or s < bestScore:
        best = m
        bestScore = s

print "Tested %d mobiles, smallest size %d:" % (count, best.score())
best.draw()
\$\endgroup\$
  • \$\begingroup\$ @Nabb: Higher weights than 9 aren't possible. As for 1 9 2 8 it generates 1-------8+-9--2; from the top of my head I can't come up with anything better (but I wouldn't rely on that) -- do you have something? \$\endgroup\$ – balpha Apr 30 '11 at 11:12
  • 1
    \$\begingroup\$ @balpha: Nevermind, wasn't thinking straight when I commented earlier. I thought for some reason that you could stick them as 1-9 and 2-8, but obviously those pairs themselves don't balance! \$\endgroup\$ – Nabb Apr 30 '11 at 13:11
  • \$\begingroup\$ Okay, here's one that may actually be better with multiple layers: 2 2 9 1, i.e. (2+2)*3 = 9+1*3 for 16 size instead of 2-9+--2----1 which is 18. I'd guess that there's a threshold (maybe 5 or 6) after which a single horizontal row is always optimal. \$\endgroup\$ – Nabb Apr 30 '11 at 13:35
  • \$\begingroup\$ @Nabb: Yep; that's indeed a good counter-example. \$\endgroup\$ – balpha Apr 30 '11 at 16:08
  • \$\begingroup\$ @Nabb, A single bar with 2-2-+9-1 balances, with a score of 13 (4*2+2*2 = 9*1+1*3). So I don't think that one is a good counterexample. \$\endgroup\$ – Keith Randall May 1 '11 at 17:01
1
\$\begingroup\$

Well this is an old question, but I just saw it appear in the top questions tab so here's my (optimal) solution:

#include <stdio.h>
#include <limits.h>
#include <math.h>
#include <stdlib.h>

int main(int argc, const char *const *argv) {
    if(argc < 2) {
        fprintf(stderr,
            "Balances weights on a hanging mobile\n\n"
            "Usage: %s <weight1> [<weight2> [...]]\n",
            argv[0]
        );
        return 1;
    }
    int total = argc - 1;
    int values[total];
    int maxval = 0;
    for(int n = 0; n < total; ++ n) {
        char *check = NULL;
        long v = strtol(argv[n+1], &check, 10);
        if(v <= 0 || v > INT_MAX || *check != '\0') {
            fprintf(stderr,
                "Weight #%d (%s) is not an integer within (0 %d]\n",
                n + 1, argv[n+1], INT_MAX
            );
            return 1;
        }
        values[n] = (int) v;
        if(values[n] > maxval) {
            maxval = values[n];
        }
    }
    int maxwidth = (int) log10(maxval) + 1;
    for(int n = 0; n < total; ++ n) {
        int width = (int) log10(values[n]) + 1;
        fprintf(stdout,
            "%*s\n%*d\n",
            (maxwidth + 1) / 2, "|",
            (maxwidth + width) / 2, values[n]
        );
    }
    return 0;
}

From looking at the rules I'm pretty sure it isn't cheating, although it feels like it is. This will just output all the given numbers in a vertical chain, for a total cost of 2*number_of_inputs (which is the minimum possible because each number must have a bar above it no matter what the layout). Here's an example:

./mobile 3 8 9 7 5

Produces:

|
3
|
8
|
9
|
7
|
5

Which is of course in perfect balance.


I was originally going to try something more in the spirit of this challenge, but it quickly turned out that it just optimised away to this structure anyway

\$\endgroup\$
  • \$\begingroup\$ Probably not clear from my description, but you can not connect a | to the bottom of a weight. \$\endgroup\$ – Keith Randall Sep 28 '16 at 20:34
  • \$\begingroup\$ @KeithRandall ah ok; with that in mind I might have to have a go at solving this properly. \$\endgroup\$ – Dave Sep 28 '16 at 22:49
1
\$\begingroup\$

Here is a solution that brute forces the smallest single row solution. The code iterates over all permutations and compute the center of mass for each. If the center of mass has integer coordinates, we've found a solution.

After all permutations have been tried, we add a segment to the mix (equivalent to a weight of mass 0) in our current set of weights and retry.

To run the program, do python balance.py 1 2 2 4.

#!/usr/bin/env python3
import itertools, sys

# taken from http://stackoverflow.com/a/30558049/436792
def unique_permutations(elements):
    if len(elements) == 1:
        yield (elements[0],)
    else:
        unique_elements = set(elements)
        for first_element in unique_elements:
            remaining_elements = list(elements)
            remaining_elements.remove(first_element)
            for sub_permutation in unique_permutations(remaining_elements):
                yield (first_element,) + sub_permutation

def print_solution(cm, values):
    print(('  ' * cm) + '|')
    print('-'.join(['-' if v == 0 else '+'  for v in values]))
    print(' '.join([' ' if v == 0 else '|'  for v in values]))
    print(' '.join([' ' if v == 0 else str(v) for v in values]))



input = list(map(int, sys.argv[1:]))
mass = sum(input)
while True:
    n = len(input)
    permutations = filter(lambda p: p[0] != 0 and p[n-1] != 0, unique_permutations(input))
    for p in permutations:
        cm = 0
        for i in range(n):
            cm += p[i] * i;
        if (cm % mass == 0):
            print_solution(cm//mass, p)
            sys.exit(0)
    input.append(0)

which produces these best solutions :

    |
+-+-+-+-+
| | | | |
8 3 9 5 7


    |
+-+-+-+-+-+
| | | | | |
9 2 3 5 3 3

                |
+-+-+-+-+-+-+---+-+-+-+-+-+-+-+-+
| | | | | | |   | | | | | | | | |
8 8 8 8 8 8 8   8 8 8 8 8 8 8 8 7


                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | | | | |
1 1 2 2 3 3 4 4 8 8 5 5 6 6 7 7 7 7 9 9


                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | |
3 4 4 4 4 5 5 5 5 6 7 6 7 7 7 6 6
\$\endgroup\$
0
\$\begingroup\$

Python 3

This does no worse than 1 more than optimal on any of the test cases, I believe, and does so in 5 seconds.

Basically, I use a single bar approach. I randomly order the input, then insert the weights onto the bar one at a time. Each element is either put in the position that minimizes the excess weight on either side, or the second best position from that perspective, using the former 75% of the time and the latter 25% of the time. Then, I check whether the mobile is balanced at the end, and is better than the best mobile found so far. I store the best one, then halt and print it out after 5 seconds of searching.

Results, in 5 second runs:

py mobile.py <<< '3 8 7 5 9'
Best mobile found, score 15:
    |    
+-+-+-+-+
| | | | |
8 7 3 5 9
py mobile.py <<< '2 2 1 9'
Best mobile found, score 13:
   |    
+-++-+-+
| |  | |
1 9  2 2
py mobile.py <<< '2 3 3 5 3 9'
Best mobile found, score 18:
      |    
+-+-+-+-+-+
| | | | | |
2 3 3 5 9 3
py mobile.py <<< '8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7'
Best mobile found, score 49:
                |               
+-+--+-+-+-+-+-+++-+-+-+-+-+-+-+
| |  | | | | | | | | | | | | | |
7 8  8 8 8 8 8 8 8 8 8 8 8 8 8 8
\py mobile.py <<< '1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 7 7'
Best mobile found, score 61:
                    |                   
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+
| | | | | | | | | | | | | | | | | | |  |
1 7 7 5 4 3 1 9 6 7 8 2 2 9 3 7 6 5 8  4
py mobile.py <<< '3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7'
Best mobile found, score 51:
                |                
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | |
4 4 6 7 7 4 5 7 6 6 5 4 6 3 5 5 7

Code:

import random
import time

class Mobile:
    def __init__(self):
        self.contents = {}
        self.lean = 0

    def usable(self, loc):
        return not any(loc + k in self.contents for k in (-1,0,1))
    def choose_point(self, w):
        def goodness(loc):
            return abs(self.lean + w * loc)
        gl = sorted(list(filter(self.usable,range(min(self.contents.keys() or [0]) - 5,max(self.contents.keys() or [0]) + 6))), key=goodness)
        return random.choice((gl[0], gl[0], gl[0], gl[1]))

    def add(self, w, loc):
        self.contents[loc] = w
        self.lean += w*loc

    def __repr__(self):
        width = range(min(self.contents.keys()), max(self.contents.keys()) + 1)
        return '\n'.join((''.join(' ' if loc else '|' for loc in width),
                          ''.join('+' if loc in self.contents or loc == 0 else '-' for loc in width),
                          ''.join('|' if loc in self.contents else ' ' for loc in width),
                          ''.join(str(self.contents.get(loc, ' ')) for loc in width)))

    def score(self):
        return max(self.contents.keys()) - min(self.contents.keys()) + len(self.contents) + 2

    def my_score(self):
        return max(self.contents.keys()) - min(self.contents.keys()) + 1

best = 1000000
best_mob = None
in_weights = list(map(int,input().split()))
time.clock()
while time.clock() < 5:
    mob = Mobile()
    for insert in random.sample(in_weights, len(in_weights)):
        mob.add(insert, mob.choose_point(insert))
    if not mob.lean:
        if mob.score() < best:
            best = mob.score()
            best_mob = mob

print("Best mobile found, score %d:" % best_mob.score())
print(best_mob)

The only one of these solutions which I believe is suboptimal is the longest one, which has this solution, which I found after a 10 minute run:

Best mobile found, score 60:
                   |                   
+-+-+-+-+-+-+-+-+-+++-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | | | | |
3 2 9 4 7 8 1 6 9 8 7 1 6 2 4 5 7 3 5 7
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.