簡體   English   中英

最長等距子序列

[英]Longest equally-spaced subsequence

我按排序順序有一百萬個整數,我想找到連續對之間差異相等的最長子序列。 例如

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的RAM,需要2分14秒。 使用pypy只需9秒! 但是,它在大輸入時因內存錯誤而崩潰。
  • Armin的O(nd)時間方法用pypy花了9秒,但只有20MB的RAM。 當然,如果范圍更大,情況會更糟。 低內存使用率意味着我也可以用一個= [random.randint(0,100000)來測試它,對於x inrange(200000)中的r,但是在我用pypy給它的幾分鍾內它沒有完成。

為了能夠測試Kluev的方法,我重申了

a= [random.randint(0,40000) for r in xrange(28000)] 
a = list(set(a))
a.sort()

制作一個大約20000的長度列表。 與pypy的所有時間

  • ZelluX,9秒
  • Kluev,20秒
  • 阿明,52秒

似乎如果ZelluX方法可以成為線性空間,它將是明顯的贏家。

我們可以通過適應您的方式,及時提供解決方案O(n*m) ,內存需求非常少。 這里n是給定輸入數字序列中的項目數, m是范圍,即最高數字減去最低數字。

調用A所有輸入數字的序列(並使用預先計算的set()在恆定時間內回答問題“這是A中的這個數字嗎?”)。 調用d我們正在尋找的子序列的步驟 (這個子序列的兩個數字之間的差異)。 對於d的每個可能值,對所有輸入數進行以下線性掃描:對於A中每個數字n的遞增順序,如果尚未看到數字,則在A中查找從n開始的序列長度步驟d。 然后標記已經看到的那個序列中的所有項目,這樣我們就可以避免再次從它們中搜索相同的d。 因此,對於d的每個值,復雜度僅為O(n)

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運行。 (使用CPython在400-500秒測量,使用PyPy測量80-90秒,隨機數字在0到10'000'000之間。)

  • 如果您仍想搜索整個范圍,並且如果常見情況是存在長序列,則只要d太大而無法找到更長的序列,就會停止顯着的改進。

更新:我發現了一個關於這個問題的論文,你可以在這里下載。

這是一個基於動態編程的解決方案。 它需要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] (i <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

更新:這里描述的第一個算法被Armin Rigo的第二個答案淘汰,這個答案更簡單,更有效。 但這兩種方法都有一個缺點。 他們需要很多小時才能找到100萬個整數的結果。 所以我嘗試了另外兩個變體(參見本答案的后半部分),其中輸入整數的范圍被假定為有限。 這種限制允許更快的算法。 我還嘗試優化Armin Rigo的代碼。 最后查看我的基准測試結果。


這是使用O(N)存儲器的算法的概念。 時間復雜度為O(N 2 log N),但可以降低到O(N 2 )。

算法使用以下數據結構:

  1. prev :指向前一個(可能是不完整的)子序列元素的索引數組。
  2. hash :hashmap with key =子序列中連續對之間的差異,value =兩個其他hashmaps。 對於這些其他散列圖:key =子序列的開始/結束索引,value =對的子序列長度,子序列的結束/起始索引。
  3. pq :存儲在prevhash序列的所有可能“差異”值的優先級隊列。

算法:

  1. 使用索引i-1初始化prev 更新hashpq以注冊在此步驟中找到的所有(不完整)子序列及其“差異”。
  2. pq獲取(並刪除)最小的“差異”。 hash獲取相應的記錄並掃描其中一個二級哈希映射。 此時,具有給定“差異”的所有子序列都已完成。 如果第二級哈希映射包含的子序列長度比目前為止找到的更好,則更新最佳結果。
  3. 在數組prev :對於在步驟#2中找到的任何序列的每個元素,遞減索引和更新hash以及可能的pq 在更新hash ,我們可以執行以下操作之一:添加長度為1的新子序列,或者將一些現有子序列增加1,或合並兩個現有子序列。
  4. 刪除在步驟#2中找到的哈希映射記錄。
  5. pq不為空時,從步驟#2繼續。

該算法每次更新prev O(N)次的O(N)個元素。 並且這些更新中的每一個都可能需要向pq添加新的“差異”。 如果我們對pq使用簡單的堆實現,所有這些都意味着O(N 2 log N)的時間復雜度。 要將其減少到O(N 2 ),我們可能會使用更高級的優先級隊列實現。 此頁面列出了一些可能性: 優先級隊列

請參閱Ideone上的相應Python代碼。 此代碼不允許列表中的重復元素。 有可能解決這個問題,但無論如何都要刪除重復項(以及分別找到超出重復項的最長子序列)是一個很好的優化。

經過一點優化后的代碼相同 一旦子序列長度乘以可能的子序列“差異”超過源列表范圍,搜索就會終止。


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的第一種方法,但它不使用任何動態數據結構。 我認為源數據沒有重復。 並且(為了保持代碼簡單)我還假設最小輸入值是非負的並且接近於零。


如果不使用布爾數組,我們使用比特集數據結構和按位運算來並行處理數據,則可以改進先前的算法。 下面顯示的代碼將bitset實現為內置的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

算法

  • 主循環遍歷列表
  • 如果在預先計算列表中找到了數字,那么它屬於該列表中的所有序列,重新計算所有具有count + 1的序列
  • 刪除所有預先計算的當前元素
  • 重新計算新序列,其中第一個元素的范圍從0到當前,第二個是遍歷的當前元素(實際上,不是從0到當前,我們可以使用新元素不應該多於max(a)和new的事實列表應該有可能變得更長已經找到一個)

因此對於列表[1, 2, 4, 5, 7]輸出將是(它有點亂,請自己嘗試代碼並查看)

  • 索引0 ,元素1
    • 如果1在prealc? 不 - 什么都不做
    • 沒做什么
  • 索引1 ,元素2
    • 如果2在prealc? 不 - 什么都不做
    • 檢查我們的套裝中是否3 = 1 +( 2 - 1 )* 2? 不 - 什么都不做
  • 索引2 ,要素4
    • 如果4在prealc? 不 - 什么都不做
      • 檢查我們的套裝中是否有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? 沒有
      • 檢查10 = 2 +( 5 - 1 )* 2 - 超過max(a)== 7
  • 索引4 ,要素7
    • 如果7在prealc? 是的 - 把它歸結為結果
      • 不檢查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"])]

這是另一個答案,工作時間為O(n^2) ,除了將列表轉換為集合之外沒有任何顯着的內存要求。

這個想法很天真:就像原版海報一樣,它很貪婪,只是檢查你可以從每對點延伸一個子序列的距離 - 但是,先檢查我們是否在一個子序列的開頭 換句話說,從a點和b點你可以檢查你可以延伸到b + (ba)b + 2*(ba) ,...但是只有當a - (ba)還沒有在所有的集合中時點。 如果是,那么你已經看到了相同的子序列。

訣竅是說服自己這個簡單的優化足以將復雜度從原始O(n^3)降低到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

你的解決方案現在是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排序的,但它不會為相同的元件工作。 您可以輕松地檢查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)

關於內存使用的思考

對於1000000個元素, O(n^2)時間非常慢。 但是,如果要在這么多元素上運行此代碼,最大的問題將是內存使用情況。
可以做些什么來減少它?

  • 將布爾數組更改為位域以每位存儲更多布爾值。
  • 使每個下一個布爾數組更短,因為如果i < j我們只使用usedPairs[i][j]

幾個啟發式:

  • 僅存儲一對使用過的指標。 (與第一個想法沖突)
  • 刪除永遠不會使用更多的usedPairs(對於已經在循環中選擇的ij

這是我的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]的一系列等距數字的數字是那些在其列i-th位置具有n * j的數字,其中j是從左到右移動列時已經找到的匹配數,加上input[i] k-th前導,其中kinput[i]列中的n的索引。

示例:如果我們考慮i = 1input[i] = 4n = 3 ,那么,我們可以識別一個序列理解4input[i] ), 7 (因為它的列的位置13 )和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)

遍歷數組,記錄最佳結果和表格

(1)index - 序列中的元素差異,
(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)

這是此處描述的更通用問題的特殊情況: 發現 K = 1並且是固定的長模式 在那里證明它可以用O(N ^ 2)求解。 Runnig我在那里提出的C算法的實現需要3秒才能在我的32位機器中找到N = 20000和M = 28000的解決方案。

貪心的方法
1.僅生成一個決策序列。
2.產生了許多決定。 動態編程1.不保證始終提供最佳解決方案。
它肯定會提供最佳解決方案。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM