我按排序顺序有一百万个整数,我想找到连续对之间差异相等的最长子序列。例如
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()
为了能够测试Kluev的方法,我重新开始
a= [random.randint(0,40000) for r in xrange(28000)]
a = list(set(a))
a.sort()
制作一个大约20000
的长度列表。所有与pypy的时间
似乎如果ZelluX方法可以成为线性空间,那将是明显的赢家。
答案 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 )。
算法使用以下数据结构:
prev
:指向前一个(可能是不完整的)子序列元素的索引数组。hash
:hashmap,其中key =子序列中连续对之间的差异,value =另外两个hashmaps。对于这些其他哈希映射:key =子序列的开始/结束索引,value =对(子序列长度,子序列的结束/起始索引)。pq
:prev
和hash
中存储的子序列的所有可能“差异”值的优先级队列。算法:
prev
初始化i-1
。更新hash
和pq
以注册此步骤中找到的所有(不完整)子序列及其“差异”。pq
获取(并删除)最小的“差异”。从hash
获取相应的记录并扫描其中一个二级哈希映射。此时,具有给定“差异”的所有子序列都已完成。如果第二级哈希映射包含的子序列长度比目前为止找到的更好,则更新最佳结果。prev
中:对于步骤2中找到的任何序列的每个元素,递减索引并更新hash
和可能pq
。在更新hash
时,我们可以执行以下操作之一:添加长度为1的新子序列,或者将一些现有子序列增加1,或合并两个现有子序列。pq
不为空的情况下从第2步继续。该算法每次更新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)
<强>算法强>
所以对于列表[1, 2, 4, 5, 7]
输出会是(它有点乱,请自己尝试代码看看)
1
+(2
- 1
)* 2?不 - 什么都不做2
+(1
- 4
)* 2?否2
+(4
- 2
)* 2?是 - 添加新元素1
7 - 列表中的元素,3是步骤。 4
:
1
,因为 6 = 4 +({7: {3: {'count': 2, 'start': 1}}}
- 5
)* 2小于计算元素 7 5
+(4
- 5
)* 2?没有4
+(2
- 5
)* 2 - 超过max(a)== 7 2
:
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>的子序列。换句话说,从点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
解决方案竞争。
{{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)
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]
很少启发式:
i < j
,i
)答案 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]
,其中k
是n
列中input[i]
的索引}}
示例:如果我们考虑i = 1
,input[i] = 4
,n = 3
,那么我们就可以确定一个理解4
(input[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.它不能保证始终提供最佳解决方案
它肯定会提供最佳解决方案。