范围[l,r]中可以置换为回文的子串数

时间:2017-01-24 11:02:33

标签: string algorithm data-structures

给定s小写英文字母,|s| <= 10^5。最多q <= 10^5个查询给出范围[l, r],询问:字符串s[l...r]的多少个子字符串可以被置换形成回文。

现在,如果出现奇数次的字符数最多为1,则字符串可以置换为回文。我试图使用段树但似乎无法合并两个范围。我该怎么办呢?

2 个答案:

答案 0 :(得分:7)

为了得到更好的答案,我们可以应用Mo的算法来获得O(n^(3/2) |alphabet|) - 时间算法。这对你来说可能足够快。核心是以下增量算法,用于计算整个字符串的回文可置换子串的数量(在Python 3中):

import collections


def ppcount(s):
    n = 0
    sigcount = collections.Counter()
    sig = 0
    for c in s:
        sigcount[sig] += 1
        sig ^= 1 << (ord(c) - ord('a'))
        n += sigcount[sig]
        for i in range(26):
            n += sigcount[sig ^ (1 << i)]
    return n

变量sig跟踪到目前为止输入中哪些字母具有奇数频率。如果长度为s[l:r]的前缀的签名最多为汉明距离l,则子字符串r(包括l,排除1)是回文可置换的来自长度为r-1的前缀的签名。地图sigcount跟踪有多少前缀具有特定签名。

要应用Mo算法,首先为上面的循环体写入逆操作(即从n减去,更新sig,并递减sigcount)。读入所有查询并按(l // int(sqrt(n)), r)对其进行排序。对于按排序顺序的每个查询,使用更新和反向更新操作来调整被视为s[l:r+1]的字符串,然后报告当前总数。

使用Python代码(首先是天真的版本,用于比较;继续滚动):

import collections
import math
import random


def odd(n):
    return bool(n & 1)


def ispp(s):
    return sum(odd(n) for n in collections.Counter(s).values()) <= 1


def naiveppcount(s):
    n = len(s)
    return sum(ispp(s[l:r + 1]) for l in range(n) for r in range(l, n))


def bit(c):
    return 1 << ((ord(c) - 1) & 31)


def neighbors(sig):
    yield sig
    for i in range(26):
        yield sig ^ (1 << i)


class PPCounter(object):
    def __init__(self):
        self.count = 0
        self._sigcount = collections.Counter({0: 1})
        self._leftsig = 0
        self._rightsig = 0

    def pushleft(self, c):
        self._leftsig ^= bit(c)
        for sig in neighbors(self._leftsig):
            self.count += self._sigcount[sig]
        self._sigcount[self._leftsig] += 1

    def popleft(self, c):
        self._sigcount[self._leftsig] -= 1
        for sig in neighbors(self._leftsig):
            self.count -= self._sigcount[sig]
        self._leftsig ^= bit(c)

    def pushright(self, c):
        self._rightsig ^= bit(c)
        for sig in neighbors(self._rightsig):
            self.count += self._sigcount[sig]
        self._sigcount[self._rightsig] += 1

    def popright(self, c):
        self._sigcount[self._rightsig] -= 1
        for sig in neighbors(self._rightsig):
            self.count -= self._sigcount[sig]
        self._rightsig ^= bit(c)


def ppcount(s, intervals):
    sqrtn = int(math.sqrt(len(s)))
    intervals = sorted(
        intervals, key=lambda interval: (interval[0] // sqrtn, interval[1]))
    l = 0
    r = -1
    ctr = PPCounter()
    for interval in intervals:
        il, ir = interval
        while l > il:
            l -= 1
            ctr.pushleft(s[l])
        while r < ir:
            r += 1
            ctr.pushright(s[r])
        while l < il:
            ctr.popleft(s[l])
            l += 1
        while r > ir:
            ctr.popright(s[r])
            r -= 1
        yield interval, ctr.count


def test():
    n = 100
    s = [random.choice('abcd') for i in range(n)]
    intervals = []
    for i in range(1000):
        l = random.randrange(n)
        r = random.randrange(n)
        intervals.append((min(l, r), max(l, r)))
    for (l, r), count in ppcount(s, intervals):
        assert count == naiveppcount(s[l:r + 1])


if __name__ == '__main__':
    test()

答案 1 :(得分:0)

注意:解决方案技术基于仅记录每个位置开始或结束的可触发子串的数量可以工作,因为它必然无法区分(至少)以下两种情况(通过计算机搜索找到),它们具有相同的UpTo []和From []数组,但具有不同的PS集:

ijegajaei       dacedcadc
|-------|       |-----|
 |-----|         |-----|
    |-|             |---|

虽然我在下面描述的解决方案尝试是基于这个有缺陷的想法,除非有人有异议,我想我会留下这些信息,以防万一其他人不能走这条路。

这里有一种不同的方法,可以在 O(1)时间和在线中回答每个[l,r]查询(也就是说,我们可以在每个查询到达时回答它们 - 我们不会在 O(| A | n log n)预处理之后,需要查看所有内容然后重新排序,其中| A | = 26是字母大小。它需要O(| A | n)空间。

概述

基本思想是在预处理步骤中计算每个位置i,在i或任何更早位置结束的可触发子串的数量(让我们称之为UpTo [i]),以及数字从i或任何后来的位置开始的可触发子串(让我们称之为[i])。一旦我们得到这些信息,请注意对于任何查询[l,r],UpTo [r] + From [l]只计算一次可触发的子串,除了在和在r之前或之前结束,计数两次。因此,如果我们从表达式UpTo [r] + From [l]中减去可触发子串的总数,我们就可以得到我们正在寻找的东西 - 从l开始或之后开始的可触发子串的数量或者在r之前。显然,我们可以在两个长度为n + 1的数组中存储UpTo []和From [],并在每个查询的O(1)时间内执行此计算;现在的问题是如何计算这两个表中的条目。

后缀最小可回收子串

首先,一些定义(主要取自我之前的错误答案):

调用范围[l,r] 有效,如果它是可触发的,即,如果此子字符串中的至多一个字符具有奇数频率。如果其中的所有字符都具有偶数频率,则调用有效范围甚至,否则称其为奇数,特别是 c-odd ,其中c为具有奇数频率的独特字符。

这里我们将关注计算在特定位置i结束的可触发子串(PS)的数量EndsAt [i]。 (显然我们可以从EndsAt []的运行总计算出UpTo [],我们可以通过使用反向字符串计算From []。我们将分别计算的数字EvenEndsAt [i]甚至 PS结束于我。

考虑从j开始到i结束的均匀PS&gt;学家要么它是最短的甚至PS结束于i,在这种情况下我们称之为 minimal ,或者有一些较短的偶数PS结束于i并从一些j&#39;开始。 &GT;学家以下观察是有效计算EvenEndsAt [i]的关键:

  • 如果[j,i]和[j&#39;,i]都是偶数PS,则j < J&#39; &LT;我,然后[j,j&#39; -1]也是一个偶数PS。 (IOW:如果字符串是偶数PS,并且它具有正确的后缀,那么删除该后缀会留下一个也是偶数PS的字符串。)

这是因为从偶数中减去偶数总是给出偶数。这意味着,如果我们可以确定在某些i处结束的最小偶数PS [k,i],那么我们可以使用EvenEndsAt [i] = 1 + EvenEndsAt [k-1]来计算EvenEndsAt [i]。

找到以i

结尾的最小偶数PS

如何找到这个最小的甚至PS [k,i](当它存在时)?正如David Eisenstat的回答中所提到的,为了确定[a,b]是否是一个偶数PS,我们需要关心的是累积的奇偶校验(奇数或均匀度)位置a-1和位置b的字符频率。对于偶数PS,这26个字母中的每个字母必须相等。我还将使用签名来描述到目前为止具有奇数频率的字母集。

所以我们可以做的是向前扫描字符串,维护(在哈希表或平衡树中)我们到目前为止看到的每个签名的最近(即最右边)位置。请注意,我们必须记录所有2 ^ 26个可能的签名 - 只记录我们实际看到的那些签名,这会将空间使用限制为O(n)。

奇数PS

类似的观察结果适用于奇数PS。我们只需要小心计算每个c的每个位置结束的c-odd PS,这样我们就不会计算任何奇数PS两次。

考虑从j开始并在i结束的c-odd PS。学家要么它是以i结尾的最短c-odd PS,在这种情况下我们称之为 minimal ,或者有一些较短的c-odd PS结束于i并从某些j开始。 &GT;学家我们可以使用以下观察来计算以i结尾的c-odd PS的总数:

  • 如果[j,i]和[j&#39;,i]都是c-odd PSes(注意:对于相同的字符c!),j&lt; J&#39; &LT;我,然后[j,j&#39; -1]是甚至 PS。 (IOW:如果一个字符串是一个奇数PS,并且它有一个正确的后缀,它是一个奇数PS并且其中相同的字符具有奇数频率,那么删除该后缀会留下一个偶数PS的字符串。)

推理只比上次稍微强一点:如果我们有一个奇数和一些偶数,我们从奇数中减去一个奇数,并从每个偶数中减去偶数,我们必须结束只有偶数。因此,如果[k,i]是在位置i结束的最小c-odd PS,那么在位置i结束的c-odd PS的总数由1 + EvenEndsAt [k-1]给出。

伪代码

这是用于计算UpTo []表的伪代码。如前所述,可以通过在反向字符串上运行此来计算From [],然后可以在O(1)时间内回答每个查询(UpTo [n]或From [1]将给出PS中的总数整个字符串,这是最后一次减法所需的)。我在这里使用基于1的索引:

FindPS():
    Dictionary lastPosOfSignature       # Hashtable or balanced tree
    currentSig = 0                      # Integer used as 26-bit bitset
    For i from 1 to n:
        # Find the minimal even PS ending at i, if it exists
        currentSig ^= 1 << (S[i] - 'a')
        If lastPosOfSignature contains currentSig:
            # There's a minimal even PS ending at i.
            k = lastPosOfSignature{currentSig}
            EvenEndsAt[i] = 1
            If k > 1:
                EvenEndsAt[i] += EvenEndsAt[k-1]
        Else:
            EvenEndsAt[i] = 0

        EndsAt[i] = 0
        For c from 1 to 26:
            # Find the minimal c-odd PS ending at i, if it exists
            cOddSig = currentSig ^ (1 << (c-1))
            If lastPosOfSignature contains cOddSig:
                # There's a minimal c-odd PS ending at i.
                k = lastPosOfSignature{cOddSig}
                EndsAt[i] += 1
                If k > 1:
                    EndsAt[i] += EvenEndsAt[k-1]

        lastPosOfSignature{currentSig} = i
        UpTo[i] = EndsAt[i]
        If i > 1:
            UpTo[i] += UpTo[i-1]