检测序列是否是Python中子序列的倍数

时间:2013-03-11 06:02:26

标签: python algorithm sequence

我有一个零和一元组,例如:

(1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1)

事实证明:

(1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1) == (1, 0, 1, 1) * 3

我想要一个函数f,如果s是一个零和一的非空元组,f(s)是最短的子rs == r * n对于某个正整数n

例如,

f( (1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1) ) == (1, 0, 1, 1)

在Python中编写函数f的简单方法是什么?

编辑:

我目前使用的天真方法

def f(s):
  for i in range(1,len(s)):
    if len(s)%i == 0 and s == s[:i] * (len(s)/i):
      return s[:i]

7 个答案:

答案 0 :(得分:5)

我相信我有一个O(n)时间解决方案(实际上是2n + r,n是元组的长度,r是子tuplle),它不使用后缀树,但使用字符串匹配算法(如KMP,你应该找到现成的。)

我们使用以下鲜为人知的定理:

If x,y are strings over some alphabet,

then xy = yx if and only if x = z^k and y = z^l for some string z and integers k,l.

我现在声称,出于我们问题的目的,这意味着我们需要做的就是确定给定的元组/列表(或字符串)是否是自身的循环移位!

要确定字符串是否是自身的循环移位,我们将它与自身连接起来(它甚至不必是真正的连接,只是虚拟连接)并检查子串匹配(与其自身)。

为了证明这一点,假设字符串是自身的循环移位。

我们有给定的字符串y = uv = vu。 由于uv = vu,我们必须从上述定理得到u = z ^ k和v = z ^ 1,因此y = z ^ {k + 1}。另一个方向很容易证明。

这是python代码。该方法称为powercheck。

def powercheck(lst):
    count = 0
    position = 0
    for pos in KnuthMorrisPratt(double(lst), lst):
        count += 1
        position = pos
        if count == 2:
            break

    return lst[:position]


def double(lst):
    for i in range(1,3):
        for elem in lst:
            yield elem

def main():
    print powercheck([1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1])

if __name__ == "__main__":
    main()

这是我使用的KMP代码(由David Eppstein提供)。

# Knuth-Morris-Pratt string matching
# David Eppstein, UC Irvine, 1 Mar 2002

def KnuthMorrisPratt(text, pattern):

    '''Yields all starting positions of copies of the pattern in the text.
Calling conventions are similar to string.find, but its arguments can be
lists or iterators, not just strings, it returns all matches, not just
the first one, and it does not need the whole text in memory at once.
Whenever it yields, it will have read the text exactly up to and including
the match that caused the yield.'''

    # allow indexing into pattern and protect against change during yield
    pattern = list(pattern)

    # build table of shift amounts
    shifts = [1] * (len(pattern) + 1)
    shift = 1
    for pos in range(len(pattern)):
        while shift <= pos and pattern[pos] != pattern[pos-shift]:
            shift += shifts[pos-shift]
        shifts[pos+1] = shift

    # do the actual search
    startPos = 0
    matchLen = 0
    for c in text:
        while matchLen == len(pattern) or \
              matchLen >= 0 and pattern[matchLen] != c:
            startPos += shifts[matchLen]
            matchLen -= shifts[matchLen]
        matchLen += 1
        if matchLen == len(pattern):
            yield startPos

对于您的样本,此输出

[1,0,1,1]

正如所料。

我将此与shx2的代码(不是numpy代码)进行比较,生成随机50位字符串,然后复制以使总长度为100万。这是输出(十进制数是time.time()的输出)

1362988461.75

(50, [1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1])

1362988465.96

50 [1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1]

1362988487.14

上述方法耗时约4秒,而shx2的方法耗时约21秒!

这是时间码。 (shx2的方法叫做powercheck2)。

def rand_bitstring(n):
    rand = random.SystemRandom()
    lst = []
    for j in range(1, n+1):
        r = rand.randint(1,2)
        if r == 2:
            lst.append(0)
        else:
            lst.append(1)

    return lst

def main():
    lst = rand_bitstring(50)*200000
    print time.time()
    print powercheck(lst)
    print time.time()
    powercheck2(lst)
    print time.time()

答案 1 :(得分:4)

以下解决方案是O(N ^ 2),但其优点是不会创建数据的任何副本(或切片),因为它基于迭代器。

根据输入的大小,您避免复制数据的事实可能会导致显着的加速,但当然,对于大量输入而言,它不会像复杂性较低的算法那样扩展(例如O (N * logn)时间)。

[这是我的解决方案的第二次修订,第一次修订如下。这个更容易理解,更多的是沿着OP的元组乘法,只使用迭代器。]

from itertools import izip, chain, tee

def iter_eq(seq1, seq2):
    """ assumes the sequences have the same len """
    return all( v1 == v2 for v1, v2 in izip(seq1, seq2) )

def dup_seq(seq, n):
    """ returns an iterator which is seq chained to itself n times """
    return chain(*tee(seq, n))

def is_reps(arr, slice_size):
    if len(arr) % slice_size != 0:
        return False
    num_slices = len(arr) / slice_size
    return iter_eq(arr, dup_seq(arr[:slice_size], num_slices))

s = (1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1)
for i in range(1,len(s)):
    if is_reps(s, i):
        print i, s[:i]
        break

[我原来的解决方案]

from itertools import islice

def is_reps(arr, num_slices):
    if len(arr) % num_slices != 0:
        return False
    slice_size = len(arr) / num_slices
    for i in xrange(slice_size):
        if len(set( islice(arr, i, None, num_slices) )) > 1:
            return False
    return True

s = (1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1)
for i in range(1,len(s)):
    if is_reps(s, i):
        print i, s[:i]
        break

您可以使用以下内容来避免对set()的调用:

def is_iter_unique(seq):
    """ a faster version of testing len(set(seq)) <= 1 """
    seen = set()
    for x in seq:
        seen.add(x)
        if len(seen) > 1:
            return False
    return True

并替换此行:

if len(set( islice(arr, i, None, num_slices) )) > 1:

使用:

if not is_iter_unique(islice(arr, i, None, num_slices)):

答案 2 :(得分:3)

简化Knoothe的解决方案。他的算法是正确的,但他的实现过于复杂。这个实现也是O(n)。

由于你的数组只由1和0组成,我所做的是使用现有的str.find实现(Bayer Moore)来实现Knoothe的想法。它在运行时更加简单,速度更快。

def f(s):
    s2 = ''.join(map(str, s))
    return s[:(s2+s2).index(s2, 1)]

答案 3 :(得分:1)

这是另一个解决方案(与我之前基于迭代器的解决方案竞争),利用numpy。

它确实制作了你的数据的(单个)副本,但是利用你的值为0和1的事实,这是非常快的,这要归功于numpy的魔法。

import numpy as np

def is_reps(arr, slice_size):
    if len(arr) % slice_size != 0:
        return False
    arr = arr.reshape((-1, slice_size))
    return (arr.all(axis=0) | (~arr).all(axis=0)).all()

s = (1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1) * 1000
a = np.array(s, dtype=bool)
for i in range(1,len(s)):
    if is_reps(a, i):
        print i, s[:i]
        break

答案 4 :(得分:0)

只是解决问题的另一种方法

我首先确定长度的所有因素,然后拆分列表并检查所有部分是否相同

>>> def f(s):
    def factors(n):
        #http://stackoverflow.com/a/6800214/977038
        return set(reduce(list.__add__,
                ([i, n//i] for i in range(2, int(n**0.5) + 1) if n % i == 0)))
    _len = len(s)
    for fact in reversed(list(factors(_len))):
        compare_set = set(izip(*[iter(s)]*fact))
        if len(compare_set) == 1:
            return compare_set


>>> f(t)
set([(1, 0, 1, 1)])

答案 5 :(得分:0)

您可以通过对输入数组的旋转二进制形式进行XOR运算,将其存档在次线性时间内:

  1. 获取数组的二进制表示形式input_binary
  2. i = 1 to len(input_array)/2循环,对于每个循环,将input_binary向右旋转i位,将其保存为rotated_bin,然后比较XOR } rotated_bininput_binary
  3. 产生0的第一个i是所需子字符串的索引。
  4. 完整代码:

    def get_substring(arr):
        binary = ''.join(map(str, arr)) # join the elements to get the binary form
    
        for i in xrange(1, len(arr) / 2):
            # do a i bit rotation shift, get bit string sub_bin
            rotated_bin = binary[-i:] + binary[:-i]
            if int(rotated_bin) ^ int(binary) == 0:
                return arr[0:i]
    
        return None
    
    
    if __name__ == "__main__":
        test = [1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1]
        print get_substring(test) # [1,0,1,1]
    

答案 6 :(得分:0)

这个只是Haskell中一个愚蠢的递归比较。 Knoothe的百万长串(f a)需要大约一秒钟。很酷的问题!我会再考虑一下。

a = concat $ replicate 20000 
    [1,1,1,0,0,1,0,1,0,0,1,0,0,1,1,1,0,0,
     0,0,0,0,1,1,1,1,0,0,0,1,1,0,1,1,1,1,
     1,1,1,0,0,1,1,1,0,0,0,0,0,1]

f s = 
  f' s [] where
    f' [] result = []
    f' (x:xs) result =
      let y = result ++ [x]   
      in if concat (replicate (div (length s) (length y)) y) == s
            then y
            else f' xs y