Подтвердить что ты не робот

Сплит-кортеж python в подзаголовках с ограничением емкости в стиле функционального программирования

У меня есть некоторый кортеж в python. Предел пропускной способности, например, равен 5. Я хочу разбить кортеж в подмножествах, ограниченный суммой из них:

Например:

input: (3, 1, 4, 2, 2, 1, 1, 2) and capacity = 5
output: (3, 1) (4) (2, 2, 1) (1, 2) #each subtuple is less than 5, order safe.

Я ищу красивое выразительное решение этой задачи, предпочтительнее в функциональном стиле программирования (например, с помощью itertools.dropwhile или что-то в этом роде)

4b9b3361

Ответ 1

Вы можете инкапсулировать нефункциональную часть и вызвать ее из функционального кода:

from itertools import groupby

class GroupBySum:
    def __init__(self, maxsum):
        self.maxsum = maxsum
        self.index = 0
        self.sum = 0

    def __call__(self, value):
        self.sum += value
        if self.sum > self.maxsum:
            self.index += 1
            self.sum = value
        return self.index

# Example:

for _, l in groupby((3, 1, 4, 2, 2, 1, 1, 2), GroupBySum(5)):
    print(list(l))

Ответ 2

Я не мог с этим поделать, но написал что-то близкое к тому, что я делал бы в Haskell (хотя я все еще несколько питонов):

def take_summed(xs, cap):
    if len(xs) <= 1:
        return xs, ()
    else:
        x, *rest = xs

        if x > cap:
            return (), xs
        else:
            init, tail = take_summed(rest, cap - x)
            return (x,) + tuple(init), tail

def split(xs, cap=5):
    if len(xs) <= 1:
        yield xs
    else:
        chunk, rest = take_summed(xs, cap)
        yield chunk

        if rest != ():
            yield from split(rest, cap)

Не стесняйтесь разделить функции на подзадачи. Результат:

In [45]: list(split((3, 1, 4, 2, 2, 1, 1, 2), 5))
Out[45]: [(3, 1), (4,), (2, 2, 1), (1, 2)]

Проблема с тем, чтобы сделать это короче, заключается не в том, что он не выполним без побочных эффектов, а в том, что вам нужно нести дополнительное накопленное состояние, поэтому даже при использовании reduce вам нужно будет изобрести что-то действительно сложное, чтобы обойти сумма между приложениями.

Ответ 3

Здесь немного отличается подход от @Jean, который срезает входной кортеж вместо создания меньших списков с добавлением и предлагает небольшое повышение производительности:

def group_by_capacity(tup, capacity=5):
    t = iter(tup)
    curr, s =  0, next(t)

    for i, v in enumerate(t, 1):
        if s + v  > capacity:
            yield tup[curr:i]
            curr  = i
            s = v
        else:
            s += v
    yield tup[curr:]

>>> list(group_by_capacity((3, 1, 4, 2, 2, 1, 1, 2)))
[(3, 1), (4,), (2, 2, 1), (1, 2)]

Некоторое время:

In [35]: from random import randrange

In [36]: start = tuple((randrange(1,5) for _ in range(100000)))

In [37]: %%timeit
   ....: list(group_by_capacity(start))
   ....:
10 loops, best of 3: 47.4 ms per loop

In [38]: %%timeit
   ....: list(generate_tuple(start))
   ....:
10 loops, best of 3: 61.1 ms per loop

Ответ 4

Я немного удивлен, что никто еще не использовал itertools.accumulate с ключевой функцией. Во всяком случае, моя запись:

from itertools import groupby, accumulate

def sumgroup(seq, capacity):
    divided = accumulate(enumerate(seq),
                         lambda x,y: (x[0],x[1]+y[1])
                                     if x[1]+y[1] <= capacity else (x[0]+1,y[1]))
    seq_iter = iter(seq)
    grouped = groupby(divided, key=lambda x: x[0])
    return [[next(seq_iter) for _ in g] for _,g in grouped]

Существует множество вариантов, например. вы можете использовать zip(seq, divided), чтобы избежать seq_iter и т.д., но это был первый способ, который пришел на ум. Это дает мне

In [105]: seq = [3, 1, 4, 2, 2, 1, 1, 2]

In [106]: sumgroup(seq, 5)
Out[106]: [[3, 1], [4], [2, 2, 1], [1, 2]]

и согласуется с результатом GroupBySum:

In [108]: all(sumgroup(p, 5) == [list(l) for _, l in groupby(p, GroupBySum(5))]
     ...:     for width in range(1,8) for p in product(range(1,6), repeat=width))
     ...:     
     ...: 
Out[108]: True

Ответ 5

Я ждал первого ответа, чтобы обеспечить немного функциональный подход:

start = (3, 1, 4, 2, 2, 1, 1, 2)

def generate_tuple(inp):
    current_sum = 0
    current_list = []
    for e in inp:
        if current_sum + e <= 5:
            current_list.append(e)
            current_sum += e
        else:
            if current_list:  # fixes "6" in first position empty tuple bug
                yield tuple(current_list)
            current_list = [e]
            current_sum = e
    yield tuple(current_list)

print([i for i in generate_tuple(start)])

результат:

[(3, 1), (4,), (2, 2, 1), (1, 2)]

EDIT: я нашел полный функциональный подход, используя эффект памяти, иначе это не выполнимо. Это уродливо, и мне больно, только когда я думаю, как я это объясню. Я немного запустил набор входных данных или это было бы слишком легко

start = (6, 7, 3, 1, 4, 2, 2, 1, 1, 2, 3, 1 ,3, 1, 1)

теперь код. 3 линии, получить аспирин, вам понадобится столько, сколько я сделал:

mem=[0,0]
start = start + (5,)
print([start[mem[-2]:n] for i in range(0,len(start)) for n in range(i+1,len(start)) if ((n==i+1 and start[i]>=5) or (sum(start[mem[-1]:n])<=5 and sum(start[mem[-1]:n+1])>5)) and not mem.append(n)])

Я попытаюсь объяснить.

  • Я использую эффект памяти, потому что это невозможно без него. Сохраняется в mem и устанавливается на 0,0 при запуске
  • поскольку функция игнорирует последний элемент, я изменяю входные данные, чтобы добавить пороговое значение к предыдущим значениям, не отбрасывается
  • Единственная простая вещь - вычисление 2 сумм и обнаружение индекса, из которого он превышает порог. Когда этот порог обнаружен, оба условия выполняются, и активируется третье условие: индекс хранения в mem. Поскольку append возвращает None, последнее условие выполняется всегда true
  • Это ((n==i+1 and start[i]>=5) означает обнаружение одиночных значений, больших или равных 5.
  • Остальная часть - тонкая настройка до тех пор, пока результат не будет таким же, как и процедурный подход, который теперь выглядит не так уж плохо:)

Ответ 6

Не уверен, зачем они нужны в кортежах, но если вы этого не сделаете, вы можете просто удалить кастинг tuple(...):

def chunkit(tpl, capacity):
    ret = []
    cur = []
    for x in tpl:
        if sum(cur) + x > capacity:
            ret.append(tuple(cur))
            cur = [x]
        else:
            cur.append(x)
    if cur != []:
        ret.append(tuple(cur))

    return tuple(ret)

Несколько примеров:

In [24]: chunkit((3, 1, 4, 2, 2, 1, 1), 5)
Out[24]: ((3, 1), (4,), (2, 2, 1), (1,))

In [25]: chunkit((3, 1, 4, 2, 2, 1, ), 5)
Out[25]: ((3, 1), (4,), (2, 2, 1))

In [26]: chunkit((3, 1, 4, 2, 2, 1, 5), 5)
Out[26]: ((3, 1), (4,), (2, 2, 1), (5,))

In [27]: chunkit((3, 1, 4, 2, 2, 1, 5, 6), 5)
Out[27]: ((3, 1), (4,), (2, 2, 1), (5,), (6,))

In [28]: chunkit((3, 1, 4, 2, 2, 1, 5, 6, 1, 6), 5)
Out[28]: ((3, 1), (4,), (2, 2, 1), (5,), (6,), (1,), (6,))

Ответ 7

Не знаю, считается ли это функциональным, но это самое близкое, о чем я мог подумать:

def groupLimit(iterable, limit):
    i, cSum = 0, 0
    def pred(x):
        nonlocal i, cSum, limit
        i, cSum = (i + 1, x) if (x + cSum) > limit else (i, cSum + x)
        return i if x <= limit else -1
    return (tuple(g) for k, g in itertools.groupby(iterable, pred) if k != -1)

Это также будет сортировать одиночные значения, превышающие предел. Если это не означает, что последние две строки могут быть изменены на:

        return i
    return (tuple(g) for k, g in itertools.groupby(iterable, pred))

Пример:

t = (3, 1, 6, 2, 2, 1, 1, 2)
a = groupLimit(t,5)
print(tuple(a))
# version 1 -> ((3, 1), (2, 2, 1), (1, 2))
# version 2 -> ((3, 1), (6,), (2, 2, 1), (1, 2))

Ответ 8

Позволяет определить powerset с помощью itertools

from itertools import chain, combinations

def powerset(lst):
    for subset in chain.from_iterable(combinations(lst, r) for r in range(len(lst)+1)):
        yield subset

Тогда мы можем сделать это в однострочном

[subset for subset in powerset(input) if sum(subset)<=capacity]

Ответ 9

Более общее решение:

def groupwhile(iterable,predicate,accumulator_function):
    continue_group = False
    iterator = iter(iterable)
    try:
        accumulated = next(iterator)
    except StopIteration:
        return
    current_group = [accumulated]
    for item in iterator:
        continue_group = predicate(accumulated,item)
        if continue_group:
            current_group.append(item)
            accumulated = accumulator_function(accumulated,item)
        else:
            yield current_group
            accumulated = item
            current_group = [item]

    yield current_group

#your case
assert (list(groupwhile(
    (3, 1, 4, 2, 2, 1, 1, 2),
    lambda previous_sum,item: previous_sum + item <= 5,
    lambda previous_sum,item: previous_sum + item,
))) == [[3, 1], [4], [2, 2, 1], [1, 2]]

#equivalent to groupby with key not set
assert (list(groupwhile(
    (3, 1, 4, 2, 2, 1, 1, 2),
    lambda previous_item,item: previous_item == item,
    lambda _,item: item,
))) == [[3], [1], [4], [2, 2], [1, 1], [2]]

#break on duplicates
assert (list(groupwhile(
    (3, 1, 4, 2, 2, 1, 1, 2),
    lambda previous_item,item: previous_item != item,
    lambda _,item: item,
))) == [[3, 1, 4, 2], [2, 1], [1, 2]]

#start new group when the number is one
assert (list(groupwhile(
    (3, 1, 4, 2, 2, 1, 1, 2),
    lambda _,item: item != 1,
    lambda _1,_2: None,
))) == [[3], [1, 4, 2, 2], [1], [1, 2]]

Ответ 10

Мое решение, не очень чистое, но оно просто уменьшает:

# int, (int, int, ...) -> ((int, ...), ...)
def grupBySum(capacity, _tuple):

    def  _grupBySum(prev, number):
        counter = prev['counter']
        result = prev['result']
        counter = counter + (number,)
        if sum(counter) > capacity:
            result = result + (counter[:-1],)
            return {'counter': (number,), 'result': result}
        else:
            return {'counter': counter, 'result': result}

result = reduce(_grupBySum, _tuple, {'counter': (), 'result': ()}).values()
return result[1]  + (result[0],)

f = (3, 1, 4, 2, 2, 1, 1, 2)
h = grupBySum(5, f)
print(h) # -> ((3, 1), (4,), (2, 2, 1), (1, 2))