360校招试题解析(二):通过数据结构-二进制状态压缩优化算法

本次的题目解析仍然是360的,这次选择了3个三星难度的题目,但实际上,这个其中有1个题目还是有一定难度的,甚至超过之前四星难度的题目,可见赛码的难度提示也不是很准确。

360的题目还是比较有意思,其中的一个题目,我们对算法进行多次优化后,才能在规定时间内出解,是值得和大家分享的。这些题目的代码都不是很长,也不难实现,但都带有一定的技巧性。


偶串

<题目来源: 360 2017春招 原题链接-可在线提交(赛码网)>

问题描述

一个字符串S是偶串当且仅当S中的每一个字符都出现了偶数次。如字符串”aabccb”是一个偶串,因为字符a,b,c都出现了两次。而字符串”abbcc”不是偶串,因为字符a出现了一次。

现在给出一个长度为n的字符串T=t1,t2,t3,…,tn。字符串的子串为其中任意连续一段。T长度为1的子串有n个,长度为2的子串有n-1个,以此类推,T一共有n(n+1)/2个子串。给定T,你能算出它有多少个子串是偶串吗?

首先是需要明确偶串的定义,即这个串中每个字符都出现偶数次,当然,没有出现的字母不影响偶串的定义。显然,对于一个长度为计数的串不可能是偶串。其次,我们需要明确子串的定义,原串中任意连续的一段都是一个子串,特别地,长度为1的单个字母也是它的子串,只不过它不可能是偶串。

最容易的办法是枚举这个串的所有子串,再检查每个子串是否为偶串。我们来看下数据规模,极限数据为100000的串长,并且需要枚举子串的起点和终点,然后再次扫描这个子串来检测它是否为偶串,这样做的算法时间复杂度为O(n^3),显然是无法承受的。

我们需要对这个算法进行优化。首先,我们考虑判断某个子串是否为子串是否有更好的办法。考虑到题目中的字母总数仅有26个,如果我们设f(i,j)表示从原串S开头到第i个位置,字母j的个数,S(n, m)表示串中第n个字母开始到第m个字母的子串,为了方便描述,首字母的位置是1,例如f(5, 'm') = 2 表示S的前5个字母中‘m’的个数为2。如果要知道子串S(n, m)是否为偶串,我们可以判断下面的公式是否成立:

v = f(m, j) - f(n - 1, j) (j = 'a' ... 'z') [1]

的值,如果每个差值都是偶数,那么显然S(n, m)是一个偶串。这样无论是多长的子串,我们都仅需要扫描26次,也就是计算'a' ... 'z'的个数即可。尽管如此,基于题目的数据规模来讲,这样的时间复杂度仍然是不可接受的。

再进一步考虑,如果要减少算法的时间复杂度,就必然要降低枚举子串的时间复杂度,对于S从头开始且长度为l的子串(即S(1, l)),如果我们可以直接知道1 ... l - 1的这些位置中,哪些位置可以直接使公式[1]成立,那么就相当于有多少个满足偶串的子串。因为f需要计算26个值后我们才能知道最终是否满足,我们需要考虑是否能简化这个公式的计算。对于具体的f(i, j)而言,我们只关心每个字母出现的次数是奇数还是偶数,而不是具体的次数,我们可以使用0,1表示,并且使用^(异或)进行计算,再考虑到仅有26个小写字母,可以考虑采用二进制位的形式进行压缩存储,二进制的第k位就表示字母k的个数是偶数还是奇数

例如串'aabcccdd' 就可以用二进制的110表示,对应十进制为6,如果这个串接下来的一位是还是d,那么这个对应的二进制就是1110了,对应十进制为14。注意到,a ^ a = 0, a ^ 0 = a,因此我们将公式[1]改为如下的形式:

v = f(m) ^ f(n) (n ∈ [1, n - 1]) [2]

因此,我们只需要枚举子串的一个结尾m, 根据公式[2],f(m)已经计算出,我们只需要找出所有令v = 0 的f(n),也就是找所有的f(m) = f(n)即可。显然这里需要实现快速查找f(m) = f(n)的个数。考虑到'a' ... 'z' 都是出现奇数次的情况有fmax = (1 << 26) - 1 = 67108861,显然不太适合直接使用数组下标记做索引,一个比较好的方法是使用map来处理这个问题。由于本文讨论的重点不在于此,这部分就略过了。使用方法可以参考:
https://www.cnblogs.com/fnlin...
如果想知道其实现原理,则需要学习红黑树或者Hash,因为不同的语言(库)使用的实现方法是不一样的。

总结:从这个问题的解决中,我们知道利用题目的一些特定的条件,来构建适合的数据结构可以对程序的优化起到很大的作用,另外,二进制的压缩处理方式是一种常见数据处理方式,尤其是在二进制位数可以接受的时候。

最后,注意到我们的代码实现是非常精简的。

import sys


def main():
    string = map(str, sys.stdin.readline().strip().split())[0]
    l = len(string)

    f = r = 0
    d = {0: 1}
    for i in range(1, l + 1):
        f ^= 1 << (ord(string[i - 1]) - ord('a'))

        query = d.get(f)
        if query is not None:
            r += query
            d[f] += 1
        else:
            d[f] = 1

    print r

if __name__ == '__main__':
    main()

剪气球串

<题目来源: 360 2017春招 原题链接-可在线提交(赛码网)>

问题描述

小明买了一些彩色的气球用绳子串在一条线上,想要装饰房间,每个气球都染上了一种颜色,每个气球的形状都是各不相同的。我们用1到9一共9个数字表示不同的颜色,如12345则表示一串5个颜色各不相同的气球串。但小明希望得到不出现重复颜色的气球串,那么现在小明需要将这个气球串剪成多个较短的气球串,小明一共有多少种剪法?如原气球串12345的一种是剪法是剪成12和345两个气球串。

注意每种剪法需满足最后的子串中气球颜色各不相同(如果满足该条件,允许不剪,即保留原串)。两种剪法不同当且仅当存在一个位置,在一种剪法里剪开了,而在另一种中没剪开。详见样例分析。

显然这是一个排列组合的问题,一般我们会采用递推的方式处理,考虑已经计算了n个气球的方案数,那么在新加入一个气球以后,如果将新的气球剪断和前n个之间剪断,那么前n个气球有多少种方案,那么这n + 1就有多少种方案。如果新加入的这个气球颜色和n个气球中的最后一个颜色不同,那么我们将n个气球中的最后一个和新加入的这个连起来,那么此时的方案数就是前n - 1个气球的方案总数,因此新加入一个气球后,总会和之前产生的所有剪断方法不同。

一般地,设f(i)表示前i个气球可以剪出的气球串种类数,考虑在第i个气球加入时的情况:
(1)如果i个和i - 1气球颜色不同,则可以将i和i - 1这两个气球中间剪断,形成一种新的方案,此时的方案数为f(i - 1)
(2)如果i和i - 1, i - 2都互不相同,则可以将i和i - 1和串起来,剪断i - 2和i - 1之间的线,形成一种新的方案,此时的方案数目f(i - 2)
(3)如果i和i - 1, i - 2以及i - 3都互不相同,则可以将i, i - 1, i - 2串起来,剪断i - 2和i - 3的线,形成一种新的方案,此时的方案数为f(i - 3)
以此类推,由于气球只有9种颜色,那么如果第i个气球和前i - 8个气球的所有颜色都互不相同,那么此时的方案数是f(i - 8)
综上所述:
f(i) = ∑f(k) k∈[i - 1 ...i - 8],且i, i - 1, ... k的颜色均互不相同

需要注意到,这个代码中状态表示并非是最好的方式,因为f是可以合并成为1维数组的。思考,为什么f可以合并?

import sys


def main():
    n = map(int, sys.stdin.readline().strip().split())[0]
    color = map(int, sys.stdin.readline().strip().split())

    f = [[0 for j in range(2)] for i in range(n + 1)]
    f[0][0] = 1

    for i in range(1, n + 1):
        f[i][0] = (f[i - 1][0] + f[i - 1][3]) % 1000000007
        used = [False for k in range(10)]
        used[color[i - 1]] = True
        for j in range(i - 1, 0, - 1):
            # print j
            if not used[color[j - 1]]:
                f[i][4] = (f[i][5] + f[j][0]) % 1000000007
                used[color[j - 1]] = True
            else:
                break

    # print f
    print (f[n][0] + f[n][6]) % 1000000007


if __name__ == '__main__':
    main()

病毒

<题目来源: 360 2017春招 原题链接-可在线提交(赛码网)>

问题描述

小B最近对破解和程序攻击产生了兴趣,她迷上了病毒,然后可怕的事情就发生了。不知道什么原因,可能是小B的技术水平还不够高,小B编写的病毒程序在攻击一个服务器时出现了问题。尽管成功的侵入了服务器,但并没有按照期望的方式发挥作用。
小B的目的很简单:控制服务器的内存区域,试图在内存中装入从1到n之间的n个自然数,以覆盖内存区域。可能是小B对编程理解上的问题,病毒似乎没有完全成功。可能是由于保护机制的原因,内存写入只接受二进制的形式,所以十进制表达中除0和1之外的其他值都没有成功写入内存。小B希望知道,究竟有多少数成功的写入了服务器的内存!

这个题目相对简单的多,但是n相对比较大,如果是1 - n去逐个去按位分离检查,看每位上是否为0或者1,就比较慢。一个关键点是理解字符串和进制。

对于题目中的n,我们将n按十进制的位拆分开,然后找到不大于n的最大的二进制数,这个时候要把这个二进数看做十进制的。例如n = 2329,那么不大于它的最大的二进制就是1111,(下一个是10000,就比n大了),又比如n = 2309,第一个比其小的最大的二进制数1101。这样二进制中比其小的任何一个数显然都是1..n中某个数按位分拆出来的。

那么问题的答案就是统计小于等于这个二进制数的个数即可。

import sys


def main():
    while True:
        line = map(int, sys.stdin.readline().strip().split())
        if len(line):
            n = line[0]
        else:
            break

        temp = n
        b = []
        while temp > 0:
            b.append(temp % 10)
            temp /= 10

        f = False
        r = 0
        for i in b[::-1]:
            r *= 2
            if i > 1:
                f = True
            if f or i == 1:
                r += 1

        print r


if __name__ == '__main__':
    main()

你可能感兴趣的:(数据结构,算法,优化,二进制,状态记录)