搜索整数序列


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。因此,简单地尝试通过迭代逐个匹配每个测试序列将变得非常无效率。

很抱歉,如果不清楚。


4
听起来好像您只是想检测子序列(en.wikipedia.org/wiki/Subsequence)。是吗 然后尝试搜索“子序列算法”。
Kilian Foth,

老实说,对我来说,几千个最大长度<= 20的序列听起来并不多。一个简单的暴力方法应该可以解决问题。还是您有成千上万个序列“ A”,每个序列都要针对成千上万个可能的子序列进行测试?
布朗

序列A连续不断,但是它们彼此完全独立。但是,处理延迟会直接延迟所有其他延迟,因此速度很重要。
David Gibson

1
您的字母多大?您是否真的有任意整数,或者值的范围有限,以便我们可以进行一些预计算?
弗兰克(Frank)

整数的可能范围是100,000
David Gibson

Answers:


18

嗯,我可以想到两种可能的算法:通过A序列进行线性扫描,或使用索引的恒定时间查找构建字典。

如果您正在测试许多潜在的亚对一个更大的序列,我建议你使用的变体与字典。

线性扫描

描述

我们为序列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以前已经建立了字符到位置查找表,这与我的第二种解决方案没有什么不同。
阿蒙

更新了更好的算法
2013年

关于该FSM的更多细节,您可以将所有测试序列“编译”为一个FSM,然后遍历整个序列。根据您最终所处的状态,确定要匹配的子序列。当然,宁愿让计算机做任何事情,也不愿手动处理任何不重要的事情。

@MichaelT您是正确的,我们可以通过这种方式构建识别器。但是,我们已经降至n·O(B) + O(f(A))中的初始化成本。建立所有B的类似trie的结构将需要O(n·B)之类的东西,匹配项在O(A)中。从理论上讲,这确实有可能变得更便宜(在第三个解决方案中构建图形可能会很昂贵,但这只是一次性成本)。我认为trie更适合A≫ n·B,并且具有无法处理流输入的缺点-所有B必须在匹配之前加载。我可能会在6小时内更新答案。
阿蒙

6

这是一种实用的方法,它避免了实施自己的算法的“艰苦工作”,也避免了“重新发明轮子”:利用正则表达式引擎解决问题。

只需将A的所有数字放入字符串中,然后将B的所有数字放入由正则表达式分隔的字符串中(.*)^在开头和$结尾处添加一个字符。然后让您喜欢的正则表达式引擎搜索所有匹配项。例如,当

A =(1234356),B =(236)

为B创建一个reg exp ^(.*)2(.*)3(.*)6(.*)$。现在运行全局正则表达式搜索。要找出您的子序列在A中的哪些位置匹配,只需检查前3个子匹配的长度即可。

如果您的整数范围介于0到9之间,则可以考虑先使用单个字母对它们进行编码,以使此功能生效,或者您必须使用分隔字符来适应该想法。

当然,这种方法的速度将在很大程度上取决于您所使用的reg exp引擎的速度,但是有高度优化的引擎可供使用,我想很难“开箱即用”地实现更快的算法。 。


无需一路调用正则表达式及其引擎。可以使用简单的确定性有限自动机来运行它。它是一条“直线”路线。

@MichaelT:好吧,我手头没有“通用有限自动机”库,OP并没有告诉我们他使用的编程语言,但是如今,几乎每种严肃的编程语言都可以使用正则表达式。 ”。这应该使我的建议非常容易实现,并且比amon的解决方案要少得多的代码。恕我直言,OP应该尝试一下,如果这对于他来说太慢,那么他仍然可以尝试使用更复杂的解决方案来更好地为他服务。
布朗

您不需要通用库。您需要的只是“模式”的数组和指向数组中索引的指针。索引指向下一个“寻找”值,当您从源中读取它时,请增加索引。当您击中数组的末尾时,您已经将其匹配。如果您阅读源代码的末尾但未到达末尾,则说明您没有匹配它。

@MichaelT:那为什么不发布该算法的草图作为答案?
Doc Brown

主要是因为它已经得到了更好的回答-“我们为序列A维护一个光标。然后我们遍历子序列B中的所有项目。对于每个项目,我们将光标向A向前移动,直到找到匹配的项目。如果没有,找到了匹配项,那么B不是子序列。”

0

如果获取长度和迭代序列很有效,则此算法应该非常有效。

  1. 比较两个序列的长度。存储时间越长,存储时间sequence越短subsequence
  2. 从两个序列的开头开始,循环直到的结尾sequence
    1. 当前位置的数字是否sequence等于的当前位置的数字subsequence
    2. 如果是,则将两个位置再移一次
    3. 如果没有,只移动的位置sequence一个进一步
  3. subsequence末尾的位置sequence
  4. 如果是,则两个序列匹配
  5. 如果不是,则两个序列不匹配
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.