Сложность времени, касающаяся списка размером 10 ** 6 - PullRequest
3 голосов
/ 23 сентября 2019

Недавно я провалил задачу кодирования, которая связана со сложностью времени.Я занимался этим в свое свободное время, но все еще не могу заставить его работать быстро для больших списков.Сначала я переосмыслил проблему, реорганизовал ее и т. Д., Сделал некоторые дополнительные улучшения, попытался использовать pandas (что оказалось намного медленнее) и т. Д.

Iмне интересно узнать, какие методы я мог бы использовать для повышения скорости выполнения этого кода.

Ввод: список с максимальным размером 10**6, содержащий несортированные целые числа в range(1,10**5).

Задача состоит в том, чтобы вычислить «общую цену» из этой произвольной конструкции и вернуть «общую цену» и упорядоченный список индексов , представляющих те элементы, которыеесли не со скидкой.

Цена товара по индексу i снижается на следующий меньший / меньший элемент .Если в items[i+1:] нет меньших значений, цена товара не дисконтируется (или вы можете считать, что она дисконтирована 0).

Пример ввода: items = [5, 3, 4, 1, 5]

Пример вывода: 13, [3, 4]

Здесь items[0] дисконтируется items[1], items[1] дисконтируется items[3], items[2] дисконтируетсяitems[3], items[3] и items[4] не обесцениваются.

Таким образом, общая цена составляет 13, определяемая как (5-3) + (3-1) + (4-1) + (1-0) + (5-0)

У меня есть функция, которая решает эту проблему довольно быстро в большинстве случаев, но по мере приближения к максимумуразмер списка, это занимает гораздо больше времени.Например, список длиной 50000 обрабатывается за <1 секунду.Список длиной 100К обрабатывается за <3 секунды.Список длиной 200К занимает <10 секунд, а 400К занимает около 50 секунд.<strong> Работа с миллионом элементов занимает ~ 1000 + секунд.

Для тестирования я создаю большой список примерно так, а затем передаю его (или его фрагменты) функциям, например:

data = list(np.array(np.random.randint(1,10**5,(10**6)), dtype='int64'))
total, full_price = get_total(data[:100000])

Вот более быстрая, не pandas функция:

def get_total(data):
    init_total = sum(data)
    items = data[:] 
    size = len(items)
    discount = [get_discount(items.pop(0),items) for i in range(size)]
    full = [i for (i,v) in enumerate(discount) if v == 0]
    total = init_total - sum(discount)
    return total, full, None

def get_discount(this, _items):
    next_lowest_index, discount = next(((x,val) for x, val in enumerate(_items) if val < this), (np.NaN, 0))
    return discount

Я упоминал, что я тоже пробовал pandas, но этот код много медленнее даже в небольших списках (n = 1000).Я попытался отсортировать его по значению:

def frame_total(data):
    if type(data) == list:
        data = pd.DataFrame(data)
    data = data[:].sort_values(0, 'index')
    df = pd.DataFrame({ 'val':data[0],
                        'discount': [0] * data.shape[0]
                        }, dtype='int')
    df.discount = [next(iter(df.loc[(df.index > i) & (df.val < row.val)].sort_index().val),0) 
                   for i,row in df.iterrows()]
    total = data.sum() - df.discount.sum()
    full_indices = list(df[df.discount == 0].sort_index().index)
    return total, full_indices, None

И другой, который не сортирует входные данные, который не заметно быстрее:

def frame2(data):
    if type(data) == list:
        data = pd.DataFrame(data)
    data = data[:]
    df = pd.DataFrame({ 'val':data[0],
                        'discount': [0] * data.shape[0]
                        }, dtype='int')
    df.discount = [next(iter(df.val[i+1:].loc[df.val < row.val]),0) for i,row in df.iterrows()]
    total = data.sum() - df.discount.sum()
    full_indices = list(df[df.discount == 0].index)
    return total, full_indices, None

Обратите внимание, что полные цены более вероятнысуществовать ближе к концу списка (при увеличении i вероятность того, что любое значение <<code>items[i] существует в items[i+1:], уменьшается).Я чувствую, что это важно, но я не могу понять, как это использовать.

Решено, спасибо @DarrylG и объяснение здесь

def get_next_smallest(data,default=0):
    """
        returns the discounted value for all items in a list
        discounted value is the next smaller item in the list, e.g.:
        for any n, the next smallest item is the first item in data[n+1:] < data[n]
        provides O(n) complexity solution.
    """
    discounts=[default for i in data] # stores the corresponding next smaller value
    stack = [] # initialize our empty stack
    for i, this in enumerate(data):
        while len(stack) > 0 and this < data[stack[-1]]:
            discounts[stack.pop()] = this
        stack.append(i)
    return discounts

def get_total(data):
    init_total = sum(data)
    default = 0  # should be a value that will NOT be present in the data, like 0 or -1
    discounts = get_next_smallest(data, default)
    full = [i for i,v in enumerate(discounts) if v == default]
    total = init_total - sum(discounts)
    return total, full

Ответы [ 3 ]

2 голосов
/ 23 сентября 2019

Вот алгоритм O (n) - с использованием алгоритма из Для данного массива найдите следующий меньший элемент для каждого элемента , чтобы найти следующий меньший элемент

def find_next_smaller_elements(xs):
 " finds next smallest element in O(n) "
    ys=[-1 for x in xs]
    stack=[]
    for i,x in enumerate(xs):
        while len(stack)>0 and x<xs[stack[-1]]:
           ys[stack.pop()]=x
        stack.append(i)
    return ys

def get_total(data):
" Computes desired cost function "
    next_smaller = find_next_smaller_elements(data)

    return sum([ x[0] if x[1] == -1 else x[0]-x[1]  for x in list(zip(data, next_smaller))])

Тест(маленький список)

data = [5, 3, 4, 1, 5]
print(get_total(data)) # 13

Сроки теста

for k in [1000, 10000, 100000, 1000000]:
    data = list(np.array(np.random.randint(1,10**5,k, dtype='int64')))
    t0 = time.time()
    ans = get_total(data)
    print(k, time.time()-t0)

Результаты:

  • No.Items => Время (секунды)
  • 1000 => 0,0029
  • 10000 => 0,0369
  • 100000 => 0,2059
  • 1000000 => 1,96400

Таким образом, миллион предметов только в 2секунд.

1 голос
/ 25 сентября 2019

Итерируя ваши данные в обратном порядке, как предлагает @Mad Physicist, вы можете получить алгоритм, требующий гораздо меньше памяти и при этом более быстрый:

def get_total(data):
    tot = sum(data)
    smallest_tail = deque()
    no_discount = []
    i = len(data) - 1 # manually handle the index
    for x in reversed(data):
        while smallest_tail:
            s = smallest_tail[-1]
            if s >= x: # s won't be next smaller for anyone because of x
                smallest_tail.pop()
            else:
                tot -= s
                break
        if not smallest_tail:
            no_discount.append(i)
        smallest_tail.append(x)
        i -= 1
    return tot, list(reversed(no_discount))

по сравнению с вашим текущим решением (на моей машине):

:data = list(np.array(np.random.randint(1, 10**5, 10**6, dtype='int64')))
:get_total_dz(data) == get_total(data)
True
:%timeit r = get_total_dz(data) # yours, replacing 'len(stack) > 0' with 'stack'
672 ms ± 6.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
:%timeit r = get_total(data) # mine
435 ms ± 2.29 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1 голос
/ 23 сентября 2019

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

def find_undiscounted(data):
    skipped = [len(data) - 1]
    current = data[-1]
    for i in range(len(data) - 2, -1, -1):
        if current >= data[i]:
            skipped.append(i)
            current = data[i]
    return skipped[::-1]

Для комплексного решения потребуется стек, но его вполне можно сделать за один проход.Не забудьте использовать collections.deque, если решите реализовать его таким образом.

...