Ускоренная техника Python подсчитывает тройки из списка чисел, кратных друг другу

Предположим, что у нас есть список чисел, l. Мне нужно СЧЕТЬ все кортежи длины 3 из l, (l_i,l_j,l_k), что l_i равномерно делит l_j, а l_j равномерно делит l_k. При условии, что индексы i,j,k имеют отношение i<j<k

Т.е.;

Если l=[1,2,3,4,5,6], то кортежи будут [1,2,6], [1,3,6],[1,2,4], поэтому COUNT будет 3.

Если l=[1,1,1], то единственным кортежем будет [1,1,1], поэтому COUNT будет равным 1.

Вот что я сделал до сих пор, используя списки:

def myCOUNT(l):
    newlist=[[x,y,z] for x in l for y in l for z in l if (z%y==0 and y%x==0 and l.index(x)<l.index(y) and l.index(y)<l.index(z))]
    return len(newlist)

>>>l=[1,2,3,4,5,6]
>>>myCOUNT(l)
3

Это работает, но по мере того, как l становится длиннее (и может достигать 2000 элементов), время, которое требуется, увеличивается слишком сильно. Есть ли более быстрый/лучший способ сделать это?

Ответ 1

Мы можем подсчитать количество троек с заданным числом посередине, подсчитав, сколько факторов этого числа осталось слева от него, подсчитывая, сколько кратных этого числа справа от них и умножается. Выполнение этого для любого заданного среднего элемента - это O (n) для списка длины-n, и для всех n возможных средних элементов это O (n ^ 2).

def num_triples(l):
    total = 0
    for mid_i, mid in enumerate(l):
        num_left = sum(1 for x in l[:mid_i] if mid % x == 0)
        num_right = sum(1 for x in l[mid_i+1:] if x % mid == 0)
        total += num_left * num_right
    return total

Кстати, код в вашем вопросе на самом деле не работает. Он попал в общую ловушку для новичков, указав index вместо того чтобы использовать enumerate для получения индексов итераций. Это не так просто, но на самом деле это неправильно, когда на входе есть повторяющиеся элементы, заставляя ваш myCOUNT возвращать 0 вместо 1 на примере ввода [1, 1, 1].

Ответ 2

Поиск всех кортежей в O (n 2)

Алгоритм алгоритма повторяется во всех возможных комбинациях, что делает его O (n 3).

Вместо этого вы должны прекомпилировать дерево разделения вашего списка чисел и восстановить троек с путей по дереву.

Дерево отделки

Дерево деления - это граф, узлы которого являются числами, а дети являются кратными каждому числу.

Например, учитывая список [1, 2, 3, 4], дерево деления выглядит так.

   1
  /|\
 2 | 3
  \|
   4

Вычисление дерева деления требует сравнить каждое число против всех остальных, создавая его создание O (n 2).

Вот базовая реализация дерева разделов, которое может быть использовано для вашей проблемы.

class DivisionTree:
    def __init__(self, values):
        values = sorted(values)

        # For a division-tree to be connected, we need 1 to be its head
        # Thus we artificially add it and note whether it was actually in our numbers
        if 1 in values:
            self._has_one = True
            values = values[1:]
        else:
            self._has_one = False

        self._graph = {1: []}

        for v in values:
            self.insert(v)

    def __iter__(self):
        """Iterate over all values of the division-tree"""
        yield from self._graph

    def insert(self, n):
        """Insert value in division tree, adding it as child of each divisor"""
        self._graph[n] = []

        for x in self:
            if n != x and n % x == 0:
                self._graph[x].append(n)

    def paths(self, depth, _from=1):
        """Return a generator of all paths of *depth* down the division-tree"""
        if _from == 1:
            for x in self._graph[_from]:
                yield from self.paths(depth , _from=x)

        if depth == 1:
            yield [_from]
            return

        if _from != 1 or self._has_one:
            for x in self._graph[_from]:
                for p in self.paths(depth - 1, _from=x):
                    yield [_from, *p]

использование

Когда мы построили DivisionTree, достаточно выполнить итерацию по всем путям по графику и выбрать только те, которые имеют длину 3.

пример

l = [1,2,3,4,5,6]

dt = DivisionTree(l)

for p in dt.paths(3):
    print(p)

Выход

[1, 2, 4]
[1, 2, 6]
[1, 3, 6]

Это решение предполагает, что список номеров изначально отсортирован, как в вашем примере. Хотя выход можно отфильтровать относительно условия на индексы i < j < k чтобы обеспечить более общее решение.

Сложность времени

Генерирование дерева деления - O (n 2).

В свою очередь, может быть до n! разные пути, хотя остановка итерации всякий раз, когда мы идем глубже, чем 3, препятствует их перемещению. Это заставляет нас перебирать по следующим путям:

  • пути, соответствующие трем кортежам, говорят, что их m;

  • пути, соответствующие двум кортежам, из них O (n 2);

  • пути, соответствующие одному кортежу, есть O (n) из них.

Таким образом, это в целом дает алгоритм O (n 2 + m).

Ответ 3

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

a = [1, 2, 3, 4, 5, 6]

def count(a):   
    result = 0
    length = len(a)

    for i in range(length):
        for j in range(i + 1, length):
            for k in range(j + 1, length):
                if a[j] % a[i] == 0 and a[k] % a[j] == 0:
                    result += 1

    return result

print(count(a))

Выход:

3

В вашем методе index решения слишком дорог (требуется O (n) операций). Также вам не нужно перебирать полный список для каждого x, y и z (x = a[i], y = a[j], z = a[k]). Обратите внимание, как я использую индексы в моих циклах для y и z потому что я знаю, что всегда выполняется a.index(x) < a.index(y) < a.index(z).


Вы можете написать это как один вкладыш:

def count(a):   
    length = len(a)

    return sum(1 for i in range(length)
               for j in range(i + 1, length)
               for k in range(j + 1, length)
               if a[j] % a[i] == 0 and a[k] % a[j] == 0)

PS Пожалуйста, не используйте букву l для имен переменных, потому что она очень похожа на 1 :)

Ответ 4

Существует способ сделать это с помощью комбинаций itertools:

from itertools import combinations 
l=[1,2,3,4,5,6] 

>>> [(x,y,z) for x,y,z in combinations(l,3) if z%y==0 and y%x==0]
[(1, 2, 4), (1, 2, 6), (1, 3, 6)]

Поскольку комбинации генерируют кортежи в порядке списка, вам не нужно проверять индекс z.

Тогда ваша функция myCOUNT станет:

def cnt(li):
    return sum(1 for x,y,z in combinations(li,3) if z%y==0 and y%x==0)

>>> cnt([1,1,1])
1
>>> cnt([1,2,3,4,5,6])
3

Это известная проблема.

Ниже приведены некоторые моменты для решений:

from itertools import combinations 

class DivisionTree:
    def __init__(self, values):    
        # For a division-tree to be connected, we need 1 to be its head
        # Thus we artificially add it and note whether it was actually in our numbers
        if 1 in values:
            self._has_one = True
            values = values[1:]
        else:
            self._has_one = False

        self._graph = {1: []}

        for v in values:
            self.insert(v)

    def __iter__(self):
        """Iterate over all values of the division-tree"""
        yield from self._graph

    def insert(self, n):
        """Insert value in division tree, adding it as child of each divisor"""
        self._graph[n] = []

        for x in self:
            if n != x and n % x == 0:
                self._graph[x].append(n)

    def paths(self, depth, _from=1):
        """Return a generator of all paths of *depth* down the division-tree"""
        if _from == 1:
            for x in self._graph[_from]:
                yield from self.paths(depth , _from=x)

        if depth == 1:
            yield [_from]
            return

        if _from != 1 or self._has_one:
            for x in self._graph[_from]:
                for p in self.paths(depth - 1, _from=x):
                    yield [_from, *p]


def f1(li):
    return sum(1 for x,y,z in combinations(li,3) if z%y==0 and y%x==0)

def f2(l):
    newlist=[[x,y,z] for x in l for y in l for z in l if (z%y==0 and y%x==0 and l.index(x)<l.index(y) and l.index(y)<l.index(z))]
    return len(newlist)

def f3(a):   
    result = 0
    length = len(a)

    for i in range(length):
        for j in range(i + 1, length):
            for k in range(j + 1, length):
                if a[j] % a[i] == 0 and a[k] % a[j] == 0:
                    result += 1

    return result

def f4(l):
    dt = DivisionTree(l)
    return sum(1 for _ in dt.paths(3))  

def f5(l):
    total = 0
    for mid_i, mid in enumerate(l):
        num_left = sum(1 for x in l[:mid_i] if mid % x == 0)
        num_right = sum(1 for x in l[mid_i+1:] if x % mid == 0)
        total += num_left * num_right
    return total    


if __name__=='__main__':
    import timeit
    tl=list(range(3,155))
    funcs=(f1,f2,f3,f4,f5)
    td={f.__name__:f(tl) for f in funcs}
    print(td)
    for case, x in (('small',50),('medium',500),('large',5000)):
        li=list(range(2,x))
        print('{}: {} elements'.format(case,x))
        for f in funcs:
            print("   {:^10s}{:.4f} secs".format(f.__name__, timeit.timeit("f(li)", setup="from __main__ import f, li", number=1))) 

И результаты:

{'f1': 463, 'f2': 463, 'f3': 463, 'f4': 463, 'f5': 463}
small: 50 elements
       f1    0.0010 secs
       f2    0.0056 secs
       f3    0.0018 secs
       f4    0.0003 secs
       f5    0.0002 secs
medium: 500 elements
       f1    1.1702 secs
       f2    5.3396 secs
       f3    1.8519 secs
       f4    0.0156 secs
       f5    0.0110 secs
large: 5000 elements
       f1    1527.4956 secs
       f2    6245.9930 secs
       f3    2074.2257 secs
       f4    1.3492 secs
       f5    1.2993 secs

Вы можете видеть, что f1,f2,f3 явно O (n ^ 3) или хуже, а f4,f5 - O (n ^ 2). f2 заняло более 90 минут за то, что f4 и f5 сделали за 1,3 секунды.

Ответ 5

Решение в O (M * log (M)) для отсортированного списка, содержащего положительные числа

Как ответил user2357112, мы можем подсчитать количество триплетов в O (n ^ 2), вычислив для каждого числа число его коэффициентов и кратность. Однако, если вместо сравнения каждой пары мы переходим через ее кратность меньше наибольшего числа и проверяем, находятся ли они в списке, мы можем изменить эффективность на O (N + M * log (N)), когда M является наибольшим число в списке.

Код:

def countTriples(myList):
    counts = {} #Contains the number of appearances of every number.
    factors = {} #Contains the number of factors of every number.
    multiples = {} #Contains the number of multiples of every number.

    for i in myList: #Initializing the dictionaries.
        counts[i] = 0
        factors[i] = 0
        multiples[i] = 0

    maxNum = max(myList) #The maximum number in the list.

    #First, we count the number of appearances of every number.
    for i in myList:
        counts[i] += 1

    #Then, for every number in the list, we check whether its multiples are in the list.
    for i in counts:
        for j in range(2*i, maxNum+1, i):
            if(counts.has_key(j)):
                factors[j] += counts[i]
                multiples[i] += counts[j]

    #Finally, we count the number of triples.
    ans = 0
    for i in counts:
        ans += counts[i]*factors[i]*multiples[i] #Counting triplets with three numbers.
        ans += counts[i]*(counts[i]-1)*factors[i]/2 #Counting triplets with two larger and one smaller number.
        ans += counts[i]*(counts[i]-1)*multiples[i]/2 #Counting triplets with two smaller numbers and one larger number.
        ans += counts[i]*(counts[i]-1)*(counts[i]-2)/6 #Counting triplets with three copies of the same number.

    return ans

Хотя это решение будет быстро работать для списков, содержащих множество небольших чисел, оно не будет работать для списков, содержащих большие числа:

countTriples(list(range(1,1000000)) #Took 80 seconds on my computer
countTriples([1,2,1000000000000]) #Will take a very long time

Быстрое решение с неизвестной эффективностью для несортированных списков

Другим методом подсчета количества кратных и коэффициентов каждого числа в списке будет использование структуры данных двоичного дерева, причем листья соответствуют номерам. Структура данных поддерживает три операции:

1) Добавьте число в каждую позицию, кратное числу.

2) Добавьте число в каждую позицию, указанную в наборе.

3) Получите значение позиции.

Мы используем ленивое распространение и распространяем обновления от корня до нижних узлов только во время запросов.

Чтобы найти количество факторов каждого элемента в списке, мы перебираем список, запрашиваем количество факторов текущего элемента из структуры данных и добавляем 1 к каждой позиции, которая кратно элементу.

Чтобы найти число кратных каждому элементу, мы сначала найдем для каждого элемента в списке все его факторы, используя алгоритм, описанный в предыдущем решении.

Затем мы перебираем список в обратном порядке. Для каждого элемента мы запрашиваем количество его кратных из структуры данных и добавляем 1 к его факторам в структуре данных.

Наконец, для каждого элемента добавим умножение его коэффициентов и умножим на ответ.

Код:

'''A tree that supports two operations:
    addOrder(num) - If given a number, adds 1 to all the values which are multiples of the given number. If given a tuple, adds 1 to all the values in the tuple.
    getValue(num) - returns the value of the number.
    Uses lazy evaluation to speed up the algorithm.
'''
class fen:
    '''Initiates the tree from either a list, or a segment of the list designated by s and e'''
    def __init__(this, l, s = 0, e = -1):
        if(e == -1): e = len(l)-1
        this.x1 = l[s]
        this.x2 = l[e]

        this.val = 0
        this.orders = {}

        if(s != e):
            this.s1 = fen(l, s, (s+e)/2)
            this.s2 = fen(l, (s+e)/2+1, e)
        else:
            this.s1 = None
            this.s2 = None

    '''Testing if a multiple of the number appears in the range of this node.'''
    def _numGood(this, num):
        if(this.x2-this.x1+1 >= num): return True
        m1 = this.x1%num
        m2 = this.x2%num
        return m1 == 0 or m1 > m2

    '''Testing if a member of the group appears in the range of this node.'''
    def _groupGood(this, group):
        low = 0
        high = len(group)
        if(this.x1 <= group[0] <= this.x2): return True
        while(low != high-1):
            mid = (low+high)/2;
            if(group[mid] < this.x1): low = mid
            elif(group[mid] > this.x2): high = mid
            else: return True
        return False

    def _isGood(this, val):
        if(type(val) == tuple):
            return this._groupGood(val)
        return this._numGood(val)

    '''Adds an order to this node.'''
    def addOrder(this, num, count = 1):
        if(not this._isGood(num)): return
        if(this.x1 == this.x2): this.val += count
        else :this.orders[num] = this.orders.get(num, 0)+count

    '''Pushes the orders to lower nodes.''' 
    def _pushOrders(this):
        if(this.x1 == this.x2): return
        for i in this.orders:
            this.s1.addOrder(i, this.orders[i])
            this.s2.addOrder(i, this.orders[i])
        this.orders = {}

    def getValue(this, num):
        this._pushOrders()
        if(num < this.x1 or num > this.x2):
            return 0
        if(this.x1 == this.x2):
            return this.val
        return this.s1.getValue(num)+this.s2.getValue(num)

def countTriples2(myList):
    factors = [0 for i in myList]
    multiples = [0 for i in myList]

    numSet = set((abs(i) for i in myList))
    sortedList = sorted(list(numSet))

    #Calculating factors.
    tree = fen(sortedList)
    for i in range(len(myList)):
        factors[i] = tree.getValue(abs(myList[i]))
        tree.addOrder(abs(myList[i]))

    #Calculating the divisors of every number in the group.
    mxNum = max(numSet)
    divisors = {i:[] for i in numSet}

    for i in sortedList:
        for j in range(i, mxNum+1, i):
            if(j in numSet):
                divisors[j].append(i)
    divisors = {i:tuple(divisors[i]) for i in divisors}
    #Calculating the number of multiples to the right of every number.
    tree = fen(sortedList)
    for i in range(len(myList)-1, -1, -1):
        multiples[i] = tree.getValue(abs(myList[i]))
        tree.addOrder(divisors[abs(myList[i])])

    ans = 0
    for i in range(len(myList)):
        ans += factors[i]*multiples[i]

    return ans

Это решение работало для списка с номерами 1..10000 за шесть секунд на моем компьютере и для списка, содержащего номера 1..100000 за 87 секунд.