查找未排序和排序列表之间的最小距离

时间:2014-01-30 09:38:10

标签: javascript python algorithm language-agnostic

设A是一个列表,S是相同元素的排序列表。假设所有元素都不同。如何找到将A变为S的最小“移动”(move X before Y (or end))?

示例:

A = [8,1,2,3]
S = [1,2,3,8]

A => S requires one move: 
   move 8 before end

A = [9,1,2,3,0]
S = [0,1,2,3,9]

A => S requires two moves:
   move 9 before 0
   move 0 before 1

我更喜欢javascript或python,但任何语言都可以。

4 个答案:

答案 0 :(得分:12)

此问题相当于longest increasing subsequence问题。

您必须定义比较运算符less。当且仅当less(a, b)在目标序列中true之前时,a才会返回b。现在使用此比较运算符,计算源序列的最大增加子序列。您将必须移动不属于此子序列的每个元素(否则子序列将不是最大值),您可以将其移动一次(将其移动到其目标位置)。

编辑:根据amit的要求,这是我对上述声明的证明: 让我们表示目标序列B,并让我们表示源序列A。如上所述,设n = |A|并让k为最长增长序列的长度。

  • 我们假设有可能从BA,移动次数少于n - k。这意味着n - k + 1中的至少A个元素不会被移动。设s 1 ,s 2 ,... s m 是未移动的元素集。从我们的假设我们知道m > k。现在由于这些元素没有移动,它们相对于彼此的相对位置不能改变。因此,目标序列B中所有这些元素的相对位置与A中的相对位置相同。因此,对于任何ij,如上定义的运算符less(s i ,s j )应该为真。但如果这是真的那么s 1 ,s 2 ,... s m 正在增加序列,而m > k这导致与k是最长增长序列的长度的假设相矛盾。
  • 现在让我们通过移动所有元素来显示一个从B到达A的算法,但这些元素是最长增长序列的一部分。我们将按照它们在B中出现的顺序移动元素。我们不会移动属于最长增长序列的元素。如果当前元素是B中的第一个元素,我们只需将其移动到序列的开头即可。否则,我们将当前元素向移动到之后的前一个元素的位置。注意,此元素可以是我们移动的前一个元素,也可以是最长增长序列中的元素。请注意,在我们即将移动索引为i的元素的每个步骤中,索引为1, 2, ...i-1的所有元素已经具有相对于彼此的正确相对位置。

编辑:添加一些代码以使答案更清晰。我不觉得自己是javascript的专家,所以请随意纠正或批评我的解决方案。

让我们定义一个函数transform(a, s),它接受​​两个参数 - 如声明中所述列出a和b。首先,我将创建一个地图positions,将a中的每个元素映射到s中的位置:

var positions = {};
for (var i = 0; i < a.length; ++i) {
  positions[a[i]] = i;
}

现在我有了这个数组,我可以定义一个辅助函数,如上面的答案所述。 Less将采用两个值ab(以及我刚刚创建的辅助贴图),并且当且仅当a位于b s之前时才返回true (目标清单):

function less(a, b, positions) {
  return positions[a] < positions[b];
}

现在我不会描述如何在a中找到与该比较运算符相关的最大增加子序列。您可以查看this question以获取有关如何执行此操作的详细说明。我将简单地假设我定义了一个函数:

function max_increasing_subsequence(a, positions)

相对于如上定义的比较运算符a(使用less)作为列表,返回positions中的最大增加子序列。我将使用你的第二个例子来说明我们到目前为止所做的事情:

A = [9,1,2,3,0]
S = [0,1,2,3,9]

职位的价值如下:

positions = { 0 : 0,
              1 : 1,
              2 : 2,
              3 : 3,
              9 : 4}

max_increasing_subsequence(a, positions)的结果为[1, 2, 3]。顺便说一下,如果a中可能存在重复元素,那么返回索引而不是max_increasing_subsequence中的元素可能会更好(在此特定示例中,差异将不可见)。

现在我将创建另一个帮助器映射,以指示最大增加子序列中包含哪些元素:

var included = {};
l = max_increasing_subsequence(a, positions);
for (var i = 0; i < l.length; ++i) {
  included[l[i]] = true;
}

现在,您可以通过s上的单次迭代来完成解决方案。我将为最后一个元素添加一个特殊情况,以使代码更容易理解:

if (!(s[s.length - 1] in included)) {
  console.log("Move" + s[s.length - 1] + " at the end");
}
for (var i = s.length - 2; i >= 0; --i) {
  if (!(s[i] in included)) {
    console.log("Move" + s[i] + " before " + s[i + 1]);
  }
}

请注意,在上面的解决方案中,我假设您每次登录一个新命令时,都会在执行完所有先前命令后立即根据数组a的顺序进行记录。

总而言之,我认为转换看起来应该是这样的:

function transform(a, s) {
  var positions = {};
  for (var i = 0; i < a.length; ++i) {
    positions[a[i]] = i;
  }
  var included = {};
  l = max_increasing_subsequence(a, positions);
  var included = {};
  for (var i = 0; i < l.length; ++i) {
    included[l[i]] = true;
  }
  if (!(s[s.length - 1] in included)) {
    console.log("Move" + s[s.length - 1] + " at the end");
  }
  for (var i = s.length - 2; i >= 0; --i) { // note s.length - 2 - don't process last element
    if (!(s[i] in included)) {
      console.log("Move" + s[i] + " before " + s[i + 1]);
    }
  }
}

我希望这段代码能让我的答案更加清晰。

答案 1 :(得分:5)

如果您将两个列表视为两个字符串 - 例如数字是ASCII编码中的值 - 然后问题等同于找到允许您将第一个字符串转换为第二个字符串的操作。反过来,操作的数量是Levenshtein或字符串之间的编辑距离。

Levenshtein distance可以找到using dynamic programming,在矩阵中存储两个字符串的所有前缀之间的距离,然后追溯您的步骤以找到矩阵的每一行,这是最佳的操作(需要最少操作才能到达它的操作)。

@IvayloStrandjev建议的最长的增加子序列算法与最长的常见子序列问题有关,后者又与编辑距离有关,作为仅允许插入和替换的替代度量。可能它在空间中更具性能,因为它利用了其中一个序列必须进行排序的事实;我只是想提供一个我更容易理解的替代答案。

以下是Python中完整矩阵Levenshtein算法的实现,如上面链接的维基百科页面中所述(最初在1974 paper by Wagner and Fischer中找到),其中还提供了proof of correctness。在这里,我们还将操作的名称存储在与操作得分相同大小的矩阵中,并在完成一行后打印最佳操作。

import argparse

import numpy as np


class Levenshtein(object):
    def __init__(self, string1, string2):
        self.string1 = string1
        self.string2 = string2
        self.scores_matrix = np.zeros(
            (len(self.string1) + 1, len(self.string2) + 1), dtype=np.int16)
        self.operations_matrix = np.empty_like(
            self.scores_matrix, dtype=(np.str_, 16))
        self.total_steps = 0

    def distance(self):
        m = len(self.string1) + 1
        n = len(self.string2) + 1
        for i in range(m):
            self.scores_matrix[i, 0] = i
        for j in range(n):
            self.scores_matrix[0, j] = j
        for j in range(1, n):
            for i in range(1, m):
                if self.string1[i - 1] == self.string2[j - 1]:
                    self.scores_matrix[i, j] = self.scores_matrix[i - 1, j - 1]
                    self.operations_matrix[i, j] = 'match'
                else:
                    self.scores_matrix[i, j] = self.select_operation(i, j)
                if j == n - 1:  # a row is complete
                    self.determine_best_op_and_print(i)
        return self.scores_matrix[m - 1, n - 1]

    def select_operation(self, i, j):
        possible_ops = ['delete', 'insert', 'substitute']
        ops_scores = [
            self.scores_matrix[i - 1, j] + 1,  # deletion
            self.scores_matrix[i, j - 1] + 1,  # insertion
            self.scores_matrix[i - 1, j - 1] + 1]  # substitution
        chosen_op = min(ops_scores)
        chosen_op_name = possible_ops[ops_scores.index(chosen_op)]
        self.operations_matrix[i, j] = chosen_op_name
        return chosen_op

    def determine_best_op_and_print(self, i):
        reversed_row = self.scores_matrix[i][::-1]
        reversed_pos_min = np.argmin(reversed_row)
        pos_min = len(self.scores_matrix[i]) - (reversed_pos_min + 1)
        best_op_name = self.operations_matrix[i, pos_min]
        if best_op_name != 'match':
            self.total_steps += 1
            print best_op_name, self.string1[i - 1], self.string2[pos_min - 1]


def parse_cli():
    parser = argparse.ArgumentParser()
    parser.add_argument('--list', nargs='*', required=True)
    return parser.parse_args()

if __name__ == '__main__':
    args = parse_cli()
    A = args.list
    S = sorted(A)
    lev = Levenshtein(A, S)
    dist = lev.distance()
    print "{} total steps were needed; edit distance is {}".format(
        lev.total_steps, dist)

以下是如何使用您提供的示例以及预期的输出运行代码:

$ python levenshtein.py --list 8 1 2 3
substitute 8 1
1 total steps were needed; edit distance is 2

$ python levenshtein.py --list 9 1 2 3 0
substitute 9 0
substitute 0 9
2 total steps were needed; edit distance is 2

答案 2 :(得分:1)

这很大程度上取决于未说明问题的一些参数。首先,什么举动是合法的?邻接元素只交换?任意删除和插入?其次,你只需要移动的数量,还是需要一个特定的移动列表?这导致了不同的算法:

  1. 仅限邻居互换 - 如果您只关心最小数字,则称为反转计数。
  2. 删除,非相邻互换等 - 前面提到的Levenshtein距离是更一般的编辑距离。关于这一点的一个技巧是如何定义移动集。是将一个元素移动一次移动一个元素还是两个移动(删除和插入)?
  3. 反转计数非常简单,可以使用一些基本的递归算法来完成。您可以使用合并排序来查找两个列表之间的反转计数,方法是使用一个列表来生成另一个列表的转换版本,其中新元素是索引。因此,如果你有两个序列,你可以这样做:

    sequence = [seq2.index(element) for element in seq]
    

    用于计算反转的简单直接Python合并排序实现是:

    if len(sequence) <= 1:
        return 0, sequence
    else:
        firstHalf = sequence[:int(len(sequence)/2)]
        secondHalf = sequence[int(len(sequence)/2):]
        count1, firstHalf = mergeSortInversionCount(firstHalf)
        count2, secondHalf = mergeSortInversionCount(secondHalf)
        firstN = len(firstHalf)
        secondN = len(secondHalf)
        secondHalfEnd = secondN
        count3 = count1 + count2
        # Count the inversions in the merge
        # Uses a countdown through each sublist
        for i in xrange(firstN-1, -1, -1):
            x = firstHalf[i]
            inversionFound = False
            for j in xrange(secondHalfEnd-1,-1,-1):
                if x > secondHalf[j]:
                    inversionFound = True
                    break
            if inversionFound:
                secondHalfEnd = j+1
                count3 += j+1
        mergeList = firstHalf + secondHalf
        mergeList.sort()
        return count3, mergeList
    

    这只是将列表分成两半并计算反转,对列表进行排序。从算法来讲,合并排序是非常有效的(NlogN,虽然实际上你可以用一些numpy矩阵更快地计算它,或者通过开发一个对C代码的次要适应用于底层Python排序算法。从技术上讲,这种方法可以转换任何将变量类型转换为数字,它基本上简化为列表排序方法,因此只要您跟踪计数,就可以使用其他按元素排序的排序来执行相同的操作。

    使用这些方法中的任何一种(反演计数,Levenstein等),您都可以清楚地记录这些动作。倒置计数记录交换,logc指出了为Levenstein记录一些更普遍的移动的合理方法。就个人而言,我倾向于使用反转计数,因为它们相当简单。但这在很大程度上取决于你想要什么。如果你需要比双元素邻居互换更多的操作,Levenstein是一个明智的选择。

答案 3 :(得分:0)

执行Cycle Sort并计算移动次数。这保证是最低数量。