如何检查两个排列是否对称?

时间:2016-02-22 15:35:34

标签: python algorithm

鉴于A B LL的两个排列nm是偶数,我们称这些排列为对称"对称" (缺少一个更好的术语),如果存在m > n - A[n:m] == B[L-m:L-n] - B[n:m] == A[L-m:L-n] - all other elements are in place A = 0 1 2 3 4 5 6 7 ,如(用python表示法):

1 2

非正式地,请考虑

5 6

取任意一片,例如B = 0 5 6 3 4 1 2 7 。它从第二个索引开始,长度为2.现在采用对称的切片:它以倒数第二个索引结束,也是2个字符长,所以它是A。交换这些切片给出了

B

现在,n=1, m=3A = 0 1 2 3 4 5 6 7 B = 1 0 2 3 4 5 7 6 是"对称"在上述意义上(n,m)。另一方面

n

不是"对称" (不存在具有上述属性的m)。

如何在python中编写一个算法,找出两个给定的排列(=列表)是否是#34;对称的"如果是,请找到Lset(A)==set(B), len(set(A))==len(A)?为简单起见,我们只考虑偶数L(因为奇数情况可以通过消除中间固定元素而简单地减少到偶数)并假设正确的输入(testing 0123456789 L= 10 test_alexis ok in 15.4252s test_evgeny_kluev_A ok in 30.3875s test_evgeny_kluev_B ok in 27.1382s test_evgeny_kluev_C ok in 14.8131s test_ian ok in 26.8318s test_jared_goguen ok in 10.0999s test_jason_herbburn ok in 21.3870s test_tom_karzes ok in 27.9769s )。

(我毫无疑问地提出了所有可能的对称性,但寻找更智能,更快速的东西)。

有趣的事实:给定div的对称排列数为Triangular number

我使用this code来测试你的答案。

赏金更新:这里有很多优秀的答案。 @Jared Goguen's solution似乎是最快的。

最后时间:

<div style="width:50px; height:100px" id="box" div>

10 个答案:

答案 0 :(得分:5)

以下是该问题的可行解决方案:

def isSymmetric(A, B):
    L = len(A) #assume equivalent to len(B), modifying this would be as simple as checking if len(A) != len(B), return []
    la = L//2 # half-list length
    Al = A[:la]
    Ar = A[la:]
    Bl = B[:la]
    Br = B[la:]
    for i in range(la):
        lai = la - i #just to reduce the number of computation we need to perform
        for j in range(1, lai + 1):
            k = lai - j #same here, reduce computation
            if Al[i] != Br[k] or Ar[k] != Bl[i]: #the key for efficient computation is here: do not proceed unnecessarily
                 continue
            n = i #written only for the sake of clarity. i is n, and we can use i directly
            m = i + j
            if A[n:m] == B[L-m:L-n] and B[n:m] == A[L-m:L-n]: #possibly symmetric
                if A[0:n] == B[0:n] and A[m:L-m] == B[m:L-m] and A[L-n:] == B[L-n:]:
                    return [n, m]
    return []

正如你所提到的,虽然的想法看起来很简单,但它实际上是一个非常棘手的。然而,一旦我们看到模式,实现就是直截了当的。

解决方案的核心思想是这一行:

if Al[i] != Br[k] or Ar[k] != Bl[i]: #the key for efficient computation is here: do not proceed unnecessarily

所有其他行只是问题陈述中的直接代码转换,或者是为了更有效的计算而进行的优化。

找到解决方案涉及的步骤很少:

首先,我们需要将每个列表A和列表B拆分为两个半列表(称为AlArBlBr)。每个半列表将包含原始列表的一半成员:

Al = A[:la]
Ar = A[la:]
Bl = B[:la]
Br = B[la:]

其次,为了使评估高效,此处的目标是找到我称之为枢轴索引来决定是否列表中的位置(索引)值得评估或不检查列表是否对称。此数据透视索引是查找高效解决方案的核心理念。所以我会尝试详细说明一下:

考虑A列表的左半部分,假设您有这样的成员:

Al = [al1, al2, al3, al4, al5, al6]

我们可以想象对于上面提到的列表有一个相应的索引列表

Al  = [al1, al2, al3, al4, al5, al6]
iAl = [0,   1,   2,   3,   4,   5  ] #corresponding index list, added for explanation purpose

(注意:我提到想象一个相应的索引列表的原因是为了便于解释)

同样,我们可以想象其他三个列表可能有类似的索引列表。我们将它们分别命名为iAriBliBr,并且它们都具有iAl的相同成员。

列表的索引对我们来说真正重要 - 为了解决问题。

这就是我的意思:假设我们有两个参数:

  1. 索引(让我们给它一个变量名i,我会使用符号^作为当前i
  2. 长度(让我们给它一个变量名j,我会用符号==直观地表示它的长度值。
  3. 对于iAl索引元素的每个评估 - 然后每个评估意味着:

      

    i中给出索引j和长度值iAl,   确定是否值得检查对称的东西   从索引开始并具有长度的资格   (因此名称支点索引来)。

    现在,让我们以i = 0j = 1为例,举一个评估。评估结果如下:

    iAl = [0, 1, 2, 3, 4, 5]
           ^ <-- now evaluate this index (i) = 0
           == <-- now this has length (j) of 1
    

    为了进一步评估那些索引 i长度 j,那么对应的iBr必须具有与相同 长度相同的项值,但在不同的索引上(让我们将其命名为 index k

    iBr = [0, 1, 2, 3, 4, 5]
                          ^ <-- must compare the value in this index to what is pointed by iAl
                          == <-- must evaluate with the same length = 1
    

    例如,对于上述情况,这是可能的&#34;对称&#34;仅针对两个列表Al-Br的排列(稍后我们将考虑另外两个列表Ar-Bl):

    Al = [0, x, x, x, x, x] #x means don't care for now
    Br = [x, x, x, x, x, 0]
    

    此时,最好注意

      

    即使上述条件不是,也不值得进一步评估   真

    这就是让算法更加高效的地方;也就是说,通过有选择地仅评估所有可能情况中的少数可能的情况。如何找到几个可能的案例?

      

    试图找到索引和长度之间的关系   四个清单。也就是说,对于给定的索引 i长度 j   列表(例如Al),对应中的索引 k必须是什么   列表(在这种情况下是Br)。对应列表的长度不需要   找到它,因为它与列表中的相同(即j)。

    知道了,现在让我们继续看看我们是否可以在评估过程中看到更多模式。

    现在考虑长度(j)的影响。例如,如果我们要从索引 0进行评估,但长度为2,则对应列表需要具有不同的索引 { {1}}评估的时间长度为k

    1

    或者,如上图所示,真正重要的是狐狸iAl = [0, 1, 2, 3, 4, 5] ^ <-- now evaluate this index (i) = 0 ===== <-- now this has length (j) of 2 iBr = [0, 1, 2, 3, 4, 5] ^ <-- must compare the value in this index to what is pointed by iAl ===== <-- must evaluate with the same length = 2 i = 0是这样的:

    y = 2

    看一看上面的模式与# when i = 0 and y = 2 Al = [0, y, x, x, x, x] #x means don't care for now Br = [x, x, x, x, 0, y] #y means to be checked later i = 0时的情况略有不同 - 示例中y = 1 的索引位置已移位:

    0

    因此,长度会移动,其中必须检查对应列表的索引。在第一种情况下,当# when i = 0 and y = 1, k = 5 Al = [0, x, x, x, x, x] #x means don't care for now Br = [x, x, x, x, x, 0] # when i = 0 and y = 2, k = 4 Al = [0, y, x, x, x, x] #x means don't care for now Br = [x, x, x, x, 0, y] #y means to be checked later i = 0时,y = 1。但在第二种情况下,当k = 5i = 0时,y = 1。因此,当我们为固定的索引 k = 4更改长度 j时,我们找到了支点索引关系案例为i)到对应列表索引 0

    现在,考虑具有固定长度 k索引 i对对应列表索引 {{的影响1}}。例如,我们将长度修复为j,然后对于索引 k,我们有:

    y = 4

    在上面的示例中,可以看出我们需要为给定的i = 0iAl = [0, 1, 2, 3, 4, 5] ^ <-- now evaluate this index (i) = 0 ========== <-- now this has length (j) of 4 iAl = [0, 1, 2, 3, 4, 5] ^ <-- now evaluate this index (i) = 1 ========== <-- now this has length (j) of 4 iAl = [0, 1, 2, 3, 4, 5] ^ <-- now evaluate this index (i) = 2 ========== <-- now this has length (j) of 4 #And no more needed 评估 3 的可能性,但是如果索引 i更改为j,长度为i

    1

    请注意,我们只需要评估 2 的可能性。因此,索引 j = 4的增加会减少要评估的可能案例的数量!

    通过上述所有模式,我们几乎找到了使算法运行所需的所有基础。但要完成此操作,我们需要找到iAl = [0, 1, 2, 3, 4, 5] ^ <-- now evaluate this index (i) = 1 ========== <-- now this has length (j) of 4 iAl = [0, 1, 2, 3, 4, 5] ^ <-- now evaluate this index (i) = 2 ========== <-- now this has length (j) of 4 对{strong} {<1}}中显示的索引与{{中的索引之间的关系1}}对同一i

    现在,我们实际上可以看到它们只是镜像我们在Al-Br对中找到的关系!

    (恕我直言,这真的很美!所以我认为术语&#34;对称&#34;排列与真相并不远)

    例如,如果我们使用[i, j] => [k, j]Ar-Bl

    评估了以下[i, j]
    Al-Br

    然后,要使其对称,我们必须拥有相应的Al-Br

    i = 0

    y = 2对的索引 镜像(或对称)Al = [0, y, x, x, x, x] #x means don't care for now Br = [x, x, x, x, 0, y] #y means to be checked later 对的索引!

    因此,结合我们上面找到的所有模式,我们现在可以找到数据透视索引来评估Ar-BlAr = [x, x, x, x, 3, y] #x means don't care for now Bl = [3, y, x, x, x, x] #y means to be checked later Al-Br和{{ 1}}。

      

    我们只需要检查枢轴索引中列表的值   第一。如果Ar-BlAlArBl数据透视索引中的列表值   我们需要检查评估 中的匹配然后才   对于对称标准(从而使计算有效!)

    将上述所有知识放入代码中,以下是检查对称性的Br Python代码:

    Al

    然后你会进行对称测试!

    (好吧,这是一个相当大的挑战,我需要花一点时间来弄清楚如何。)

答案 1 :(得分:4)

我重写了代码而没有一些复杂性(和错误)。

def test_o_o(a, b):

    L = len(a)
    H = L//2
    n, m = 0, H-1

    # find the first difference in the left-side
    while n < H:
        if a[n] != b[n]: break
        n += 1
    else: return

    # find the last difference in the left-side
    while m > -1:
        if a[m] != b[m]: break 
        m -= 1
    else: return

    # for slicing, we want end_index+1
    m += 1

    # compare each slice for equality
    # order: beginning, block 1, block 2, middle, end
    if (a[0:n] == b[0:n] and \
        a[n:m] == b[L-m:L-n] and \
        b[n:m] == a[L-m:L-n] and \
        a[m:L-m] == b[m:L-m] and \
        a[L-n:L] == b[L-n:L]):

        return n, m

实施既优雅又高效。

breakelse: return结构确保函数尽可能快地返回。他们还验证nm已设置为有效值,但在显式检查切片时似乎不需要这样做。可以移除这些线,而不会对时间产生明显影响。

一旦评估为False,显式比较切片也会短路。

最初,我通过将b转换为a来检查排列是否存在:

b = b[:]
b[n:m], b[L-m:L-n] = b[L-m:L-n], b[n:m]
if a == b:
   return n, m

但这比明确比较切片要慢。让我知道这个算法是否能说不出话来,我可以提供进一步的解释(甚至可以证明)它为何起作用并且是最小的。

答案 2 :(得分:4)

我尝试为此任务实现3种不同的算法。它们都具有O(N)时间复杂度并且需要O(1)额外空间。有趣的事实:所有其他答案(到目前为止已知)实现了这些算法中的2个(尽管它们并不总是保持最佳的渐近时间/空间复杂度)。以下是每种算法的高级描述:

算法A

  1. 比较列表,组&#34;不等于&#34;间隔,确保恰好有两个这样的间隔(特殊情况,当间隔在中间相遇)。
  2. 检查&#34;不平等&#34;间隔是对称放置的,它们的内容也是对称的&#34;。
  3. 算法B

    1. 比较列表的前半部分以猜测&#34;要交换的间隔&#34;。
    2. 检查这些间隔的内容是否对称&#34;。并确保列表在这些间隔之外是相等的。
    3. 算法C

      1. 比较列表的前半部分以找到第一个不匹配的元素。
      2. 在第二个列表中找到第一个列表中不匹配的元素。这暗示了&#34;要交换的间隔的位置&#34;。
      3. 检查这些间隔的内容是否对称&#34;。并确保列表在这些间隔之外是相等的。
      4. 每种算法的步骤1有两种替代实现:(1)使用itertools,(2)使用普通循环(或列表推导)。 itertools对于长列表有效,但在短列表上相对较慢。

        这是算法C,第一步使用itertools实现。它看起来比其他两种算法更简单(在本文末尾)。它很快,即使是短名单:

        import itertools as it
        import operator as op
        
        def test_C(a, b):
            length = len(a)
            half = length // 2
            mismatches = it.imap(op.ne, a, b[:half]) # compare half-lists
        
            try:
                n = next(it.compress(it.count(), mismatches))
                nr = length - n
                mr = a.index(b[n], half, nr)
                m = length - mr
            except StopIteration: return None
            except ValueError: return None
        
            if a[n:m] == b[mr:nr] and b[n:m] == a[mr:nr] \
                    and a[m:mr] == b[m:mr] and a[nr:] == b[nr:]:
                return (n, m)
        

        这可以使用大多数itertools来完成:

        def test_A(a, b):
            equals = it.imap(op.eq, a, b) # compare lists
            e1, e2 = it.tee(equals)
            l = it.chain(e1, [True])
            r = it.chain([True], e2)
            borders = it.imap(op.ne, l, r) # delimit equal/non-equal intervals
            ranges = list(it.islice(it.compress(it.count(), borders), 5))
        
            if len(ranges) == 4:
                n1, m1 = ranges[0], ranges[1]
                n2, m2 = ranges[2], ranges[3]
            elif len(ranges) == 2:
                n1, m1 = ranges[0], len(a) // 2
                n2, m2 = len(a) // 2, ranges[1]
            else:
                return None
        
            if n1 == len(a) - m2 and m1 == len(a) - n2 \
                    and a[n1:m1] == b[n2:m2] and b[n1:m1] == a[n2:m2]:
                return (n1, m1)
        

        @j_random_hacker在OP注释中已经提供了该算法的高级描述。以下是一些细节:

        从比较列表开始:

        A  0 1 2 3 4 5 6 7
        B  0 5 6 3 4 1 2 7
        =  E N N E E N N E
        

        然后找到相等/不等间隔之间的边界:

        =  E N N E E N N E
        B  _ * _ * _ * _ *
        

        然后确定不相等元素的范围:

        B  _ * _ * _ * _ *
            [1 : 3] [5 : 7]
        

        然后检查是否确实有2个范围(当两个范围在中间相遇时有特殊情况),范围本身是对称的,它们的内容也是。

        其他替代方法是使用itertools处理每个列表的一半。这允许稍微简单(并且稍微更快)的算法,因为不需要处理特殊情况:

        def test_B(a, b):
            equals = it.imap(op.eq, a, b[:len(a) // 2]) # compare half-lists
            e1, e2 = it.tee(equals)
            l = it.chain(e1, [True])
            r = it.chain([True], e2)
            borders = it.imap(op.ne, l, r) # delimit equal/non-equal intervals
            ranges = list(it.islice(it.compress(it.count(), borders), 2))
        
            if len(ranges) != 2:
                return None
        
            n, m = ranges[0], ranges[1]
            nr, mr = len(a) - n, len(a) - m
        
            if a[n:m] == b[mr:nr] and b[n:m] == a[mr:nr] \
                    and a[m:mr] == b[m:mr] and a[nr:] == b[nr:]:
                return (n, m)
        

答案 3 :(得分:3)

我构建了列表B中字符所在的地图,然后使用它来确定列表A中隐含的子范围。一旦我有了子范围,我就可以理智地检查一些信息,并比较切片。

如果A[i] == x,则x出现在B中的哪个位置?拨打该职位p

我知道i,左侧子范围的开始

我知道L (= len(A)),所以我知道L-i,右侧子范围的结束

如果我知道p,那么我知道右子范围的暗示开始,假设B [p]和A [i]是对称范围对的开始。因此,如果列表是对称的,OP的L - m将为p

设置L-m == p会给我m,所以我有四个终点。

完整性测试是:

  • n和m在列表的左半部分
  • n&lt; = m(注意:OP没有禁止n == m)
  • L-n位于列表的右半部分(已计算)
  • L-m在右半边(这是一个快速失败的好检查)

如果所有这些都结账,请比较A [左] == B [右]和B [左] == A [右]。如果为真,请返回left

def find_symmetry(a:list, b:list) -> slice or None:

    assert len(a) == len(b)
    assert set(a) == set(b)
    assert len(set(a)) == len(a)

    length = len(a)
    assert length % 2 == 0

    half = length // 2
    b_loc = {bi:n for n,bi in enumerate(b)}

    for n,ai in enumerate(a[:half]):
        L_n = length - 1 - n    # L - n
        L_m = b_loc[ai]         # L - m (speculative)

        if L_m < half:         # Sanity: bail if on wrong side
            continue

        m = b_loc[a[L_n]]  # If A[n] starts range, A[m] ends it.

        if m < n or m > half:   # Sanity: bail if backwards or wrong side
            continue

        left = slice(n, m+1)
        right = slice(L_m, L_n+1)

        if a[left] == b[right] and \
            b[left] == a[right]:
            return left

    return None

res = find_symmetry(
        [ 10, 11, 12, 13, 14, 15, 16, 17, ],
        [ 10, 15, 16, 13, 14, 11, 12, 17, ])
assert res == slice(1,3)

res = find_symmetry(
    [ 0, 1, 2, 3, 4, 5, 6, 7, ],
    [ 1, 0, 2, 3, 4, 5, 7, 6, ])
assert res is None

res = find_symmetry("abcdefghijklmn", "nbcdefghijklma")
assert res == slice(0,1)

res = find_symmetry("abcdefghijklmn", "abjklfghicdmen")
assert res == slice(3,4)

res = find_symmetry("abcdefghijklmn", "ancjkfghidelmb")
assert res == slice(3,5)

res = find_symmetry("abcdefghijklmn", "bcdefgaijklmnh")
assert res is None

res = find_symmetry("012345", "013245")
assert res == slice(2,3)

答案 4 :(得分:3)

这是正确的事情:

Br = B[L//2:]+B[:L//2]
same_full = [a==b for (a,b) in zip(A, Br)]
same_part = [a+b for (a,b) in zip(same_full[L//2:], same_full[:L//2])]

for n, vn in enumerate(same_part):
    if vn != 2:
        continue
    m = n
    for vm in same_part[n+1:]:
        if vm != 2:
            break
        m+=1

    if m>n:
        print("n=", n, "m=", m+1)

我很确定你可以做一些更好的计数,但是...... meh

答案 5 :(得分:3)

我相信以下伪代码应该有效:

  1. 找到i的第一个元素A[i] != B[i],设置n = i。如果没有这样的元素,则返回success。如果n >= L/2,请返回fail
  2. 找到i > n的第一个元素A[i] == B[i],设置m = i。如果没有此类元素或m > L/2,请设置m = L/2
  3. 检查A[0:n] == B[0:n]A[n:m] == B[L-m:L-n]B[n:m] == A[L-m:L-n]A[m:L-m] == B[m:L-m]A[L-n:L] == B[L-n:L]。如果一切正常,请返回success。否则,请返回fail
  4. 复杂性是O(n),它应该是最低的,因为总是需要比较列表中的所有元素。

答案 6 :(得分:3)

这是一个简单的解决方案,通过我和我的测试:

  1. 比较输入,寻找不匹配的子序列。
  2. 根据规则转换不匹配的子序列来转换A。结果是否与B匹配?
  3. 算法为O(N);没有嵌入式循环,显式或隐式。

    在步骤1中,我需要检测交换的子串是否相邻的情况。这只能发生在字符串的中间,但我发现更容易只注意移动的部分的第一个元素(firstval)。与显式检查所有约束相比,第2步更简单(因此更不容易出错)。

    def compare(A, B):
        same = True
        for i, (a, b) in enumerate(zip(A,B)):
            if same and a != b:  # Found the start of a presumed transposition
                same = False
                n = i
                firstval = a  # First element of the transposed piece
            elif (not same) and (a == b or b == firstval):  # end of the transposition
                m = i
                break
    
        # Construct the transposed string, compare it to B
        origin = A[n:m] 
        if n == 0:  # swap begins at the edge
            dest = A[-m:]
            B_expect = dest + A[m:-m] + origin
        else:
            dest = A[-m:-n]
            B_expect = A[:n] + dest + A[m:-m] + origin + A[-n:]
    
        return bool(B_expect == B)
    

    样品使用:

    >>> compare("01234567", "45670123")
    True
    

    加分:我相信这段关系的名称将是“对称块换位”。 块换位交换两个子序列,取{ {1}}到ABCDE。 (见定义4 here;我实际上通过谷歌搜索“ADCBE”找到了这个。我在名称中添加了“对称”来描述长度条件。

答案 7 :(得分:3)

制作索引列表(ds),其中两个列表的前半部分不同。 可能的n是第一个这样的索引,最后一个这样的索引是m - 1。 检查是否有效对称。 len(ds)== m - n确保没有任何差距。

2016-02-26-03-43/

答案 8 :(得分:3)

这是一个传递测试代码的O(N)解决方案:

def sym_check(a, b):
    cnt = len(a)

    ml = [a[i] == b[i] for i in range(cnt)]

    sl = [i for i in range(cnt) if (i == 0 or ml[i-1]) and not ml[i]]
    el = [i+1 for i in range(cnt) if not ml[i] and (i == cnt-1 or ml[i+1])]

    assert(len(sl) == len(el))

    range_cnt = len(sl)

    if range_cnt == 1:
        start1 = sl[0]
        end2 = el[0]

        if (end2 - start1) % 2 != 0:
            return None

        end1 = (start1 + end2) // 2
        start2 = end1

    elif range_cnt == 2:

        start1, start2 = sl
        end1, end2 = el

    else:
        return None

    if end1 - start1 != end2 - start2:
        return None

    if start1 != cnt - end2:
        return None

    if a[start1:end1] != b[start2:end2]:
        return None

    if b[start1:end1] != a[start2:end2]:
        return None

    return start1, end1

我只用Python 2测试过它,但我相信它也适用于Python 3。

它标识两个列表不同的范围。它寻找两个这样的范围(如果只有一个这样的范围,它会尝试将其除以一半)。然后检查两个范围是否相同,并且相对于彼此处于适当的位置。如果是,则检查范围中的元素是否匹配。

答案 9 :(得分:3)

又一个版本:

def compare(a, b):
    i_zip = list(enumerate(zip(a, b)))
    llen = len(a)
    hp = llen // 2

    def find_index(i_zip):
        for i, (x, y) in i_zip:
            if x != y:
                return i
        return i_zip[0][0]

    # n and m are determined by the unmoved items:
    n = find_index(i_zip[:hp])
    p = find_index(i_zip[hp:])
    m = llen - p
    q = llen - n
    # Symmetric?
    if a[:n] + a[p:q] + a[m:p] + a[n:m] + a[q:] != b:
        return None
    return n, m

此解决方案基于:

  1. 所有符合对称性要求的有效置换列表对A,B将具有以下结构:

    A = P1 + P2 + P3 + P4 + P5 B = P1 + P4 + P3 + P2 + P5 ^n ^m ^hp ^p ^q <- indexes

    ,len(P1)== len(P5)和len(P2)== len(P4)

  2. 因此,如果正确确定了索引nm,则上述函数的最后3行将确定正确的解。 (p&amp; q只是m&amp; n的镜像索引。
  3. 查找n是确定A和B的项目何时开始分歧的问题。接下来,相同的方法应用于从中点p开始查找hpm只是p的镜像索引。找到所有涉及的索引并出现解决方案。