75

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

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()
  • ZeluX的动态编程方式使用1.6G RAM,耗时2分14秒。使用 pypy 只需 9 秒!但是,它在大输入时因内存错误而崩溃。
  • Armin 的 O(nd) 时间方法使用 pypy 需要 9 秒,但只有 20MB 的 RAM。当然,如果范围更大,情况会更糟。低内存使用意味着我也可以使用 a= [random.randint(0,100000) for r in xrange(200000)] 对其进行测试,但是在我使用 pypy 给它的几分钟内它并没有完成。

为了能够测试Kluev的方法,我重新运行

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

大致列出长度20000。pypy 的所有时间

  • ZeluX,9 秒
  • 克鲁耶夫,20 秒
  • 阿明,52 秒

看来,如果 ZeluX 方法可以成为线性空间,那将是明显的赢家。

4

10 回答 10

19

通过调整您的解决方案,我们可以O(n*m)在内存需求很少的情况下及时找到解决方案。这里n是给定输入数字序列中的项目数,并且m是范围,即最高数字减去最低数字。

将所有输入数字的序列称为 A(并使用预先计算set()的方法在恒定时间内回答“这个数字在 A 中吗?”的问题)。调用 d我们正在寻找的子序列的步骤(这个子序列的两个数字之间的差异)。对于 d 的每个可能值,对所有输入数字执行以下线性扫描:对于从 A 中按升序排列的每个数字 n,如果尚未看到该数字,则在 A 中查找从 n 开始的序列的长度,其中 a步骤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. (使用 CPython 测量 400-500 秒,使用 PyPy 测量 80-90 秒,随机数字子集介于 0 和 10'000'000 之间。)

  • 如果您仍然想搜索整个范围,并且如果常见情况是存在长序列,那么一个显着的改进是一旦 d 太大而无法找到更长的序列时就停止。

于 2013-08-10T10:40:17.370 回答
12

更新:我找到了一篇关于这个问题的论文,你可以在这里下载。

这是一个基于动态规划的解决方案。它需要 O(n^2) 时间复杂度和 O(n^2) 空间复杂度,并且不使用散列。

a我们假设所有数字都按升序保存在数组中,并n保存其长度。二维数组定义了以andl[i][j]结尾的最长等距子序列的长度,如果- = - (i < j < k) ,则= + 1 。a[i]a[j]l[j][k]l[i][j]a[j]a[i]a[k]a[j]

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
于 2013-08-11T16:34:34.897 回答
11

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


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

算法使用以下数据结构:

  1. prev:指向(可能不完整)子序列的前一个元素的索引数组。
  2. hash:带有键的哈希图=子序列中连续对之间的差异,值=另外两个哈希图。对于这些其他哈希图:键 = 子序列的开始/结束索引,值 = 对(子序列长度,子序列的结束/开始索引)。
  3. pqprev: 存储在和中的子序列的所有可能“差异”值的优先级队列hash

算法:

  1. prev用索引初始化i-1。更新hashpq注册在此步骤中找到的所有(不完整)子序列及其“差异”。
  2. 从 .获取(并删除)最小的“差异” pqhash从其中一个二级哈希映射中获取相应记录并扫描。此时,所有具有给定“差异”的子序列都是完整的。如果二级哈希图包含的子序列长度比目前发现的要好,则更新最佳结果。
  3. 在数组prev中:对于在步骤 #2 中找到的任何序列的每个元素,递减 index 和 updatehash并可能pq. 在更新hash时,我们可以执行以下操作之一:添加长度为 1 的新子序列,或将某些现有子序列增加 1,或合并两个现有子序列。
  4. 删除在步骤 #2 中找到的哈希映射记录。
  5. pq当不为空时,从第 2 步继续。

该算法每次更新 O(N) 次 O(N) 个元素prev。并且这些更新中的每一个都可能需要为pq. 如果我们使用简单的堆实现,所有这些都意味着 O(N 2 log N)的时间复杂度pq。为了将其减少到 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
于 2013-08-11T10:08:49.923 回答
3

算法

  • 遍历列表的主循环
  • 如果在 precalculate 列表中找到 number,则它属于该列表中的所有序列,重新计算 count + 1 的所有序列
  • 删除所有为当前元素预先计算的
  • 重新计算新序列,其中第一个元素从 0 到当前,第二个是遍历的当前元素(实际上,不是从 0 到当前,我们可以使用新元素不应该超过 max(a) 和 new列表应该有可能变得比已经找到的更长)

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

  • 索引0,元素1
    • 如果1在预计算中?不 - 什么都不做
    • 没做什么
  • 索引1,元素2
    • 如果2在预计算中?不 - 什么都不做
    • 检查我们的集合中是否有 3 = 1+ ( 2- 1) * 2?不 - 什么都不做
  • 索引2,元素4
    • 如果4在预计算中?不 - 什么都不做
      • 检查我们的集合中是否有6 = 2+ ( 4- 2) * 2?不
      • 检查我们的集合中是否有7 = 1+ ( 4- 1) * 2?是 - 添加新元素{7: {3: {'count': 2, 'start': 1}}} 7 - 列表的元素,3 是步骤。
  • 索引3,元素5
    • 如果5在预计算中?不 - 什么都不做
      • 不要检查4,因为6 = 4 + ( 5- 4) * 2 小于计算的元素7
      • 检查我们的集合中是否有8 = 2+ ( 5- 2) * 2?不
      • 检查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"])]
于 2013-08-10T09:28:52.107 回答
3

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

这个想法非常幼稚:就像最初的海报一样,它是贪婪的,只是检查您可以从每对点扩展子序列多远 --- 但是,首先检查我们是否处于子序列的开头。换句话说,从点开始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
于 2013-08-15T06:25:32.607 回答
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]andi[2]i[2]and开头的子序列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)

关于内存使用的思考

O(n^2)1000000 个元素的时间非常慢。但是,如果您要在如此多的元素上运行此代码,最大的问题将是内存使用。
可以做些什么来减少它?

  • 将布尔数组更改为位域以存储每个位的更多布尔值。
  • 使每个下一个布尔数组更短,因为我们只使用usedPairs[i][j]ifi < j

一些启发式方法:

  • 仅存储使用过的索引对。(与第一个想法冲突)
  • 删除永远不会使用更多的 usedPairs(用于此类ij已在循环中选择)
于 2013-08-10T09:20:42.933 回答
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 * ji-th其列位置的数字,其中j是从左到右移动列时已经找到的匹配数,加上 的k-th前身input[i],其中k的索引n列中input[i]

例子:如果我们考虑i = 1, input[i] = 4, , 那么,我们可以识别一个包含( )n = 3的序列, (因为它在它的列的位置上有一个)和,因为是 0,所以我们取 的第一个前任。4input[i]7311ki

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

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)
于 2013-08-10T21:46:57.090 回答
0

遍历数组,记录最佳结果和一张表,其中包含

(1) 索引 - 序列中的元素差异,
(2) 计数 - 到目前为止序列中的元素数,以及
(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)
于 2013-08-10T15:59:11.237 回答
0

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

于 2013-08-20T15:27:29.413 回答
0

贪心法
1 .只产生一个决策序列。
2. 产生许多决策。动态规划 1. 不保证总是给出最优解。
2.它肯定给出了一个最佳的解决方案。

于 2017-02-25T04:12:23.967 回答