整数シーケンスの検索


14

かなり複雑な検索の問題があり、それをなんとか次の説明にまとめました。私はグーグル検索を行ってきましたが、問題にきれいに適合すると思われるアルゴリズムを見つけることができませんでした。特に、任意の整数をスキップする必要があります。たぶんここの誰かが私に何かを指し示すことができますか?

整数Aのシーケンスを例にとります(1(2 3 4))

さまざまな整数のシーケンスを取り、それらのいずれかがそのようなAに一致するかどうかをテストします。

  1. Aには、テストされたシーケンス内のすべての整数が含まれます
  2. テストされたシーケンスの整数の順序はAで同じです
  3. テストシーケンスにないAの整数は気にしません
  4. 最初のものだけでなく、一致するすべてのテストシーケンスが必要です。

A = (1 2 3 4)
B = (1 3)
C = (1 3 4)
D = (3 1)
E = (1 2 5)

BはAと一致します

CはAと一致します

順序が異なるため、DはAと一致しません

EはAにない整数を含むため、Aとは一致しません

この説明が十分に明確であることを願っています。私ができた最善の方法は、テストシーケンスのツリーを構築し、Aを反復処理することです。整数をスキップできるようにする必要があると、多くの失敗した検索パスにつながります。

ありがとう

いくつかの提案を読んで、私はあまりにも曖昧にしたいくつかの点を明確にしなければならないと感じています。

  1. 繰り返し数が許可されます。実際、これは単一のテストシーケンスがAに一致することを許可するため、非常に重要です。

    A =(1234356)、B =(236)、一致は-23 --- 6または-2--3-6のいずれかです

  2. 少なくとも数千の非常に多数のテストシーケンスがあると予想され、シーケンスAの最大長は20になる可能性があります。したがって、反復することで各テストシーケンスを1つずつ一致させることは非常に非効率的です。

これが明確でない場合は申し訳ありません。


4
単にサブシーケンス(en.wikipedia.org/wiki/Subsequence)を検出したいように聞こえます。それですか?次に、「サブシーケンスアルゴリズム」を検索してみます。
キリアンフォス

正直なところ、最大長<= 20の数千のシーケンスは、私には大きな数には聞こえません。簡単なブルートフォースアプローチでこのトリックを行う必要があります。または、数千の可能なサブシーケンスに対してテストするために、それぞれが数千のシーケンス「A」を持っていますか?
Doc Brown

シーケンスAの連続ストリームがありますが、それらは互いに完全に独立しています。ただし、処理の遅延は他のすべてを直接遅延させるため、速度が重要です。
デビッドギブソン

1
アルファベットの大きさは?あなたは本当に任意の整数を持っていますか、またはいくつかの事前計算を行うことができるような値の有限範囲がありますか?
フランク

整数の可能な範囲は100,000sである
デヴィッド・ギブソン

回答:


18

うーん、私は2つの可能なアルゴリズムを考えることができます:Aシーケンスを通る線形スキャン、またはインデックスの一定時間のルックアップで辞書を構築します。

単一のより大きなシーケンスAに対して多数の潜在的なサブシーケンスBをテストする場合、辞書でバリアントを使用することをお勧めします。

リニアスキャン

説明

シーケンスAのカーソルを維持します。次に、サブシーケンスBのすべてのアイテムを反復処理します。アイテムごとに、一致するアイテムが見つかるまでカーソルをAで前方に移動します。一致するアイテムが見つからなかった場合、Bはサブシーケンスではありません。

これは常にO(seq.size)で実行されます。

擬似コード

命令型:

def subsequence? seq, subseq:
  i = 0
  for item in subseq:
    i++ while i < seq.size and item != seq[i]
    return false if i == seq.size
  return true

機能スタイル:

let rec subsequence? = function
| _ [] -> true
| [] _ -> false
| cursor::seq item::subseq ->
  if   cursor = item
  then subsequence? seq subseq
  else subsequence? seq item::subseq

実装例(Perl):

use strict; use warnings; use signatures; use Test::More;

sub is_subsequence_i ($seq, $subseq) {
  my $i = 0;
  for my $item (@$subseq) {
    $i++ while $i < @$seq and $item != $seq->[$i];
    return 0 if $i == @$seq;
  }
  return 1;
}

sub is_subsequence_f ($seq, $subseq) {
  return 1 if @$subseq == 0;
  return 0 if @$seq == 0;
  my ($cursor, @seq) = @$seq;
  my ($item, @subseq) = @$subseq;
  return is_subsequence_f(\@seq, $cursor == $item ? \@subseq : $subseq);
}

my $A = [1, 2, 3, 4];
my $B = [1, 3];
my $C = [1, 3, 4];
my $D = [3, 1];
my $E = [1, 2, 5];

for my $is_subsequence (\&is_subsequence_i, \&is_subsequence_f) {
  ok $is_subsequence->($A, $B), 'B in A';
  ok $is_subsequence->($A, $C), 'C in A';
  ok ! $is_subsequence->($A, $D), 'D not in A';
  ok ! $is_subsequence->($A, $E), 'E not in A';
  ok $is_subsequence->([1, 2, 3, 4, 3, 5, 6], [2, 3, 6]), 'multiple nums';
}

done_testing;

辞書検索

説明

シーケンスAのアイテムをインデックスにマッピングします。次に、Bの各アイテムに適したインデックスを検索し、小さいインデックスをスキップして、可能な限り小さいインデックスを下限として選択します。インデックスが見つからない場合、Bはサブシーケンスではありません。

O(subseq.size・k)のようなもので実行されます。ここで、kはに重複する数値がいくつあるかを示しseqます。さらに、O(seq.size)のオーバーヘッド

このソリューションの利点は、ルックアップテーブルを構築するためのオーバーヘッドを支払うと、ネガティブな決定にはるかに速く(一定時間まで)到達できることです。

擬似コード:

命令型:

# preparing the lookup table
dict = {}
for i, x in seq:
  if exists dict[x]:
    dict[x].append(i)
  else:
    dict[x] = [i]

def subsequence? subseq:
  min_index = -1
  for x in subseq:
    if indices = dict[x]:
      suitable_indices = indices.filter(_ > min_index)
      return false if suitable_indices.empty?
      min_index = suitable_indices[0]
    else:
      return false
  return true

機能スタイル:

let subsequence? subseq =
  let rec subseq-loop = function
  | [] _ -> true
  | x::subseq min-index ->
    match (map (filter (_ > min-index)) data[x])
    | None -> false
    | Some([]) -> false
    | Some(new-min::_) -> subseq-loop subseq new-min
  in
    subseq-loop subseq -1

実装例(Perl):

use strict; use warnings; use signatures; use Test::More;

sub build_dict ($seq) {
  my %dict;
  while (my ($i, $x) = each @$seq) {
    push @{ $dict{$x} }, $i;
  }
  return \%dict;
}

sub is_subsequence_i ($seq, $subseq) {
  my $min_index = -1;
  my $dict = build_dict($seq);
  for my $x (@$subseq) {
    my $indices = $dict->{$x} or return 0;
    ($min_index) = grep { $_ > $min_index } @$indices or return 0;
  }
  return 1;
}

sub is_subsequence_f ($seq, $subseq) {
  my $dict = build_dict($seq);
  use feature 'current_sub';
  return sub ($subseq, $min_index) {
    return 1 if @$subseq == 0;
    my ($x, @subseq) = @$subseq;
    my ($new_min) = grep { $_ > $min_index } @{ $dict->{$x} // [] } or return 0;
    __SUB__->(\@subseq, $new_min);
  }->($subseq, -1);
}

my $A = [1, 2, 3, 4];
my $B = [1, 3];
my $C = [1, 3, 4];
my $D = [3, 1];
my $E = [1, 2, 5];

for my $is_subsequence (\&is_subsequence_i, \&is_subsequence_f) {
  ok $is_subsequence->($A, $B), 'B in A';
  ok $is_subsequence->($A, $C), 'C in A';
  ok ! $is_subsequence->($A, $D), 'D not in A';
  ok ! $is_subsequence->($A, $E), 'E not in A';
  ok $is_subsequence->([1, 2, 3, 4, 3, 5, 6], [2, 3, 6]), 'multiple nums';
}

done_testing;

辞書検索バリアント:有限状態マシンとしてのエンコード

説明

より多くのメモリを取り引きすれば、アルゴリズムの複雑さをO(subseq.size)までさらに減らすことができます。要素をインデックスにマッピングする代わりに、各ノードがそのインデックスで要素を表すグラフを作成します。エッジは可能な遷移を示しa, b, aますa@1 → b@2, a@1 → a@3, b@2 → a@3。たとえば、シーケンスにはエッジがあります。このグラフは、有限状態マシンに相当します。

ルックアップ中に、最初はツリーの最初のノードであるカーソルを維持します。次に、サブリストBの各要素のエッジを調べます。そのようなエッジが存在しない場合、Bはサブリストではありません。すべての要素の後にカーソルに有効なノードが含まれている場合、Bはサブリストです。

擬似コード

命令型:

# preparing the graph
graph = {}
for x in seq.reverse:
  next_graph = graph.clone
  next_graph[x] = graph
  graph = next_graph

def subseq? subseq:
  cursor = graph
  for x in subseq:
    cursor = graph[x]
    return false if graph == null
  return true

機能スタイル:

let subseq? subseq =
  let rec subseq-loop = function
  | [] _ -> true
  | x::subseq graph -> match (graph[x])
    | None -> false
    | Some(next-graph) -> subseq-loop subseq next-graph
  in
    subseq-loop subseq graph

実装例(Perl):

use strict; use warnings; use signatures; use Test::More;

sub build_graph ($seq) {
  my $graph = {};
  for (reverse @$seq) {
    $graph = { %$graph, $_ => $graph };
  }
  return $graph;
}

sub is_subsequence_i ($seq, $subseq) {
  my $cursor = build_graph($seq);
  for my $x (@$subseq) {
    $cursor = $cursor->{$x} or return 0;
  }
  return 1;
}

sub is_subsequence_f ($seq, $subseq) {
  my $graph = build_graph($seq);
  use feature 'current_sub';
  return sub ($subseq, $graph) {
    return 1 if @$subseq == 0;
    my ($x, @subseq) = @$subseq;
    my $next_graph = $graph->{$x} or return 0;
    __SUB__->(\@subseq, $next_graph);
  }->($subseq, $graph);
}

my $A = [1, 2, 3, 4];
my $B = [1, 3];
my $C = [1, 3, 4];
my $D = [3, 1];
my $E = [1, 2, 5];

for my $is_subsequence (\&is_subsequence_i, \&is_subsequence_f) {
  ok $is_subsequence->($A, $B), 'B in A';
  ok $is_subsequence->($A, $C), 'C in A';
  ok ! $is_subsequence->($A, $D), 'D not in A';
  ok ! $is_subsequence->($A, $E), 'E not in A';
  ok $is_subsequence->([1, 2, 3, 4, 3, 5, 6], [2, 3, 6]), 'multiple nums';
}

done_testing;

余談ですが、どのようにstudy機能するのか、それが適用されるアルゴリズムがここで実用的な用途を持っているかどうかを突っ込んでいますか?

1
@MichaelTわかりません…私は学部生ですが、実際に勉強する方法をまだ発見していません</ joke>。Perl組み込み関数について話している場合:最近は何もしていません。現在の実装は、わずか数十行の後方互換性です。正規表現エンジンは、可変サイズのパターンに一致する前に定数文字列を検索するなど、このようなヒューリスティックを直接使用します。study私の2番目のソリューションとは異なり、以前に文字と位置のルックアップテーブルを構築していた。
アモン

さらに優れたアルゴリズムを使用して更新
アモン

そのFSMについてさらに詳しく説明すると、すべてのテストシーケンスを1つのFSMに「コンパイル」してから、シーケンス全体を実行できます。最後にどの状態にあったかに応じて、どのサブシーケンスが一致したかが決まります。それは確かに、どんな些細なものでも手でやるのではなく、コンピューターにやらせたいことです。

@MichaelTこのようにしてレコグナイザーを構築できることは確かです。ただし、すでにn・O(B) + O(f(A))の初期化コストに達しています。すべてのBのトライのような構造を構築するには、O(A)に一致するO(n・B)のようなものが必要です。これは理論的にはより安価になる可能性があります(3番目のソリューションでグラフを作成するのは高価になる可能性がありますが、それは1回限りのコストです)。トライはA≫ n・Bにより適していて、ストリーミング入力を処理できないという欠点があります。すべてのBはマッチングの前にロードする必要があります。おそらく6時間以内に回答を更新します。
アモン

6

独自のアルゴリズムを実装する「ハードワーク」を回避し、「車輪の再発明」を回避する実用的なアプローチを次に示します。問題に正規表現エンジンを使用します。

Aのすべての数字を文字列に入れ、Bのすべての数字を正規表現で区切られた文字列に入れるだけ(.*)です。^最初と$最後に文字を追加します。次に、お気に入りの正規表現エンジンですべての一致を検索します。たとえば、

A =(1234356)、B =(236)

のようなBの正規表現を作成します^(.*)2(.*)3(.*)6(.*)$。次に、グローバル正規表現検索を実行します。Aのどの位置でサブシーケンスが一致するかを調べるには、最初の3つのサブ一致の長さをチェックします。

整数の範囲が0〜9のままの場合、この作業を行うために最初に1文字でエンコードすることを検討するか、分離文字を使用してアイデアを適応させる必要があります。

もちろん、そのアプローチの速度は、使用している正規表現エンジンの速度に大きく依存しますが、高度に最適化されたエンジンが利用可能であり、「箱から出して」より速いアルゴリズムを実装するのは難しいと思います。


正規表現とそのエンジンを呼び出すためにずっと行く必要はありません。単純な決定性有限オートマトンを使用して実行することが可能です。その「直線」ルート。

@MichaelT:「ジェネリック有限オートマトン」ライブラリは手元になく、OPは彼が使用するプログラミング言語については教えてくれませんでしたが、今日ではほとんどすべての本格的なプログラミング言語で正規表現が利用できます「。これにより、たとえばamonのソリューションよりもはるかに少ないコードで、私の提案を非常に簡単に実装できるはずです。私見OPはそれを試してみる必要があります、それが彼にとって遅すぎる場合、彼はより複雑な解決策が彼により良く役立つかどうかまだ試すことができます。
ドックブラウン

汎用ライブラリは必要ありません。必要なのは、「パターン」の配列と、配列内のインデックスへのポインタだけです。インデックスは次の「探している」値を指しており、ソースから値を読み取るときにインデックスをインクリメントします。配列の最後に到達すると、一致しました。ソースの末尾を最後まで読まずに読んだ場合、一致していません。

@MichaelT:では、答えとしてそのアルゴリズムのスケッチを投稿してみませんか?
ドックブラウン

主に、すでにより適切に回答されているためです-「シーケンスAのカーソルを維持します。次に、サブシーケンスBのすべてのアイテムを反復処理します。アイテムごとに、一致するアイテムが見つかるまでAでカーソルを前方に移動します。一致するアイテムが見つかった場合、Bはサブシーケンスではありません。」

0

このアルゴリズムは、長さを取得してシーケンスを反復することが効率的である場合、非常に効率的です。

  1. 両方のシーケンスの長さを比較します。長い方sequenceと短い方を入れてsubsequence
  2. 両方のシーケンスの先頭から開始し、の終わりまでループしますsequence
    1. 現在の位置のsequence数は、現在の位置の数と等しいsubsequence
    2. はいの場合、両方の位置をもう1つ移動します
    3. そうでない場合は、sequenceさらに1つの位置だけを移動します
  3. subsequenceの終わりの位置ですsequence
  4. はいの場合、2つのシーケンスは一致します
  5. そうでない場合、2つのシーケンスは一致しません
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.