Самая длинная равноотстоящая подпоследовательность
У меня есть миллион целых чисел в отсортированном порядке, и я хотел бы найти самую длинную подпоследовательность, где разница между последовательными парами равна. Например
1, 4, 5, 7, 8, 12
имеет подпоследовательность
4, 8, 12
Мой наивный метод жадный и просто проверяет, как далеко вы можете расширить подпоследовательность из каждой точки. Это занимает время O(n²)
для каждой точки.
Есть ли более быстрый способ решить эту проблему?
Обновление. Я буду проверять код, указанный в ответах, как можно скорее (спасибо). Однако уже ясно, что использование памяти n ^ 2 не будет работать. До сих пор нет кода, который заканчивается при вводе как [random.randint(0,100000) for r in xrange(200000)]
.
Сроки. Я тестировал со следующими входными данными в своей 32-битной системе.
a= [random.randint(0,10000) for r in xrange(20000)]
a.sort()
- Метод динамического программирования ZelluX использует 1.6G оперативной памяти и занимает 2 минуты и 14 секунд. С pypy требуется всего 9 секунд! Однако он падает с ошибкой памяти на больших входах.
- Метод O (nd) времени Armin занял 9 секунд с pypy, но только 20 МБ ОЗУ. Конечно, это было бы намного хуже, если бы диапазон был намного больше. Низкое использование памяти означало, что я мог бы также протестировать его с помощью = = [random.randint(0,100000) для r в xrange (200000)], но он не завершился за несколько минут, которые я дал ему с помощью pypy.
Чтобы проверить метод Kluev I, запустите
a= [random.randint(0,40000) for r in xrange(28000)]
a = list(set(a))
a.sort()
чтобы составить список длин примерно 20000
. Все тайминги с pypy
- ZelluX, 9 секунд
- Kluev, 20 секунд
- Armin, 52 секунды
Похоже, что если метод ZelluX можно было бы сделать линейным, это было бы явным победителем.
Ответы
Ответ 1
Обновление:. Первый описанный здесь алгоритм устарел вторым ответом Армин Риго, который намного проще и эффективнее. Но оба этих метода имеют один недостаток. Им нужно много часов, чтобы найти результат для миллиона целых чисел. Поэтому я попробовал еще два варианта (см. Вторую половину ответа), где диапазон входных целых чисел считается ограниченным. Такое ограничение допускает гораздо более быстрые алгоритмы. Также я попытался оптимизировать код Armin Rigo. Посмотрите результаты тестов в конце.
Вот идея алгоритма с использованием памяти O (N). Сложность времени - O (N 2 log N), но может быть уменьшена до O (N 2).
Алгоритм использует следующие структуры данных:
-
prev
: массив индексов, указывающий на предыдущий элемент (возможно, неполной) подпоследовательности.
-
hash
: hashmap с ключом = разность между последовательными парами в подпоследовательности и значением = два других хэшмапа. Для этих других hashmaps: key = начальный/конечный индекс подпоследовательности, value = пара (длина подпоследовательности, конечный/начальный индекс подпоследовательности).
-
pq
: очередь приоритетов для всех возможных "разностных" значений для подпоследовательностей, хранящихся в prev
и hash
.
Алгоритм:
- Инициализировать
prev
с помощью индексов i-1
. Обновите hash
и pq
, чтобы зарегистрировать все (неполные) подпоследовательности, найденные на этом шаге, и их "отличия".
- Получить (и удалить) самую маленькую "разницу" из
pq
. Получите соответствующую запись из hash
и сканируйте одну из хэш-карт второго уровня. В это время все подпоследовательности с заданной "разностью" полны. Если хэш-карта второго уровня содержит длину подпоследовательности, которая лучше, чем найденная до сих пор, обновите лучший результат.
- В массиве
prev
: для каждого элемента любой последовательности, найденной на шаге 2, индекс декремента и обновление hash
и, возможно, pq
. При обновлении hash
мы могли бы выполнить одну из следующих операций: добавить новую подпоследовательность длины 1 или вырастить некоторую существующую подпоследовательность на 1 или слить две существующие подпоследовательности.
- Удалить запись хэш-карты, найденную на шаге 2.
- Продолжайте с шага # 2, а
pq
не пуст.
Этот алгоритм обновляет O (N) элементов prev
O (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]))
Он похож на первый метод Armin Rigo, но он не использует никаких динамических структур данных. Я полагаю, что исходные данные не имеют дубликатов. И (чтобы код был прост), я также предполагаю, что минимальное входное значение неотрицательно и близко к нулю.
Предыдущий алгоритм может быть улучшен, если вместо массива булевых элементов мы используем структуру данных битов и побитовые операции для параллельного обработки данных. Приведенный ниже код реализует битрейт как встроенное целое число 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
Ответ 2
Мы можем иметь решение O(n*m)
во времени с очень небольшими потребностями в памяти, адаптируя ваши. Здесь n
- количество элементов в данной входной последовательности чисел, а m
- это диапазон, т.е. Наибольшее число минус наименьшее.
Вызвать A последовательность всех входных номеров (и использовать предварительно вычисленный set()
для ответа в постоянное время вопрос "это число в A?" ). Вызов d шаг подпоследовательности, который мы ищем (разница между двумя номерами этой подпоследовательности). Для любого возможного значения d выполните следующее линейное сканирование по всем входным номерам: для каждого числа n из A в порядке возрастания, если число еще не было видно, посмотрите в на длину последовательности, начиная с n, с шаг d. Затем отметьте все элементы в этой последовательности, как уже было видно, так что мы не будем искать их снова, для того же d. Из-за этого сложность составляет всего O(n)
для каждого значения d.
A = [1, 4, 5, 7, 8, 12] # in sorted order
Aset = set(A)
for d in range(1, 12):
already_seen = set()
for a in A:
if a not in already_seen:
b = a
count = 1
while b + d in Aset:
b += d
count += 1
already_seen.add(b)
print "found %d items in %d .. %d" % (count, a, b)
# collect here the largest 'count'
Обновление:
-
Это решение может быть достаточно хорошим, если вас интересуют только значения d, которые относительно малы; например, если получение наилучшего результата для d <= 1000
будет достаточно хорошим. Тогда сложность уменьшается до O(n*1000)
. Это делает алгоритм аппроксимативным, но фактически выполнимым для n=1000000
. (Измеряется через 400-500 секунд с CPython, 80-90 секунд с PyPy со случайным подмножеством чисел от 0 до 10'000'000.)
-
Если вы все еще хотите найти весь диапазон, и если общий случай состоит в том, что существуют длинные последовательности, заметным улучшением является остановка, как только d будет слишком большим для получения еще более длинной последовательности.
Ответ 3
ОБНОВЛЕНИЕ: я нашел статью по этой проблеме, вы можете скачать ее здесь.
Вот решение, основанное на динамическом программировании. Для этого требуется сложность времени O (n ^ 2) и сложность пространства O (n ^ 2) и не использует хеширование.
Предположим, что все числа сохраняются в массиве a
в порядке возрастания, а n
сохраняет свою длину. 2D-массив l[i][j]
определяет длину самой длинной равноотстоящей подпоследовательности, заканчивающуюся a[i]
и a[j]
, и l[j][k]
= l[i][j]
+ 1, если a[j]
- a[i]
= a[k]
- a[j]
( я < j < k).
lmax = 2
l = [[2 for i in xrange(n)] for j in xrange(n)]
for mid in xrange(n - 1):
prev = mid - 1
succ = mid + 1
while (prev >= 0 and succ < n):
if a[prev] + a[succ] < a[mid] * 2:
succ += 1
elif a[prev] + a[succ] > a[mid] * 2:
prev -= 1
else:
l[mid][succ] = l[prev][mid] + 1
lmax = max(lmax, l[mid][succ])
prev -= 1
succ += 1
print lmax
Ответ 4
Алгоритм
- Основной цикл, перемещающийся по списку
- Если номер найден в списке предварительных вычислений, то он принадлежит всем последовательностям, находящимся в этом списке, пересчитывает все последовательности с count + 1
- Удалить все предварительно рассчитанные для текущего элемента
- Пересчитать новые последовательности, где первый элемент находится в диапазоне от 0 до текущего, а второй - текущий элемент обхода (фактически, не от 0 до текущего, мы можем использовать тот факт, что новый элемент не должен быть больше этого max (a), и новый список должен иметь возможность стать дольше, чем уже найдено)
Итак, для списка [1, 2, 4, 5, 7]
вывод будет (это немного грязно, попробуйте сам код и посмотрите)
- индекс 0, элемент 1:
- if
1
в precalc? Нет - ничего не делай
- Ничего не делать
- индекс 1, элемент 2:
- если
2
в precalc? Нет - ничего не делай
- проверить, если 3 =
1
+ (2
- 1
) * 2 в нашем наборе? Нет - ничего не делай
- индекс 2, элемент 4:
- if
4
в precalc? Нет - ничего не делать
- проверьте, если 6=
2
+ (4
- 2
) * 2 в нашем наборе? Нет
- проверьте, если 7=
1
+ (4
- 1
) * 2 в нашем наборе? Да - добавить новый элемент {7: {3: {'count': 2, 'start': 1}}}
7 - элемент списка, 3 - шаг.
- индекс 3, элемент
5
:
- если
5
в prealc? Нет - ничего не делать
- не проверяйте
4
, потому что 6= 4 + (5
- 4
) * 2 меньше, чем вычисляемый элемент 7
- проверьте, если 8=
2
+ (5
- 2
) * 2 в нашем наборе? Нет
- check 10=
2
+ (5
- 1
) * 2 - больше, чем max (a) == 7
- индекс 4, элемент
7
:
- если 7 в precalc? Да - положите его в результат
- не проверяйте
5
, потому что 9= 5 + (7
- 5
) * 2 больше max (a) == 7
result = (3, {'count': 3, 'start': 1}) # step 3, count 3, start 1, превратить его в последовательность
Сложность
Это не должно быть больше O (N ^ 2), и я думаю, что это меньше из-за более раннего прекращения поиска новых секвенций, я попытаюсь позже детально проанализировать
код
def add_precalc(precalc, start, step, count, res, N):
if step == 0: return True
if start + step * res[1]["count"] > N: return False
x = start + step * count
if x > N or x < 0: return False
if precalc[x] is None: return True
if step not in precalc[x]:
precalc[x][step] = {"start":start, "count":count}
return True
def work(a):
precalc = [None] * (max(a) + 1)
for x in a: precalc[x] = {}
N, m = max(a), 0
ind = {x:i for i, x in enumerate(a)}
res = (0, {"start":0, "count":0})
for i, x in enumerate(a):
for el in precalc[x].iteritems():
el[1]["count"] += 1
if el[1]["count"] > res[1]["count"]: res = el
add_precalc(precalc, el[1]["start"], el[0], el[1]["count"], res, N)
t = el[1]["start"] + el[0] * el[1]["count"]
if t in ind and ind[t] > m:
m = ind[t]
precalc[x] = None
for y in a[i - m - 1::-1]:
if not add_precalc(precalc, y, x - y, 2, res, N): break
return [x * res[0] + res[1]["start"] for x in range(res[1]["count"])]
Ответ 5
Вот еще один ответ, работающий со временем O(n^2)
и без каких-либо заметных требований к памяти, помимо того, что переключение списка в набор.
Идея довольно наивная: как и оригинальный плакат, она жадна и просто проверяет, как далеко вы можете расширить подпоследовательность из каждой пары точек, но, сначала проверяя, что мы находимся в начале подпоследовательности. Другими словами, из точек a
и b
вы проверяете, насколько далеко вы можете перейти на b + (b-a)
, b + 2*(b-a)
,... но только если a - (b-a)
еще не находится в наборе всех точек. Если это так, то вы уже видели ту же подпоследовательность.
Хитрость заключается в том, чтобы убедить себя, что этой простой оптимизации достаточно, чтобы снизить сложность до O(n^2)
от оригинала O(n^3)
. Это оставило для читателя упражнение:-) Время здесь конкурентоспособно с другими решениями O(n^2)
.
A = [1, 4, 5, 7, 8, 12] # in sorted order
Aset = set(A)
lmax = 2
for j, b in enumerate(A):
for i in range(j):
a = A[i]
step = b - a
if b + step in Aset and a - step not in Aset:
c = b + step
count = 3
while c + step in Aset:
c += step
count += 1
#print "found %d items in %d .. %d" % (count, a, c)
if count > lmax:
lmax = count
print lmax
Ответ 6
Теперь ваше решение O(N^3)
(вы сказали O(N^2) per index
). Здесь O(N^2)
времени и O(N^2)
решения памяти.
Идея
Если мы знаем подпоследовательность, проходящую через индексы i[0]
, i[1]
, i[2]
, i[3]
, мы не должны пытаться подпоследовательности, которая начинается с i[1]
и i[2]
или i[2]
и i[3]
Примечание. Я отредактировал этот код, чтобы сделать его немного проще с помощью сортировки a
, но он не будет работать для равных элементов. Вы можете проверить число max число равных элементов в O(N)
легко
псевдокод
Я ищу только для максимальной длины, но это ничего не меняет
whereInA = {}
for i in range(n):
whereInA[a[i]] = i; // It doesn't matter which of same elements it points to
boolean usedPairs[n][n];
for i in range(n):
for j in range(i + 1, n):
if usedPair[i][j]:
continue; // do not do anything. It was in one of prev sequences.
usedPair[i][j] = true;
//here quite stupid solution:
diff = a[j] - a[i];
if diff == 0:
continue; // we can't work with that
lastIndex = j
currentLen = 2
while whereInA contains index a[lastIndex] + diff :
nextIndex = whereInA[a[lastIndex] + diff]
usedPair[lastIndex][nextIndex] = true
++currentLen
lastIndex = nextIndex
// you may store all indicies here
maxLen = max(maxLen, currentLen)
Мысли об использовании памяти
O(N^2)
время очень медленное для 1000000 элементов. Но если вы собираетесь запускать этот код на таком количестве элементов, самой большой проблемой будет использование памяти.
Что можно сделать, чтобы уменьшить его?
- Измените булевские массивы на битовые поля, чтобы сохранить больше логических элементов на бит.
- Сделать каждый следующий логический массив короче, потому что мы используем только
usedPairs[i][j]
, если i < j
Немного эвристики:
- Храните только пары используемых указателей. (Конфликты с первой идеей)
- Удалить usedPairs, которые никогда не будут использовать больше (для таких
i
, j
, которые уже были выбраны в цикле)
Ответ 7
Это мои 2 цента.
Если у вас есть список с именем input:
input = [1, 4, 5, 7, 8, 12]
Вы можете построить структуру данных, которая для каждой из этих точек (за исключением первой) сообщит вам, насколько далеко это точка от любого из ее предшественников:
[1, 4, 5, 7, 8, 12]
x 3 4 6 7 11 # distance from point i to point 0
x x 1 3 4 8 # distance from point i to point 1
x x x 2 3 7 # distance from point i to point 2
x x x x 1 5 # distance from point i to point 3
x x x x x 4 # distance from point i to point 4
Теперь, когда у вас есть столбцы, вы можете рассмотреть элемент ввода i-th
(который есть input[i]
) и каждое число n
в его столбце.
Цифрами, которые принадлежат к серии эквидистантных чисел, которые включают input[i]
, являются те, которые имеют n * j
в позиции i-th
своего столбца, где j
- количество совпадений, уже найденных при перемещении столбцов слева направо, плюс предшественник k-th
input[i]
, где k
- индекс n
в столбце input[i]
.
Пример: если мы рассмотрим i = 1
, input[i] = 4
, n = 3
, тогда мы сможем идентифицировать последовательность, постигающую 4
(input[i]
), 7
(поскольку она имеет 3
в позиции 1
его столбца) и 1
, так как k
равно 0, поэтому мы берем первый предшественник i
.
Возможная реализация (извините, если код не использует те же обозначения, что и объяснение):
def build_columns(l):
columns = {}
for x in l[1:]:
col = []
for y in l[:l.index(x)]:
col.append(x - y)
columns[x] = col
return columns
def algo(input, columns):
seqs = []
for index1, number in enumerate(input[1:]):
index1 += 1 #first item was sliced
for index2, distance in enumerate(columns[number]):
seq = []
seq.append(input[index2]) # k-th pred
seq.append(number)
matches = 1
for successor in input[index1 + 1 :]:
column = columns[successor]
if column[index1] == distance * matches:
matches += 1
seq.append(successor)
if (len(seq) > 2):
seqs.append(seq)
return seqs
Самый длинный:
print max(sequences, key=len)
Ответ 8
Перемещение массива, сохранение записи оптимального результата/с и таблицы с
(1) индекс - разность элементов в последовательности,
(2) count - количество элементов в последовательности до сих пор и
(3) последний записанный элемент.
Для каждого элемента массива посмотрите на отличие от каждого предыдущего элемента массива; если этот элемент последний в последовательности, проиндексированной в таблице, отрегулируйте эту последовательность в таблице и обновите лучшую последовательность, если это применимо, в противном случае начните новую последовательность, если текущий макс больше длины возможной последовательности.
Сканирование назад мы можем остановить наше сканирование, когда d больше середины диапазона массивов; или когда максимальный ток больше длины возможной последовательности, для d больше наибольшей индексированной разности. Последовательности, в которых s[j]
больше, чем последний элемент в последовательности, удаляются.
Я преобразовал свой код с JavaScript в Python (мой первый код на Python):
import random
import timeit
import sys
#s = [1,4,5,7,8,12]
#s = [2, 6, 7, 10, 13, 14, 17, 18, 21, 22, 23, 25, 28, 32, 39, 40, 41, 44, 45, 46, 49, 50, 51, 52, 53, 63, 66, 67, 68, 69, 71, 72, 74, 75, 76, 79, 80, 82, 86, 95, 97, 101, 110, 111, 112, 114, 115, 120, 124, 125, 129, 131, 132, 136, 137, 138, 139, 140, 144, 145, 147, 151, 153, 157, 159, 161, 163, 165, 169, 172, 173, 175, 178, 179, 182, 185, 186, 188, 195]
#s = [0, 6, 7, 10, 11, 12, 16, 18, 19]
m = [random.randint(1,40000) for r in xrange(20000)]
s = list(set(m))
s.sort()
lenS = len(s)
halfRange = (s[lenS-1] - s[0]) // 2
while s[lenS-1] - s[lenS-2] > halfRange:
s.pop()
lenS -= 1
halfRange = (s[lenS-1] - s[0]) // 2
while s[1] - s[0] > halfRange:
s.pop(0)
lenS -=1
halfRange = (s[lenS-1] - s[0]) // 2
n = lenS
largest = (s[n-1] - s[0]) // 2
#largest = 1000 #set the maximum size of d searched
maxS = s[n-1]
maxD = 0
maxSeq = 0
hCount = [None]*(largest + 1)
hLast = [None]*(largest + 1)
best = {}
start = timeit.default_timer()
for i in range(1,n):
sys.stdout.write(repr(i)+"\r")
for j in range(i-1,-1,-1):
d = s[i] - s[j]
numLeft = n - i
if d != 0:
maxPossible = (maxS - s[i]) // d + 2
else:
maxPossible = numLeft + 2
ok = numLeft + 2 > maxSeq and maxPossible > maxSeq
if d > largest or (d > maxD and not ok):
break
if hLast[d] != None:
found = False
for k in range (len(hLast[d])-1,-1,-1):
tmpLast = hLast[d][k]
if tmpLast == j:
found = True
hLast[d][k] = i
hCount[d][k] += 1
tmpCount = hCount[d][k]
if tmpCount > maxSeq:
maxSeq = tmpCount
best = {'len': tmpCount, 'd': d, 'last': i}
elif s[tmpLast] < s[j]:
del hLast[d][k]
del hCount[d][k]
if not found and ok:
hLast[d].append(i)
hCount[d].append(2)
elif ok:
if d > maxD:
maxD = d
hLast[d] = [i]
hCount[d] = [2]
end = timeit.default_timer()
seconds = (end - start)
#print (hCount)
#print (hLast)
print(best)
print(seconds)
Ответ 9
Это частный случай для более общей проблемы, описанной здесь: Откройте длинные шаблоны, где K = 1 и исправлено. Там показано, что его можно решить в O (N ^ 2). Запустив мою реализацию алгоритма C, предложенного там, требуется 3 секунды, чтобы найти решение для N = 20000 и M = 28000 на моей 32-битной машине.
Ответ 10
Жадный метод
1. Создается только одна последовательность решений.
2. Создано много решений.
Динамическое программирование
1. Это не гарантирует оптимальное решение.
2. Это определенно дает оптимальное решение.