最长等距子序列

时间:2013-08-10 07:59:47

标签: python algorithm

我按排序顺序有一百万个整数,我想找到连续对之间差异相等的最长子序列。例如

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 in xrange(200000)]但是在我用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方法可以成为线性空间,那将是明显的赢家。

10 个答案:

答案 0 :(得分:19)

我们可以通过适应您的内容,及时提供解决方案O(n*m),只需很少的内存需求。这里n是给定输入数字序列中的项目数,m是范围,即最高数字减去最低数字。

调用所有输入数字的序列(并使用预先计算的set()在常数时间内回答问题“这是A中的这个数字吗?”)。调用我们正在寻找的子序列的(这个子序列的两个数字之间的差异)。对于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太大而无法找到更长的序列,就会停止显着的改进。

答案 1 :(得分:12)

更新:我发现了一篇关于此问题的论文,您可以下载here

这是一个基于动态编程的解决方案。它需要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&lt; j&lt; 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

答案 2 :(得分:11)

更新:此处描述的第一个算法已被Armin Rigo's second answer淘汰,这更简单,更高效。但这两种方法都有一个缺点。他们需要很多小时才能找到100万个整数的结果。所以我尝试了另外两个变体(参见本答案的后半部分),其中输入整数的范围被假定为有限。这种限制允许更快的算法。我还尝试优化Armin Rigo的代码。最后查看我的基准测试结果。


这是使用O(N)存储器的算法的概念。时间复杂度为O(N 2 log N),但可以降低到O(N 2 )。

算法使用以下数据结构:

  1. prev:指向前一个(可能是不完整的)子序列元素的索引数组。
  2. hash:hashmap,其中key =子序列中连续对之间的差异,value =另外两个hashmaps。对于这些其他哈希映射:key =子序列的开始/结束索引,value =对(子序列长度,子序列的结束/起始索引)。
  3. pqprevhash中存储的子序列的所有可能“差异”值的优先级队列。
  4. 算法:

    1. 使用索引prev初始化i-1。更新hashpq以注册此步骤中找到的所有(不完整)子序列及其“差异”。
    2. pq获取(并删除)最小的“差异”。从hash获取相应的记录并扫描其中一个二级哈希映射。此时,具有给定“差异”的所有子序列都已完成。如果第二级哈希映射包含的子序列长度比目前为止找到的更好,则更新最佳结果。
    3. 在数组prev中:对于步骤2中找到的任何序列的每个元素,递减索引并更新hash和可能pq。在更新hash时,我们可以执行以下操作之一:添加长度为1的新子序列,或者将一些现有子序列增加1,或合并两个现有子序列。
    4. 删除步骤2中找到的哈希映射记录。
    5. pq不为空的情况下从第2步继续。
    6. 该算法每次更新prev O(N)次的O(N)个元素。并且每个更新都可能需要向pq添加新的“差异”。如果我们使用pq的简单堆实现,所有这些都意味着O(N 2 log N)的时间复杂度。要将其减少到O(N 2 ),我们可能会使用更高级的优先级队列实现。本页列出了一些可能性:Priority Queues

      请参阅Ideone上的相应Python代码。此代码不允许列表中的重复元素。有可能解决这个问题,但无论如何都要删除重复项(以及分别找到超出重复项的最长子序列)是一个很好的优化。

      the same code after a little optimization。一旦子序列长度乘以可能的子序列“差异”超过源列表范围,搜索就会终止。


      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数据结构和按位运算来并行处理数据,可以改进以前的算法。下面显示的代码将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
      

答案 3 :(得分:3)

<强>算法

  • 遍历列表的主循环
  • 如果在预先计算列表中找到了数字,那么它属于该列表中的所有序列,重新计算所有带有count + 1的序列
  • 删除所有预先计算的当前元素
  • 重新计算新序列,其中第一个元素的范围从0到当前,第二个是遍历的当前元素(实际上,不是从0到当前,我们可以使用新元素不应该超过最大值的事实(a并且新列表应该有可能变得更长,已经找到了一个)

所以对于列表[1, 2, 4, 5, 7]输出会是(它有点乱,请自己尝试代码看看)

  • index 0 ,元素 1
    • 如果{1}}在prealc?不 - 什么都不做
    • 什么都不做
  • index 1 ,元素 2
    • 如果{1}}在prealc?不 - 什么都不做
    • 检查我们的套装中是否有3 = 1 +(2 - 1)* 2?不 - 什么都不做
  • index 2 ,元素 4
    • 如果{1}}在prealc?不 - 什么都不做
      • 检查我们的套装中是否 6 = 2 +(1 - 4)* 2?否
      • 检查我们的设置中是否 7 = 2 +(4 - 2)* 2?是 - 添加新元素1 7 - 列表中的元素,3是步骤。
  • index 3 ,元素4
    • 如果{1}}在prealc?不 - 什么都不做
      • 不检查1,因为 6 = 4 +({7: {3: {'count': 2, 'start': 1}}} - 5)* 2小于计算元素 7
      • 检查我们的设置中是否 8 = 5 +(4 - 5)* 2?没有
      • 检查 10 = 4 +(2 - 5)* 2 - 超过max(a)== 7
  • index 4 ,元素2
    • 如果 7 在prealc?是的 - 把它归结为结果
      • 不检查2因为 9 = 5 +(5 - 1)* 2超过max(a)== 7

result =(3,{'count':3,'start':1})#step 3,count 3,start 1,将其变为序列

<强>复杂性

它不应该超过O(N ^ 2),并且我认为它因为早期终止搜索新序列而减少,我将尝试稍后提供详细分析

<强>代码

7

答案 4 :(得分:3)

这是另一个答案,及时工作O(n^2),除了将列表转换为集合之外没有任何显着的内存要求。

这个想法非常天真:就像原版海报一样,它很贪婪,只是检查你可以从每对点延伸一个子序列的距离 - 但是,先检查我们是否在开始< / em>的子序列。换句话说,从点ab开始,您可以查看可以延伸到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 解决方案竞争。

{{1}}

答案 5 :(得分:2)

您的解决方案现在为O(N^3)(您说O(N^2) per index)。这里是O(N^2)时间和O(N^2)内存解决方案。

如果我们知道经过索引i[0]i[1]i[2]i[3]的子序列,我们就不应该尝试以i[1]和{开头的子序列{1}}或i[2]i[2]

注意我编辑了那段代码,以便使用i[3]排序更容易,但它不适用于相同的元素。您可以轻松检查a中数量相等的最大数量

的伪代码

我只寻求最大长度,但这不会改变任何事情

O(N)

关于内存使用的想法

对于1000000个元素,

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)
  • 时使用usedPairs[i][j]

很少启发式:

  • 仅存储一对使用过的指标。 (与第一个想法冲突)
  • 删除永远不会使用更多的usedPairs(适用于已在循环中选择的i < ji

答案 6 :(得分:1)

这是我的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],其中kn列中input[i]的索引}}

示例:如果我们考虑i = 1input[i] = 4n = 3,那么我们就可以确定一个理解4input[i])的序列,{{1 (因为它的列7位置3)和1,因为1为0,所以我们采用{{1}的第一个前身}。

可能的实施(对不起,如果代码没有使用与解释相同的符号):

k

最长的一个:

i

答案 7 :(得分:0)

遍历数组,记录最佳结果和表格

(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)

答案 8 :(得分:0)

这是此处描述的更通用问题的特殊情况:Discover long patterns其中K = 1并且是固定的。在那里证明它可以用O(N ^ 2)求解。 Runnig我在那里提出的C算法的实现需要3秒才能在我的32位机器中找到N = 20000和M = 28000的解决方案。

答案 9 :(得分:0)

贪婪的方法
1,只生成一个决策序列 2.产生了许多决定。 动态编程 1.它不能保证始终提供最佳解决方案 它肯定会提供最佳解决方案。