今日头条2017校招题目解析(一):KMP中next数组与Trie树的应用

 阅读约 14 分钟

这段时间工作上的事情特别忙,所以也有一段时间没有更新了,这次我们来处理今日头条2017秋招的题目, 共4个题目,总体来说要100%通过测试数据有一定难度。这次我们选择其中的3个问题来进行简单分析,期间会提到KMP算法的next数组和Trie树在这次解题中的应用。

下次更新计划详细介绍Trie树以及应用,并解析剩余的一个题目,这个题目依然会用到Trie树来处理。网上关于题目的解析也比较多,算法也比较多,我会尝试用一些比较高效而简洁算法来实现。在赛码网题目都没有给出数据规模,我在题目后进行了补充,因为数据规模对我们选择算法很重要。


字典序

<题目来源: 今日头条2017秋招 原题链接-可在线提交(赛码网)>

问题描述

给定整数n和m,将1到n的这n个整数按字典序排列之后,求其中的第m个数字。 对于n = 11,m = 4,按字典序排列依次为1, 10,
11, 2, 3, 4, 5, 6, 7, 8, 9,因此第4个数字为2。

clipboard.png

数据规模
对于20%的数据, 1 <= m <= n <= 5
对于80%的数据, 1 <= m <= n <= 10^7
对于100%的数据, 1 <= m <= n <= 10^18

我们不妨先列出一些数字来观察字典序到底是个什么序:例如1 - 103的字典序的前面若干个数
1
10
100
101
102
103
11
12
13
14
15
16
17
18
19
20
21
...

比较典型的一个树形结构,既然是字典序,我们自然联想到字典树,也就是Trie树了。观察下构造的对应的Trie树部分结构,如下图
*关于Trie树的基础知识和更多应用,我计划在下次详细介绍。

clipboard.png

这个Trie可以看做是一颗十叉树,如果要获取完整的字典序,我们仅需要按深度优先的顺序去遍历trie的每个节点,对于每个节点,按0-9的顺序访问叶节点即可。因此,如果我要们打印1-n的字典序就可以用深度优先搜索(dfs)来实现了。对于这个题目而言,我们只需要知道第m个数是什么,至于之前的m-1个数是什么我们其实并不关心。

由于从1开始,我们先统计以1开头的子树的节点总数k(注意需要满足所有包含在该子树的数 <=n):
a.如果k > m m,我们要找的第m个数就在这个节点开头的子树里面,换言之,第m个数一定是这个节点表示的数开头的,我们令m = m - 1,然后继续查找下一层确定下一位数字
b.如果k <= m ,那么不在这个节点的子树中,我们继续在这个节点的右侧(兄弟节点中),由于之前节点和其子树包含了若干个节点t,我们令m = m - t,然后继续查找同一层确定下一位数字
c.当m = 0时,我们就得到想要的第m个数

最后就只剩下一个需要解决的问题:如何计算以一个特定数开头的所有满足 <= n的所有数的总个数,比如可以是‘1’开头,‘23’开头等等

观察Trie树的结构,考虑开头为prefix的数所包含的所有可能的数字,我们设置两个区间标识变量begin和end,在prefix的最后一层trie树的节点中,例如prefix=234,那么最后一层节点为4,对于3位的情况,最大的数为end = begin - 1,如果end比n小,那么说明以prefix开头的数位数比3位更多,考虑下一位,同样,下一位开头为begin = begin 10, end = end 10(除第一层外,每层的数的个数都是上层的10倍),继续比较end和n的关小,看是否需要继续向下寻找,如果end 已经小于等于n了,那么当前这层的节点数就是n + 1 - begin

至此,该问题得以解决。当然,受限于题目的数据规模,如果是按DFS的方法去逐位构造统计,只能通过30%的数据。

import sys


def get_subtree_num(prefix, n):
    count = 0
    begin = prefix
    end = begin + 1

    while begin <= n:
        count += min(n + 1, end) - begin
        begin *= 10
        end *= 10

    return count

if __name__ == '__main__':
    line = map(int, sys.stdin.readline().strip().split())
    n, m = line[0], line[1]
    subtree_cnt = 0
    r = 1

    m -= 1
    while m:
        subtree_cnt = get_subtree_num(r, n)

        if subtree_cnt <= m:
            m -= subtree_cnt
            r += 1
        else:
            r *= 10
            m -= 1

    print r

String Shifting

<题目来源: 今日头条2017秋招 原题链接-可在线提交(赛码网)>

问题描述

我们规定对一个字符串的shift操作如下: shift(“ABCD”, 0) = “ABCD” shift(“ABCD”, 1) =
“BCDA” shift(“ABCD”, 2) = “CDAB” 换言之, 我们把最左侧的N个字符剪切下来, 按序附加到了右侧。
给定一个长度为n的字符串,我们规定最多可以进行n次向左的循环shift操作。如果shift(string, x) = string (0<= x <n), 我们称其为一次匹配(match)。求在shift过程中出现匹配的次数。

图片描述

数据规模
30%的样例中输入字符串的长度<100
100%的样例中输入字符串的长度<10^6

这个题目可以这样来理解,每次从串的末尾拿走一个,放到串的开头,然后比较和原串是否相同,然后再执行同样的操作,直到串中的每个字符都被移动了一次为止;

简单来说,暴力解法仍然是首选,我们可以完全模拟题目中的shifting操作,然后进行字符串比较统计结果即可。但是,这样做的效率是O(n^2),对于70%的数据规模在10^6,是不可能在限定时间内出解的。

在上面的暴力解法中,我们仅移动了一位,就又要进行一次长度为n的比较,是否代价过大。再回到题目本身,如何出现相同的情况?

设s是原串,长度为n,我们从s左侧顺序拿走t个字符组成串s1,此时,我们将该串拼接到s后面,表示成s[t]s[t + 1]s[t + 2]...s[n - 1]s[n]s[n + 1]...s[n + t - 1]
这个串需要和原串s[0]s[1]...s[n - 1]相等,也就是s[t]st + 1 ... = s[0]s[1]s[2]...[t - 1],要出现这样的情况,只有当整个串是按s[0]s[1]s[2]..s[t - 1]作为一个循环节出现的形式,例如
abcabcabc, abcaabca, aaabbb

至此,这个问题我们转换为一个查找循环节的问题
我们可以先枚举循环节的长度,然后检查是否满足,检查的方法就是字符串匹配了,尽管我们可以采用一些高效的匹配办法,但这个仍然不是最佳的解法,其实查找循环节有个非常经典的算法是利用kmp算法中的next数组,关于KMP算法网上随便一搜索就是一大堆,其中有些写的很好,也很容易理解,这里我只补充两张图来帮助理解next数组的含义,整个kmp算法的核心就在于这个next数组了。

最后,我看了下其他的一些解法,发现C++居然可以用substr过掉,还有用hash的,都是一些比较好的方法。如果我们使用相对暴力的一些算法,我们也要尽可能的减少计算,比如,我们枚举的循环节长度为k,如果len(s) % k != 0,显然就不需要再进行比较,另外循环节的长度不是超过len(n)/2。

clipboard.png

clipboard.png

import sys


def get_next(s):
    next = [0 for i in range(len(s) + 1)]
    j = next[0] = -1

    i = 0
    while i < len(s):
        if j == -1 or s[i] == s[j]:
            i += 1
            j += 1
            next[i] = j
        else:
            j = next[j]

    return next[-1]


def main():
    line = map(str, sys.stdin.readline().strip().split())[0]
    c = get_next(line)
    print len(line) / (len(line) - c) if len(line) % (len(line) - c) == 0 else 1


if __name__ == '__main__':
    main()

头条校招

<题目来源: 今日头条2017秋招 原题链接-可在线提交(赛码网)>

问题描述

头条的2017校招开始了!为了这次校招,我们组织了一个规模宏大的出题团队。每个出题人都出了一些有趣的题目,而我们现在想把这些题目组合成若干场考试出来。在选题之前,我们对题目进行了盲审,并定出了每道题的难度系数。一场考试包含3道开放性题目,假设他们的难度从小到大分别为a,
b, c,我们希望这3道题能满足下列条件:
a<= b<= c, b - a<= 10, c - b<= 10
所有出题人一共出了n道开放性题目。现在我们想把这n道题分布到若干场考试中(1场或多场,每道题都必须使用且只能用一次),然而由于上述条件的限制,可能有一些考试没法凑够3道题,因此出题人就需要多出一些适当难度的题目来让每场考试都达到要求。然而我们出题已经出得很累了,你能计算出我们最少还需要再出几道题吗?

图片描述

这个题目应该是4个题目中最简单的一个题目了。根据题目条件n个题目要分到若干考场,且每个考场都要包含3个题目,显然题目个数需要是3的倍数。

根据题目中的a<= b<= c, b - a<= 10, c - b<= 10,其实说每3个题目之间难度递增,且在此条件下,两两之间的难度差不超过10。

所以考虑先对所有题目按难度从小到大排序,这样能确保找出尽可能多的题目满足上述条件,如果不满足就必须增加题目。

思考:如何证明排序的情况下增加的题目不会比乱序的情况更多?
排序后,我们只需要按顺序观察题目是否满足该条件,如果不满足情况,我们就不得不增加题目了。

从右侧开始处理,设置当前的状态0 - 2,分别表示添加第1 - 3个题目;
1.状态为0时,首先添加第一个题目,状态设置为1;
2.状态为1时,已经添加了第一个题目,那么检查下个题目和已经添加题目的难度差,如果<=10,状态设置为2;如果<=20,那么在这两个题目中加一个题目即可,状态设置为回到0;
3.状态为2时,已经添加了前两个题目,检查第下个题目是否满足<=10,如果不满足,增加一个题目,将下个题目作为下次添加的第一个题目,最终状态都回到0;
注意最终的题目数需要为3的倍数

最后,这个题目的测试数据应该存在问题,提交的代码中有几个错误算法居然accept了,比如数据
2
3 17
应该是增加一个题目,而不是4个

import sys


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

    sta = 0
    r = 0
    p = 0
    while p < len(array):
        if sta == 0:
            a = array[p]
            p += 1
            sta = 1
        elif sta == 1:
            b = array[p]
            if b - a <= 10:
                p += 1
                sta = 2
            elif b - a <= 20:
                r += 1
                p += 1
                sta = 0
            else:
                r += 2
                sta = 0
        else:
            c = array[p]
            if c - b > 10:
                r += 1
            else:
                p += 1
            sta = 0

    # print 'sta = ', sta
    if sta:
        r += 3 - sta
    print r

if __name__ == '__main__':
    main()

阅读 1.7k更新于 2018-04-19

推荐阅读
Lite's home
用户专栏

和大家分享一些经典的算法,以及解决问题的思路和方法

1 人关注
12 篇文章
专栏主页
目录