今日頭條2017校招題目解析(一):KMP中next數組與Trie樹的應用

這段時間工做上的事情特別忙,因此也有一段時間沒有更新了,此次咱們來處理今日頭條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
...spa

比較典型的一個樹形結構,既然是字典序,咱們天然聯想到字典樹,也就是Trie樹了。觀察下構造的對應的Trie樹部分結構,以下圖
*關於Trie樹的基礎知識和更多應用,我計劃在下次詳細介紹。code

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’開頭等等ip

觀察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 - beginci

至此,該問題得以解決。固然,受限於題目的數據規模,若是是按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()
相關文章
相關標籤/搜索