75

ソートされた順序で 100 万の整数があり、連続したペア間の差が等しい最長のサブシーケンスを見つけたいと考えています。例えば

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) time メソッドは pypy で 9 秒かかりましたが、RAM は 20MB しかありませんでした。もちろん、範囲がはるかに大きい場合、これはさらに悪化します。メモリ使用量が少ないということは、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 を使用したすべてのタイミング

  • ZelluX、9秒
  • クルーエフ、20秒
  • アルミン、52秒

ZelluX メソッドを線形空間にすることができれば、明らかに勝者になるようです。

4

10 に答える 10

19

O(n*m)お客様のソリューションを適応させることで、メモリの必要量が非常に少なくても間に合うように解決策を得ることができます。これnは、指定された入力シーケンスの項目の数でありm、範囲、つまり最大数から最小数を差し引いたものです。

すべての入力数値のシーケンスを A と呼びます (そして、事前計算set()された を使用して、「この数値は A にありますか?」という質問に一定時間で答えます)。探しているサブシーケンスのステップ(このサブシーケンスの 2 つの数値の差) をd と呼びます。d のすべての可能な値に対して、すべての入力数値に対して次の線形スキャンを実行します。A から昇順のすべての数値 n について、数値がまだ表示されていない場合は、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その長さが保存されると仮定します。2D 配列は、andでl[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 の 2 番目の回答によって廃止されました。しかし、これらの方法にはどちらも欠点が 1 つあります。100 万個の整数の結果を見つけるには何時間もかかります。そのため、入力整数の範囲が制限されていると想定されるさらに2つのバリアントを試しました(この回答の後半を参照)。このような制限により、はるかに高速なアルゴリズムが可能になります。また、Armin Rigo のコードを最適化しようとしました。最後に私のベンチマーク結果をご覧ください。


これは、O(N) メモリを使用したアルゴリズムのアイデアです。時間計算量は O(N 2 log N) ですが、O(N 2 )に減らすことができます。

アルゴリズムは次のデータ構造を使用します。

  1. prev: (不完全な可能性がある) サブシーケンスの前の要素を指すインデックスの配列。
  2. hash: キーを持つハッシュマップ = サブシーケンス内の連続したペアと値 = 他の 2 つのハッシュマップの差。これらの他のハッシュマップの場合: キー = サブシーケンスの開始/終了インデックス、値 = (サブシーケンスの長さ、サブシーケンスの終了/開始インデックス) のペア。
  3. pqprev:およびに格納されているサブシーケンスのすべての可能な「差分」値の優先キューhash

アルゴリズム:

  1. prevインデックスで初期化しますi-1。このステップで見つかったすべての (不完全な) サブシーケンスとそれらの「相違点」を更新hashして登録します。pq
  2. から最小の「差」を取得 (および削除) しpqます。hash第 2 レベルのハッシュ マップの 1 つから対応するレコードを取得してスキャンします。この時点で、指定された「差分」を持つすべてのサブシーケンスが完了します。第 2 レベルのハッシュ マップに、これまでに見つかったよりも長いサブシーケンス長が含まれている場合は、最良の結果を更新します。
  3. 配列prev内: ステップ 2 で見つかったシーケンスの各要素について、インデックスをデクリメントし、更新hashし、場合によってはpq. の更新中hashに、次の操作のいずれかを実行できます: 長さ 1 の新しいサブシーケンスを追加するか、既存のサブシーケンスを 1 だけ拡張するか、2 つの既存のサブシーケンスをマージします。
  4. ステップ 2 で見つかったハッシュ マップ レコードを削除します。
  5. pqwhileが空でない場合は、手順 2 から続行します。

このアルゴリズムは、prevそれぞれ O(N) 回の O(N) 要素を更新します。また、これらの更新ごとに、新しい「違い」を に追加する必要がある場合がありますpq。に単純なヒープ実装を使用すると、これはすべて O(N 2pq log N) の時間計算量を意味します。それを O(N 2 ) に減らすには、より高度な優先度キューの実装を使用できます。可能性のいくつかは、このページにリストされています: Priority Queues .

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 による最初の方法に似ていますが、動的データ構造を使用していません。ソースデータに重複はないと思います。また、(コードを単純にするために) 最小入力値は負ではなく、ゼロに近いと仮定します。


ブール値の配列の代わりにビットセット データ構造とビット演算を使用してデータを並列処理する場合、以前のアルゴリズムは改善される可能性があります。以下に示すコードは、組み込みの Python 整数として bitset を実装します。同じ仮定があります。重複はなく、最小入力値は負ではなく、ゼロに近いです。時間の複雑さは 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

アルゴリズム

  • リストをたどるメインループ
  • 数値が事前計算リストにある場合、それはそのリストにあるすべてのシーケンスに属し、カウント + 1 ですべてのシーケンスを再計算します
  • 現在の要素に対して事前計算されたものをすべて削除
  • 最初の要素が 0 から現在までの範囲にあり、2 番目の要素がトラバーサルの現在の要素である新しいシーケンスを再計算します (実際には、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?いいえ - 何もしません
      • 6 = 4 + ( - ) * 2 は計算された要素74より小さいため、チェックしないでください54
      • セットで8 = 2+ ( 5- 2) * 2 かどうかを確認しますか? いいえ
      • チェック10 = 2+ ( 5- 1) * 2 - max(a) == 7 以上
  • インデックス4、要素7:
    • 事前計算で7の場合? はい - 結果に入れます
      • 9 = 5 + ( - ) * 2 は max(a) == 7 より大きい 5ため、チェックしないでください75

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 に答える
2

あなたの解決策はO(N^3)今です(あなたは言ったO(N^2) per index)。ここO(N^2)に時間とO(N^2)記憶の解決策があります。

考え

i[0]インデックス, i[1],i[2]を通過するサブシーケンスがわかっている場合、 andまたはandi[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)

メモリ使用量についての考え

O(n^2)1000000 要素の場合、時間は非常に遅くなります。しかし、このコードをそのような数の要素で実行する場合、最大の問題はメモリ使用量です。
それを減らすために何ができるでしょうか?

  • ブール配列をビットフィールドに変更して、ビットごとにより多くのブール値を格納します。
  • usedPairs[i][j]ifのみを使用するため、次の各ブール配列を短くしますi < j

いくつかのヒューリスティック:

  • 使用済みインデックスのペアのみを保存します。(最初のアイデアと矛盾する)
  • これ以上使用されない usedPairs を削除します (つまり、ループで既に選択されているものですi) j
于 2013-08-10T09:20:42.933 に答える
1

これは私の 2 セントです。

入力と呼ばれるリストがある場合:

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-thjk-thinput[i]kninput[i]

i = 1例: , input[i] = 4,を考慮すると、 ( ), (列の位置に があるため) と,が 0 であるため、n = 3を含むシーケンスを識別することができます。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

これは、ここで説明するより一般的な問題の特定のケースです: K=1 で固定されている長いパターンを発見します。O(N^2)で解けることが示されています。そこで提案された C アルゴリズムの実装を実行すると、32 ビット マシンで N=20000 と M=28000 の解を見つけるのに 3 秒かかります。

于 2013-08-20T15:27:29.413 に答える
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 に答える