排序字符串以匹配第二个字符串的最快方法 - 仅允许相邻的交换

时间:2017-12-01 19:33:07

标签: python algorithm sorting

我想获得转换一个字符串以匹配第二个字符串所需的最小字母交换次数。只允许相邻的掉期。

输入为:字符串长度,string_1,string_2

一些例子:

Length | String 1 | String 2 | Output
-------+----------+----------+-------
   3   | ABC      | BCA      |   2 
   7   | AABCDDD  | DDDBCAA  |  16
   7   | ZZZAAAA  | ZAAZAAZ  |   6

这是我的代码:

def letters(number, word_1, word_2):

    result = 0

    while word_1 != word_2:
        index_of_letter = word_1.find(word_2[0])
        result += index_of_letter
        word_1 = word_1.replace(word_2[0], '', 1)
        word_2 = word_2[1:]

    return result

它给出了正确的结果,但计算应保持在20秒以下。

以下是两组输入数据(1 000 000个字符长字符串):https://ufile.io/8hp46https://ufile.io/athxu

在我的设置中,第一个在大约40秒内执行,第二个在4分钟内执行。

如何在不到20秒的时间内计算结果?

3 个答案:

答案 0 :(得分:5)

@ KennyOstrom在那里是90%。反演计数确实是看待这个问题的直角。

唯一缺少的是我们需要一个“相对”反转计数,这意味着反转的数量不是达到正常的排序顺序而是达到另一个单词的顺序。因此,我们需要计算将word1稳定映射到word2(或反过来)的置换,然后计算其反转计数。稳定性在这里很重要,因为很明显会有很多非独特的字母。

这是一个numpy实现,对于您发布的两个大型示例,只需要一两秒钟。我没有广泛测试它,但它确实同意@trincot在所有测试用例上的解决方案。对于两个大对,它会找到1819136406480769230766

import numpy as np

_, word1, word2 = open("lit10b.in").read().split()
word1 = np.frombuffer(word1.encode('utf8')
                      + (((1<<len(word1).bit_length()) - len(word1))*b'Z'),
                      dtype=np.uint8)
word2 = np.frombuffer(word2.encode('utf8')
                      + (((1<<len(word2).bit_length()) - len(word2))*b'Z'),
                      dtype=np.uint8)
n = len(word1)

o1 = np.argsort(word1, kind='mergesort')
o2 = np.argsort(word2, kind='mergesort')
o1inv = np.empty_like(o1)
o1inv[o1] = np.arange(n)

order = o2[o1inv]

sum_ = 0
for i in range(1, len(word1).bit_length()):
    order = np.reshape(order, (-1, 1<<i))
    oo = np.argsort(order, axis = -1, kind='mergesort')
    ioo = np.empty_like(oo)
    ioo[np.arange(order.shape[0])[:, None], oo] = np.arange(1<<i)
    order[...] = order[np.arange(order.shape[0])[:, None], oo]
    hw = 1<<(i-1)
    sum_ += ioo[:, :hw].sum() - order.shape[0] * (hw-1)*hw // 2

print(sum_)

答案 1 :(得分:3)

您的算法在 O(n 2 时间内运行:

  • find()来电将 O(n)时间
  • replace()调用将创建一个完整的新字符串,其中 O(n)时间
  • 外部循环执行 O(n)

正如其他人所说,这可以通过使用合并排序计算反转来解决,但在这个答案中我尝试保持接近你的算法,保持外部循环和result += index_of_letter,但改变方式{{1计算。

改进可以按如下方式进行:

  • 预处理index_of_letter字符串,并在这些字母键入的字典中注明word_1中每个不同字母的第一个位置。将每个字母与下一个字母相关联。我认为为此创建一个列表是最有效的,其大小为word_1,其中每个索引都存储下一个相同字母的索引。这样,每个不同的字母都有一个链表。此预处理可以在 O(n)时间内完成,使用它可以用{em> O(1)查找替换word_1调用。每次执行此操作时,都会从链接列表中删除匹配的字母,即dict中的索引将移动到下一个匹配项的索引。
  • 之前的更改将提供绝对索引,而不考虑您在算法中删除的字母,因此这会产生错误的结果。要解决这个问题,您可以构建一个二叉树(也是预处理),其中每个节点代表find中的索引,并给出给定索引之前的未删除字母的实际数量(如果不是,则包括其自身)已删除)。二叉树中的节点永远不会被删除(这可能是变体解决方案的一个想法),但计数会被调整以反映字符的删除。最多 O(logn)节点需要在删除时获得递减值。但除此之外,没有像word_1那样重建字符串。该二叉树可以表示为列表,对应于有序序列中的节点。列表中的值将是该节点之前的未删除字母数(包括其自身)。

初始二叉树可以描述如下:

enter image description here

节点中的数字反映了左侧的节点数,包括它们自己。它们存储在replace列表中。另一个列表numLeft预先计算父母所在的索引。

实际代码可能如下所示:

parent

这在 O(nlogn)中运行,其中 logn 因子由二叉树中的向上步行提供。

我测试了数千个随机输入,上面的代码在所有情况下都会产生与代码相同的结果。但是......它在更大的输入上运行得更快。

答案 2 :(得分:1)

我假设您只是想快速找到掉期数,而不需要知道交换的确切内容。

谷歌如何计算倒数。它经常通过合并排序来教授。其中一些结果是堆栈溢出,如Merge sort to count split inversions in Python

反转是要到达已排序字符串的相邻互换的数量。 计算字符串1中的反转。 计算字符串2中的反转。

此处修改了错误,请参阅正确答案中的更正。我通常会删除一个错误的答案,但这个答案会在正确的答案中引用。

这很有道理,它适用于所有三个小测试用例,所以我只是假设这是你想要的答案。

使用一些我偶然会在免费在线课程中重新学习一些算法类的代码(为了好玩):

print (week1.count_inversions('ABC'), week1.count_inversions('BCA'))
print (week1.count_inversions('AABCDDD'), week1.count_inversions('DDDBCAA'))
print (week1.count_inversions('ZZZAAAA'), week1.count_inversions('ZAAZAAZ'))
  

0 2
  4 20
  21 15

与上面给出的值对齐:2,16和6。