Оновлення: Перший описаний тут алгоритм застарілий другою відповіддю Арміна Ріго , яка набагато простіша та ефективніша. Але обидва ці методи мають один недолік. Їм потрібно багато годин, щоб знайти результат для одного мільйона цілих чисел. Тож я спробував ще два варіанти (див. Другу половину цієї відповіді), де діапазон вхідних цілих чисел вважається обмеженим. Таке обмеження дозволяє набагато швидші алгоритми. Також я намагався оптимізувати код Armin Rigo. Подивіться мої результати порівняльного аналізу в кінці.
Ось ідея алгоритму, що використовує O (N) пам’ять. Складність часу становить O (N 2 log N), але може бути зменшена до O (N 2 ).
Алгоритм використовує такі структури даних:
prev: масив індексів, що вказують на попередній елемент (можливо, неповний) підпослідовності.
hash: хеш-карта з ключем = різниця між послідовними парами в послідовності та значення = дві інші хеш-карти. Для цих інших хеш-карт: ключ = початковий / кінцевий індекс підпослідовності, значення = пара (довжина підпослідовності, кінцевий / початковий індекс підпослідовності).
pq: черга пріоритетів для всіх можливих значень "різниці" для послідовностей, що зберігаються в prevта hash.
Алгоритм:
- Ініціалізувати за
prevдопомогою індексів i-1. Оновлення hashтаpq зареєструйте всі (неповні) підпослідовності, знайдені на цьому кроці, та їх "відмінності".
- Отримати (і видалити) найменшу "різницю" від
pq. Отримайте відповідний запис hashта відскануйте одну з хеш-карт другого рівня. На цей час усі підпослідовності з заданою "різницею" завершені. Якщо хеш-карта другого рівня містить довжину підпослідовностей краще, ніж досі, оновіть найкращий результат.
- У масиві
prev: для кожного елемента будь-якої послідовності, знайденої на кроці 2, індекс зменшення та оновлення hashта, можливо pq. Під час оновлення hashми могли виконати одну з наступних операцій: додати нову підпослідовність довжиною 1, або збільшити деяку існуючу підпослідовність на 1, або об’єднати дві існуючі підпослідовності.
- Видаліть запис хеш-карти, знайдений на кроці 2.
- Продовжуйте з кроку 2, поки
pqне є порожнім.
Цей алгоритм оновлює O (N) елементів prevO (N) разів кожен. І для кожного з цих оновлень може знадобитися додати нову "різницю" pq. Все це означає часову складність O (N 2 log N), якщо ми використовуємо просту реалізацію купи для pq. Щоб зменшити його до O (N 2 ), ми могли б використовувати більш розширені реалізації черги пріоритетів. Деякі можливості перелічені на цій сторінці: Пріоритетні черги .
Дивіться відповідний код Python на Ideone . Цей код не допускає повторення елементів у списку. Це можна виправити, але в будь-якому випадку було б непоганою оптимізацією видалити дублікати (і окремо знайти найдовшу послідовність, крім дублікатів).
І той самий код після невеликої оптимізації . Тут пошук припиняється, як тільки довжина послідовності, помножена на можливу "різницю" підпослідовності, перевищує діапазон списку джерел.
Код Armin Rigo простий і досить ефективний. Але в деяких випадках він робить деякі додаткові обчислення, яких можна уникнути. Пошук може бути припинено, як тільки довжина послідовності, помножена на можливу "різницю" підпослідовності, перевищує діапазон списку джерел:
def findLESS(A):
Aset = set(A)
lmax = 2
d = 1
minStep = 0
while (lmax - 1) * minStep <= A[-1] - A[0]:
minStep = A[-1] - A[0] + 1
for j, b in enumerate(A):
if j+d < len(A):
a = A[j+d]
step = a - b
minStep = min(minStep, step)
if a + step in Aset and b - step not in Aset:
c = a + step
count = 3
while c + step in Aset:
c += step
count += 1
if count > lmax:
lmax = count
d += 1
return lmax
print(findLESS([1, 4, 5, 7, 8, 12]))
Якщо діапазон цілих чисел у вихідних даних (M) невеликий, можливий простий алгоритм з O (M 2 ) часом та O (M) простором:
def findLESS(src):
r = [False for i in range(src[-1]+1)]
for x in src:
r[x] = True
d = 1
best = 1
while best * d < len(r):
for s in range(d):
l = 0
for i in range(s, len(r), d):
if r[i]:
l += 1
best = max(best, l)
else:
l = 0
d += 1
return best
print(findLESS([1, 4, 5, 7, 8, 12]))
Це схоже на перший метод Арміна Ріго, але він не використовує ніяких динамічних структур даних. Припускаю, вихідні дані не мають дублікатів. І (щоб спростити код), я також припускаю, що мінімальне вхідне значення є невід’ємним і близьким до нуля.
Попередній алгоритм може бути вдосконалений, якщо замість масиву булевих символів ми використовуємо структуру даних бітового набору та побітові операції для паралельної обробки даних. Наведений нижче код реалізує бітсет як вбудоване ціле число Python. Він має однакові припущення: відсутність дублікатів, мінімальне вхідне значення невід’ємне та близьке до нуля. Складність часу - O (M 2 * log L), де L - довжина оптимальної підпослідовності, складність простору - O (M):
def findLESS(src):
r = 0
for x in src:
r |= 1 << x
d = 1
best = 1
while best * d < src[-1] + 1:
c = best
rr = r
while c & (c-1):
cc = c & -c
rr &= rr >> (cc * d)
c &= c-1
while c != 1:
c = c >> 1
rr &= rr >> (c * d)
rr &= rr >> d
while rr:
rr &= rr >> d
best += 1
d += 1
return best
Тести:
Вхідні дані (близько 100000 цілих чисел) генеруються таким чином:
random.seed(42)
s = sorted(list(set([random.randint(0,200000) for r in xrange(140000)])))
А для найшвидших алгоритмів я також використовував такі дані (близько 1000000 цілих чисел):
s = sorted(list(set([random.randint(0,2000000) for r in xrange(1400000)])))
Усі результати показують час у секундах:
Size: 100000 1000000
Second answer by Armin Rigo: 634 ?
By Armin Rigo, optimized: 64 >5000
O(M^2) algorithm: 53 2940
O(M^2*L) algorithm: 7 711