sets = [None] * 2**7
best_diff = 1000
best_item = None


def get_group(op):
    if op == '+' or op == '-':
        return '+'

    if op == '*' or op == '/':
        return '*'

    return '#'


def commut_group(op):
    if op == '+' or op == '*':
        return op
    else:
        return '#'


def to_str(node, group): # (left, right, display, value)
    left = right = lp = rp = ""

    if (node[0]):
        op_group = get_group(node[2])
        left = to_str(node[0], op_group)
        right = to_str(node[1], commut_group(node[2]))

        if group == '#' or (group == '*' and op_group == '+'):
            lp = '('; rp = ')'

    return lp + left + node[2]  + right + rp


def move_node(to_tree, from_tree):
    op = from_tree[2]

    if op == 'T':
        return to_tree

    if from_tree[0][3] != None: # target to the right
        if op == '+':
            value = to_tree[3] - from_tree[0][3]
            node = (to_tree, from_tree[0], '-', value)
            return move_node(node, from_tree[1])
        if op == '-':
            value = from_tree[0][3] - to_tree[3]
            node = (from_tree[0], to_tree, '-', value)
            return move_node(node, from_tree[1])
        if op == '*':
            if from_tree[0][3] == 0:
                return (None, None, '0', 0)
            value = to_tree[3] / from_tree[0][3]
            node = (to_tree, from_tree[0], '/', value)
            return move_node(node, from_tree[1])
        if op == '/':
            if to_tree[3] == 0:
                return (None, None, '0', 0)
            value = from_tree[0][3] / to_tree[3]
            node = (from_tree[0], to_tree, '/', value)
            return move_node(node, from_tree[1])
    else: # target to the left
        if op == '+':
            value = to_tree[3] - from_tree[1][3]
            node = (to_tree, from_tree[1], '-', value)
            return move_node(node, from_tree[0])
        if op == '-':
            value = to_tree[3] + from_tree[1][3]
            node = (to_tree, from_tree[1], '+', value)
            return move_node(node, from_tree[0])
        if op == '*':
            if from_tree[1][3] == 0:
                return (None, None, '0', 0)
            value = to_tree[3] / from_tree[1][3]
            node = (to_tree, from_tree[1], '/', value)
            return move_node(node, from_tree[0])
        if op == '/':
            value = to_tree[3] * from_tree[1][3]
            node = (to_tree, from_tree[1], '*', value)
            return move_node(node, from_tree[0])


def join(x, y):
    if x[3] != None:
        return move_node(x, y)
    else:
        return move_node(y, x)


def val_none(value, target):
    if target:
        return None
    else:
        return value


def merge(x, y, target):
    res = {}

    for v1, s1 in x:
        for v2, s2 in y:
            res[v1 + v2] = (s1, s2, '+', val_none(v1 + v2, target))

            if v1 != 0 and v2 != 0:
                res[v1 * v2] = (s1, s2, '*', val_none(v1 * v2, target))

            for va, sa, vb, sb in ((v1, s1, v2, s2), (v2, s2, v1, s1)):
                res[va - vb] = (sa, sb, '-', val_none(va - vb, target))

                if va != 0 and vb != 0:
                    res[va / vb] = (sa, sb, '/', val_none(va / vb, target))

    return res


def mrange(mask):
    x = 0

    while x != mask:
        x = (x - mask) & mask
        yield x


def is_single_not_root(x):
    return (x & 1) == 0 and (x & (x - 1)) == 0


def is_single(x):
    return (x & (x - 1)) == 0


def fill(pos):
    cnt = bin(pos).count('1')
    if sets[pos] or cnt > 4:
        return

    results = {}

    for x in mrange(pos & (pos - 1)):
        y = pos ^ x

        if cnt < 4 or (pos & 1) or not (is_single(x) or is_single(y)):
            results.update(merge(sets[x], sets[y], (pos & 1) != 0))

        if is_single_not_root(x):
            results.update(sets[y]) # x not used

        if is_single_not_root(y):
            results.update(sets[x]) # y not used

    sets[pos] = sorted(results.items())


def get_best(x, y):
    global best_diff, best_item
    joined = join(x[1], y[1])

    if joined[3] > 0 and abs(joined[3] - sets[1][0][0]) < best_diff:
        best_diff = abs(joined[3] - sets[1][0][0])
        best_item = joined


def match(x, y):
    pos_x = 0
    pos_y = 0

    while pos_x < len(x) and pos_y < len(y):
        get_best(x[pos_x], y[pos_y])

        if x[pos_x][0] < y[pos_y][0]:
            pos_x += 1
        else:
            if x[pos_x][0] > y[pos_y][0]:
                pos_y += 1
            else:
                pos_x += 1; pos_y += 1


def scan():
    for i in xrange(1, 7):
        for j in xrange(1, i):
            pos_with_root = 1 | (1 << i) | (1 << j)
            pos_no_root = ((1 << 7) - 1) ^ pos_with_root
            match(sets[pos_with_root], sets[pos_no_root])

            for k in xrange(1, j):
                match(sets[pos_with_root | (1 << k)],
                      sets[pos_no_root & ~(1 << k)])


def search(numbers, target):
    sets[1] = [(target, (None, None, "T", None))]

    for i in range(6):
        sets[2 ** (i + 1)] = \
            [(float(numbers[i]),
              (None, None, str(numbers[i]), float(numbers[i])))]

    for i in range(3, 2**7):
        fill(i)

    scan()

    return to_str(best_item, '=') + " -> " + str(best_item[3])


print search([4, 8, 6, 2, 15, 50], 590)
