分区中唯一子字符串的最大数量

时间:2019-11-07 01:50:36

标签: python string algorithm

我修改了标题,以使其更易于理解。

这是问题的详细版本:

我们有一个字符串s,并希望将其拆分为子字符串。每个子字符串彼此不同。从一个剪切中,我们可以拥有的唯一子字符串的最大数量是多少。换句话说,串联形成s的唯一子字符串的最大数量是多少。

以下是一些示例:

Example 1
s = 'aababaa'
output = 4
Explain: we can split `s` into aa|b|aba|a or aab|a|b|aa, 
         and 4 is the max number of substrings we can get from one split.

Example 2
s = 'aba'
output = 2
Explain: a|ba

Example 3
s = 'aaaaaaa'
output = 3
Explain: a|aa|aaaa

注意s仅包含小写字符。我没有被告知s多久,因此无法猜测最佳时间复杂度。 :(

这是NP难题吗?如果没有,该如何有效解决?

我从一个朋友那里听到了这个问题,无法回答。我正在尝试使用Trie +贪婪解决此问题。对于第一个示例,该方法失败。

这是我想出的Trie解决方案:

def triesolution(s):
    trie = {}
    p = trie
    output = 0
    for char in s:
        if char not in p:
            output += 1
            p[char] = {}
            p = trie
        else:
            p = p[char]
    return output

例如1,由于试图将s拆分为a|ab|abaa,因此上面的代码将返回3。

添加:由于每个人的想法,看来这个问题非常接近NP问题。现在,我正在尝试从这个方向进行思考。假设我们有一个函数Guess(n)。如果我们可以从一个拆分中找到True个唯一的子字符串,则此函数将返回n,否则返回False。这里的一个观察结果是,如果Guess(n) == True,则对于所有Guess(i) == Truei <= n。因为我们可以将两个相邻的子字符串合并在一起。这种观察可能导致二元解。但是,它仍然需要我们能够非常有效地计算Guess函数。可悲的是,我仍然找不到一种计算Guess(n)的多项式方法。

7 个答案:

答案 0 :(得分:15)

这被称为“冲突感知的字符串分区问题”,并且在Anne Condon,JánMaňuch和Chris Thachuk的论文《复杂性》中通过从3-SAT减少证明它是NP完全的。碰撞感知的字符串分配问题及其与基因合成的寡核苷酸设计的关系(国际计算与组合技术会议,265-275,2008年)。

答案 1 :(得分:8)

(非常感谢Gilad Barkan(גלעדברקן)使我意识到了这一讨论。)

让我从纯理论的角度分享我对这个问题的想法(请注意,我也使用“ factor”代替“ subword”)。

我认为这里考虑的一个或多个问题的正式定义如下:

给出单词w,找到单词u_1,u_2,...,u_k,这样

  • u_i!= u_j对于每个i,j等于1 <= i
  • u_1 u_2 ... u_k = w

最大化变量(我们想要很多u_i):最大化k

最小化变体(我们想要短的u_i):最小化max {| u_i | :1 <= i <= k}

这些问题通过附加给定边界B变成决策问题,根据我们在说“多因素”变量还是“短因素”变量,它是k的下界(我们想要至少B个因子)或max {| u_i |的上限:1 <= i <= k}(我们希望长度的因子至多为B)。 要谈论NP硬度,我们需要谈论决策问题。

让我们将术语SF用于“短因子”变体,将MF用于“许多因子”变体。 特别是,这是非常关键的一点,问题的定义方式使得我们可以通过 some 字母获得一个不受任何限制的单词。问题的版本是我们先验地知道,我们只能获得输入单词,例如字母{a,b,c,d}是另一个问题! NP硬度 not 不会自动从“不受限制的”变量延续到“固定的字母”变量(后者可能更简单)。

SF和MF都是NP完全问题。这已分别在[1,1b]和[2]中显示(正如Gilad已经指出的那样)。 如果我在讨论开始时正确地理解了(也许也是)非正式问题的定义,那么讨论的问题就是MF问题。最初并没有提到单词仅限于某些固定的字母,后来又说我们可以假定仅使用小写字母。如果这意味着我们只考虑固定字母{a,b,c,...,z}上的单词,那么就NP硬度而言,这实际上将发生很大变化。

仔细研究发现SF和MF在复杂性方面存在一些差异:

  1. 论文[1,1b]显示,如果我们将字母固定为二进制字母,则SF仍然是NP完整的(更准确地说:将单词w覆盖字母a和b以及边界B,我们可以将其分解为不同的因子吗?长度最大为B?)。
  2. 论文[1,1b]表明,如果我们将边界B固定为2,则SF仍然是NP完整的(更确切地说:得到一个单词w,我们能否将其分解为最多2个不同的长度因数?)。
  3. 论文[3]显示,如果字母和边界B都是固定的,则SF可以在多项式时间内求解。
  4. 论文[2]显示MF是NP完全的,但前提是字母不受 限制或先验固定!尤其是,如果我们仅考虑某个固定字母上的输入单词(在实际情况中通常如此),则不能回答问题是否为NP完全问题。
  5. 论文[3]显示,如果MF可以在多项式时间内求解 输入范围B再次由某个常数(即 问题输入是一个单词和一个{1,2,...,K}的边界B,其中K 是一些固定常数。

对这些结果的一些评论:从(1)和(2)可以直观地看出,如果字母是二进制的,那么,为了使问题SF变得困难,边界B也不能被固定。相反,固定B = 2意味着字母大小必须变得相当大才能产生困难的实例。结果,(3)显得微不足道(实际上,[3]说得更多:然后,我们不仅可以在运行时求解多项式,而且可以将| w | ^ 2倍一个仅取决于字母大小的因数)求解并绑定到B)。 (5)也不难:如果我们的单词比B长,那么我们可以通过简单地切成不同长度的因子来获得期望的分解。如果不是,那么我们就可以蛮力所有可能性,这仅在B中呈指数关系,在这种情况下,B被假定为常数。

因此,我们的情况如下:SF似乎更加困难,因为即使对于固定的字母或对于固定的边界B,我们也有硬度。另​​一方面,如果边界为固定(在这方面比SF容易),而对应的问题是wrt字母大小是开放的。 因此,即使事实证明固定字母的MF也是NP完整的,MF的复杂度也比SF略小。但是,如果可以证明MF可以在固定时间内解决固定字母的问题,那么MF比SF容易得多……因为一种很难解决的情况有些人为(无界字母!) 。

我确实花了一些力气试图解决带有有界字母的MF的情况,但此后我无法解决并停止研究。我不相信其他研究人员已经尽力解决了这个问题(因此,这不是这些非常艰巨的公开问题之一,许多人已经尝试并失败了;我认为这是可行的)。我的猜测是,对于固定字母来说这也是NP难的,但是减少的过程可能是如此复杂,以至于您会得到类似“对于35或更大的字母来说MF很难”之类的东西,这也不是太好了。

关于其他文献,我知道论文[4],该论文考虑了将单词w分解为分别是回文的u_1,u_2,...,u_k个独立因子的问题,这些因子也是NP完全的。

吉拉德指出,我快速浏览了论文[5]。不过,似乎要考虑其他设置。在本文中,作者对给定单词中可以包含多少个不同的子序列或子单词的组合问题感兴趣,但是这些单词可以重叠。例如,aaabaab包含20个不同的子词a,b,aa,ab,ba,bb,aaa,aab,aba,baa,baaa,aaab,aaba,abaa,baab,aaaba,aabaa,abaab,aabaab,aaabaa,aaabaab(也许我误算了,但您知道了)。它们中的一些仅发生一次,例如baa,其中一些仅发生一次,例如aa。无论如何,问题不在于我们如何以某种方式拆分单词以获取许多不同的因素,因为这意味着每个单独的符号恰好构成一个因素。

关于解决此类问题的实际解决方案(请记住,我是一名理论家,因此请慎重考虑):

  • 据我所知,如果我们仅考虑固定字母上的输入单词,那么就没有理论上的下界(例如NP硬度)可以排除它在多项式时间内求解MF的情况。但是,有一个警告:如果您使用的是多重时间算法,则该算法应该以固定字母表中的符号数量成指数形式运行(或者在某些函数中以指数形式运行)!否则,对于无界字母来说,它也是多项式时间算法。因此,作为一名理论家,我将寻找仅在符号数量以及以某种方式有助于设计MF算法的情况下才能按时间指数计算的算法任务。 另一方面,在固定字母的情况下,很可能不存在这种算法,MF也是NP-hard。

  • 如果您对实际解决方案感兴趣,则近似解决方案可能会有所帮助。因此,在最坏的情况下保证分解的大小仅为最佳分解的一半是不会太糟糕的。

  • 那些没有给出可证明的近似比率但在实际环境中能很好工作的启发式方法也很有趣。

  • 将问题实例转换为SAT或ILP实例应该不太困难,然后您可以运行SAT或ILP-Solver甚至获得最佳解决方案。

  • 我的个人看法是,即使不知道MF的固定字母是否为NP-hard,也有足够的理论见解表明该问题足够棘手,因此有理由考虑适用于在实际环境中运行良好的启发式解决方案等。


参考书目:

[1] Anne Condon,JánManuch,Chris Thachuk:字符串分区的复杂性。 J.Discrete Algorithms 32:24-43(2015)

[1b] Anne Condon,Jan Manuch和Chris Thachuk:碰撞感知字符串分配问题的复杂性及其与基因合成的寡核苷酸设计的关系。茧2008:265-275

[2] Henning Fernau,Florin Manea,Robert Mercas,Markus L. Schmid:带变量的模式匹配:快速算法和新的硬度结果。 STACS 2015:302-315

[3] Markus L. Schmid:计算无相等且重复的字符串分解。理论。计算科学618:42-51(2016)

[4] Hideo Bannai,Travis Gagie,Innsaga Shunsuke Inenaga,JuhaKärkkäinen,Dominik Kempa,Marcin Piatkowski,Shiho Sugimoto:多样的回文因式分解都是NP完全的。诠释J.发现。计算科学29(2):143-164(2018)

[5]亚伯拉罕·弗拉克斯曼(Abraham Flaxman),阿拉姆·威特罗斯·哈罗(Aram Wettroth Harrow),格雷戈里B. Sorkin:具有最大不同子序列和子字符串的字符串。电器。 J.梳11(1)(2004)

答案 2 :(得分:3)

这是一个解决方案,但是它很快就爆炸了,并且远没有一个有效的解决方案。它将首先将字符串分解为唯一的子字符串列表,而不关心顺序,然后尝试使用itertools.permutation将这些子字符串重新组合回原始字符串,测试EACH置换以查看其是否与原始字符串匹配。

import itertools as it

def splitter(seq):                                                             
    temp = [seq]
    for x in range(1, len(seq)):
        print(seq[:x], seq[x:])
        temp.append(seq[:x])
        temp.append(seq[x:])
    return temp

if __name__ == "__main__":
    test = input("Enter a string: ")
    temp = splitter(test)
    copy = temp[::]
    condition = True
    for x in temp:
        if len(x) > 1:
            copy.extend(splitter(x))
    copy = sorted(list(set(copy)))
    print(copy)
    count = []
    for x in range(len(test)):
        item = it.permutations(copy, x)
        try:
            while True:
                temp = next(item)
                if "".join(list(temp)) == test:
                    if len(temp) == len(set(temp)):
                        count.append((len(temp), temp))
        except StopIteration:
            print('next permutation begin iteration')
            continue
    print(f"All unique splits: {count}")
    print(f"Longest unique split : {max(count)[0]}")

对于第一个测试,我们得到了这个:

All unique splits: [(1, ('aababaa',)), (2, ('a', 'ababaa')), (2, ('aa', 'babaa')), (2, 
('aab', 'abaa')), (2, ('aaba', 'baa')), (2, ('aabab', 'aa')), (2, ('aababa', 'a')), (3, 
('a', 'ab', 'abaa')), (3, ('a', 'aba', 'baa')), (3, ('a', 'abab', 'aa')), (3, ('aa', 'b',
 'abaa')), (3, ('aa', 'ba', 'baa')), (3, ('aa', 'baba', 'a')), (3, ('aab', 'a', 'baa')),
 (3, ('aab', 'ab', 'aa')), (3, ('aab', 'aba', 'a')), (3, ('aaba', 'b', 'aa')), (3,
 ('aaba', 'ba', 'a')), (4, ('a', 'aba', 'b', 'aa')), (4, ('aa', 'b', 'a', 'baa')), (4,
 ('aa', 'b', 'aba', 'a')), (4, ('aab', 'a', 'b', 'aa'))]
Longest unique split : 4

也许可以通过某种方式对其进行优化,但这在这台计算机上花费了几秒钟。

答案 3 :(得分:3)

我已经试着考虑这个问题,或者是否在给定的索引上进行分区。 因此,此函数是递归的,并在每个索引处创建2个分支 1.不要在索引 i 上进行分区 2.在索引i处进行分区。

基于分区,我填写了一个集合,然后返回集合的大小

def max(a,b):
    if a>b: return a
    return b



def keep(last, current, inp, map):
    # print last
    # print current
    # print map

    if len(inp) == 2 :
        if inp[0]==inp[1]: return 1
        return 2

    if current >= len(inp):
        return len(map)
    // This is when we are at the start of the string. 
    // In this case we can only do one thing not partition and thus take the entire string as a possible string.

    if current == last :
        map11 = map.copy()
        map11.add(inp[current:])
        return keep(last, current + 1, inp, map11)

    map1 = map.copy();
    if current != (len(inp)-1):
        map1.add(inp[last:current])

    map2 = map.copy()

    return max(keep(last,current+1,inp, map2), keep(current, current+1, inp, map1))

print keep(0,0,"121", set([]))
print keep(0,0,"aaaaaaa", set([]))
print keep(0,0,"aba", set([]))
print keep(0,0,"aababaa", set([]))
print keep(0,0,"21", set([]))
print keep(0,0,"22", set([]))

https://onlinegdb.com/HJynWw-iH

答案 4 :(得分:3)

您可以使用带有set的递归函数作为第二个参数来跟踪当前路径中的唯一字符串。对于每次递归,遍历所有索引加1,在该索引处将字符串拆分为可能的候选字符串,如果候选字符串尚未在集合中,则使用剩余字符串并将候选对象添加到集合中进行递归调用要从剩余字符串中获取最大数目的唯一子字符串,请向其添加1并从迭代中返回最大值的最大值。如果给定字符串为空或集合中所有候选字符串都已返回,则返回0:

def max_unique_substrings(s, seen=()):
    maximum = 0
    for i in range(1, len(s) + 1):
        candidate = s[:i]
        if candidate not in seen:
            maximum = max(maximum, 1 + max_unique_substrings(s[i:], {candidate, *seen}))
    return maximum

演示:https://repl.it/@blhsing/PriceyScalySphere

在Python 3.8中,也可以使用生成器表达式调用max函数来编写上述逻辑,该生成器表达式可以过滤已通过赋值表达式“看到”的候选:

def max_unique_substrings(s, seen=()):
    return max((1 + max_unique_substrings(s[i:], {candidate, *seen}) for i in range(1, len(s) + 1) if (candidate := s[:i]) not in seen), default=0)

答案 5 :(得分:1)

这是一个基于图论的答案。

建模
可以将这个问题建模为大小为O(n²)的图形上的最大独立集问题,如下所示:
假设w = c_1, ..., c_n为输入字符串。
假设G = (V,E)是一个无向图,其构建如下:
V = { (a, b) such that a,b in [1, n], a <= b }。我们可以看到V的大小为n(n-1)/2,其中每个顶点代表w的子字符串。
然后,对于(a1, b1)(a2, b2)的每两个顶点,我们建立边缘((a1, b1), (a2, b2)) iff
(i)[a1, b1][a2, b2]相交或
(ii)c_a1...c_b1 = c_a2...c_b2
换句话说,如果(i)它们表示的子字符串在w中重叠,或者(ii)两个子字符串相等,则在两个顶点之间建立边。

然后我们可以了解为什么G中的maximum independent set为我们的问题提供了答案。

复杂度
在一般情况下,最大独立集(MIS)问题是NP难的,时间复杂度为O(1.1996^n),并且在多项式空间[Xiao, NamaGoshi (2017)]中。
起初,我认为生成的图将是一个弦图(没有诱导的长度> 3的循环),因为它可以很好地解决此类问题,因此可以在线性时间内解决MIS问题。 但是我很快意识到事实并非如此,找到包含5个或更多长度的诱导循环的示例非常容易。
实际上,结果图不具有我们通常需要的任何“好”属性,并且可以将MIS问题的复杂性降低为多项式。
这只是问题复杂性的上限,因为多项式时间减少仅在一个方向上进行(我们可以将这个问题简化为MIS问题,而不能将其简化为MIS问题,至少不是平凡的)。因此,最终我们以最坏的情况在O(1.1996^(n(n-1)/2))中解决了这个问题。
所以,a,我无法证明它在P中,或者它是NP完整的或NP硬的。可以肯定的是,问题出在NP,但是我想这对任何人来说都不奇怪。

实施
将这个问题简化为MIS问题的优点是MIS是一个经典问题,可以找到几种实现方式,并且MIS问题也很容易写为ILP。
这是MIS问题的ILP公式:

Objective function 
maximize sum(X[i], i in 1..n)
Constraints:
for all i in 1..n, X[i] in {0, 1}
for all edge (i, j), X[i] + X[j] <= 1

我认为,这应该是解决此问题的最有效方法(将此模型用作MIS问题),因为ILP求解器非常有效,尤其是在涉及大型实例时。

这是我使用Python3和GLPK求解器完成的实现。要对其进行测试,您需要一个与Cplex文件格式兼容的LP解算器。

from itertools import combinations

def edges_from_string(w):
    # build vertices
    vertices = set((a, b) for b in range(len(w)) for a in range(b+1))
    # build edges
    edges = {(a, b): set() for (a, b) in vertices}
    for (a1, b1), (a2, b2) in combinations(edges, 2):
        # case: substrings overlap
        if a1 <= a2 <= b1:
            edges[(a1, b1)].add((a2, b2))
        if a2 <= a1 <= b2:
            edges[(a2, b2)].add((a1, b1))
        # case: equal substrings
        if w[a1:b1+1] == w[a2:b2+1]:
            if a1 < a2:
                edges[(a1, b1)].add((a2, b2))
            else:
                edges[(a2, b2)].add((a1, b1))
    return edges

def write_LP_from_edges(edges, filename):
    with open(filename, 'w') as LP_file:
        LP_file.write('Maximize Z: ')
        LP_file.write("\n".join([
            "+X%s_%s" % (a, b)
            for (a, b) in edges
        ]) + '\n')
        LP_file.write('\nsubject to \n')
        for (a1, b1) in edges:
            for (a2, b2) in edges[(a1, b1)]:
                LP_file.write(
                    "+X%s_%s + X%s_%s <= 1\n" %
                    (a1, b1, a2, b2)
                )
        LP_file.write('\nbinary\n')
        LP_file.write("\n".join([
            "X%s_%s" % (a, b)
            for (a, b) in edges.keys()
        ]))
        LP_file.write('\nend\n')
write_LP_from_edges(edges_from_string('aababaa'), 'LP_file_1')
write_LP_from_edges(edges_from_string('kzshidfiouzh'), 'LP_file_2')

然后您可以使用glpsol命令解决它们:
glpsol --lp LP_file_1
aababaa很快就解决了(在我的笔记本电脑上为0.02秒),但是正如预期的那样,随着字符串大小的增加,事情变得(非常)艰难…… 该程序仅给出数值(而不是最佳分区),尽管如此,仍可以使用类似的实现(使用pyomo

等LP解算器/ python接口找到最佳分区和相应的子字符串)。

时间和记忆
aababaa:0.02秒,0.4 MB,值:4
kzshidfiouzh:1.4秒,3.8 MB,值:10
aababababbababab:60.2秒,31.5 MB,值:8
kzshidfiouzhsdjfyu:207.5秒,55.7 MB,值:14
请注意,LP解算器还提供了解决方案的当前上下限,因此对于最后一个示例,我可以在一分钟后得到实际的解决方案作为下限。

答案 6 :(得分:0)

我的其他answer密切相关,但与该问题并不完全对应,因此对于找到最大的无相等字符串因式分解可能是否具有与是否存在无相等因式分解不同的复杂度类别,仍然存在不确定性具有约束因子的长度(引用的论文对此予以解决)。

在论文中,变量的模式匹配:快速算法和新的硬度结果(Henning Fernau,Florin Manea,RobertMercaş和Markus L. Schmid,在第32届计算机科学理论方面的研讨会上,STACS 2015年,《莱布尼兹国际信息学报》(LIPIcs)第30卷,第302-315页,2015年),作者表明,对于给定的数字k和一个单词,决定NP是完全的。 w,是否可以将w分解为k个不同的因素。

如果我们考虑templatetypedef的comment,则意味着可以对无限制的最大无相等因式分解进行多项式时间解,那么我们可以肯定地使用这种算法来回答是否可以将字符串拆分为{{1 }}通过简单地观察k是否小于我们已经知道的最大值来区分不同的因素(子字符串)。

Schmid(2016)写道:“如果字母固定,MaxEFF-s是否保持NP完整仍是一个未解决的问题。” (计算无相等和重复的字符串因式分解,理论计算机科学第618卷,2016年3月7日,第42-51页)

不过,最大无平等分解大小(MaxEFF-s)仍是参数化的,并定义为:

实例:一个单词k和一个数字wm

问题:是否存在1 ≤ m ≤ |w|w的无相等分解因子p? (s(p) ≥ m是分解的大小。)