字節跳動二次筆試總結


1. 前言:    

這次筆試是少數幾次我自己獨立完成的,但是結果並不好寫了兩道題目。起初我們實驗室三個人,商量着一人做一道,因為兩個小時做4道題目,對我們這些菜雞來說幾乎是不可能完成的任務。但結果是,都tmd快考完了,我負責的最后一題還沒寫出來。最終我的第一道題目是在同門的協助下寫出來的,第四道題目趕快結束才寫完,直接還沒來得及運行用例就提交了。實際上最后一題我的思路是比較清晰的,但是在創建圖和寫DFS的時候花了太多的時間,還有就是寫代碼的時候,一定要思維先行,想清楚要干什么再寫。下面我來帶大家看看這四道題目:

2. First Problem

第一題叫做“豆油瓶”,題目類似於Leetcode 547 朋友圈。

2.1 題目描述:

抖音上每天有幾億用戶,如果用戶A和用戶B互動不少於三次,我們就認為A和B屬於豆油,如果A和B屬於豆油,B和C屬於豆油,那么A和C 也屬於豆油。我們定義豆油瓶就是由直系和間接朋友所組成的群體。
 
給定一個N*N的矩陣M,代表抖音上所有用戶的互動次數,如果M[i][j] = 5,那么第i個用戶和第j個用戶的互動次數就為5次,為0的話代表沒有互動。對於i == j,即同一個用戶,互動次數我們計為0。請你計算並輸出發現的抖音上的豆油瓶的個數。
   
樣例輸入:
    3
    0 4 0
    4 0 0 
    0 0 0
樣例輸出:2 (用戶0和用戶1的互動次數為4次,所以0和1組成了一個豆油瓶,用戶2沒有產生互動,所以單獨為一個豆油瓶)

2.2. 解答:

這道題實質上就是求無向圖的連通分量個數,所以我們用DFS來解答。不過也能用並查集來做。
思路是:1. 根據約束條件創建無向圖 2. 求連通分量個數
var marked []bool
var count int

func DFS(v int, g [][]int) {
    fmt.Println("v", v, "marked", marked)
    marked[v] = true
    adjs := getAdjs(v, g)
 
    for _, adj := range adjs {
        if marked[adj] == false {
            DFS(adj, g)
        }
    }
}

func getAdjs(v int, g [][]int) []int {
    var adjs []int
    for i, elm := range g[v] {
        if elm != 0 {
            adjs = append(adjs, i)
        }
    }
    return adjs
}

func createGraph(comm [][]int, n int) [][]int {
    for i := 0; i < n; i++ {
        for j := 0; j < n; j++ {
            if comm[i][j] >= 3 {
                comm[i][j] = 1
            } else {
                comm[i][j] = 0
            }
        }
    }
    return comm
}

func solution(comm [][]int, n int) {
    g := createGraph(comm, n)
    fmt.Println(g)
    marked = make([]bool, n)
    count = 0

    for i := 0; i < n; i++ {
        if marked[i] == false {
            DFS(i, g)
            count++
        }
    }
}

3. Second Problem

第三題對我來說是這幾道題目中最難理解的一道題目。下面我們來看看吧。
 
 
 

3.1 題目描述

現有一個圓形花園共有n個入口,現在要修一些路,穿過這個花園。要求每個路口只能有一條路,所有的路均不會相交,求所有可行的方法總數。輸入輸出如上圖所示。

3.2 解答

這道題目,我真的是光完全理解題目就花了好久,側面反映出我的智商有問題,還需要好好磨練。這道題是使用的是動態規划的思路,下面我來解答以下這道題目的思路。
 
我們先給每個入口從1開始編號,先假設6個入口的情況。對於1號口,可以連接的入口有 2,3,6號。
 
如果連接1,2,6個點的問題會被分解為2個點和4個點的問題。為什么?看下面的圖,是不是很清晰。所以這種情況下的方法總數是F(2) *  F(4)。為什么是乘法,因為這里不是獨立的關系,是先定兩個點,在這個基礎上在計算4個點的情況。

如果連接1,3,子問題的划分和上述討論是類似的。因此總數也等於F(2) * F(4)

但是連接1,6的話,情況就不同了。這里1,6間的路把整個花園分成了兩個部分,每個部分只有兩個口,因此方法總數是F(2) * F(2)

最后六個點的情況就等於以上幾種情況相加,因此F(6) = F(2)*F(4) + F(2)*F(4) + F(2)*F(2) = 1*2 + 1*2 + 1 = 5
按說應該在多討論幾種情況,才能得出遞推公式,但是時間有限,這里我就直接給出來:
對於第二項,F(i)*F(j),i+j = n-2。
//f[2m] = 2*f[2m-2]*f[2] + f[c1]*f[c2], c1+c2 = 2m-2
func solution(n int) int {
    m := make([]int, n+1)
    m[0] = 0
    m[2] = 1
    for i := 4; i <= n; i = i + 2 {
        tmp := 0
        for j := 2; j <= i-2; j = j + 2 {
            tmp += m[i-2-j] * m[j]
        }
        m[i] = 2*m[i-2]*m[2] + tmp
    }
    return m[n]
}

4. Third Problem

4.1 題目描述

 

4.2 解答

這道題應該是這四道題目,最簡單的一道了,沒有任何特殊的技巧。關鍵是理解題目的意思,和我們通常玩的2048不同,這里進行一次操作,整個矩陣都會朝目標方向移動,並且相鄰會碰撞的兩個數字會合並,並且兩個數字只會觸發一次合並,且優先合並移動方向頂端的位置。
 
對於行[ 2 2 2 2 ],向右移動后,該行變為,[ 0 0 4 4 ]。那么是如何變成這樣的呢?
 
先進行合並操作,a[3] = a[3] + a[2],a[1] = a[1] + a[0]。之后行變為[ 0 4 0 4]。之后進行右移操作,即a[2] = a[1], a[1] = 0。 右移之后,行變為[ 0 0 4 4 ]。
func Up(board [][]int) {
    for i := 0; i < 4; i++ {
        board[0][i] += board[1][i]
        board[2][i] += board[3][i]
        board[1][i] = board[2][i]
        board[2][i] = 0
        board[3][i] = 0
    }
}

func Down(board [][]int) {
    for i := 0; i < 4; i++ {
        //
        board[1][i] += board[0][i]
        board[3][i] += board[2][i]
        //
        board[2][i] = board[1][i]
        board[0][i] = 0
        board[1][i] = 0
    }
}

func Left(board [][]int) {
    for i := 0; i < 4; i++ {
        board[i][0] += board[i][1]
        board[i][2] += board[i][3]

        board[i][1] = board[i][2]
        board[i][2] = 0
        board[i][3] = 0
    }
}

func Right(board [][]int) {
    for i := 0; i < 4; i++ {
        board[i][3] += board[i][2]
        board[i][1] += board[i][0]

        board[i][2] = board[i][1]
        board[i][1] = 0
        board[i][0] = 0
    }
}

func solution(oper []int, board [][]int) [][]int {
    for i := 0; i < len(oper); i++ {
        switch oper[i] {
        case 1:
            Up(board)
        case 2:
            Down(board)
        case 3:
            Left(board)
        case 4:
            Right(board)
        }
    }

    return board
}

5. Forth Problem

5.1 問題描述

 
 
在漫天的星空里散落着一些糖果,他們各有各的甜度。有一些糖果之間會按照一定的規則有橋梁連接,好讓你獲得了這個糖果之后,可以去獲得和該糖果相連的其他糖果。現在讓你從一個糖果出發,去盡可能多的獲取糖果。
我們將糖果編號 1 ... ... n。每個糖果的甜度記為a[i]。若糖果i和j的甜度的最大公約數>1。則糖果i和j之間有橋梁連接。

5.2 解答

這道題也是使用DFS進行求解,每個糖果是圖的一個節點。滿足約束條件的結點間有邊相連。這道題實質上是求擁有最多節點的連通子圖的節點個數。因此把第一個問題的代碼修改以下就OK了。關鍵是設置一個變量max去存儲最大值,和變量c1去存儲當前連通子圖的節點個數。
func createGraph(sweets []int) [][]int {
    n := len(sweets)
    graph := make([][]int, n)
    for i := 0; i < n; i++ {
        graph[i] = make([]int, n)
        for j := 0; j < n; j++ {
            graph[i][j] = 0
        }
    }

    for i := 0; i < n; i++ {
        for j := i + 1; j < n; j++ {
            if GCD(sweets[i], sweets[j]) > 1 {
                graph[i][j] = 1
                graph[j][i] = 1
            }
        }
    }
    return graph
}

var marked []bool
var count, max int

//gcd(x, y) = gcd(y, x%y)
func GCD(x, y int) int {
    for y != 0 {
        r := y
        y = x % y
        x = r
    }
    return x
}

func DFS(v int, g [][]int) {
    count++
    marked[v] = true
    adjs := getAdjs(v, g)

    for _, adj := range adjs {
        if marked[adj] == false {
            DFS(adj, g)
        }
    }
}

func getAdjs(v int, g [][]int) []int {
    var adjs []int
    for i, elm := range g[v] {
        if elm != 0 {
            adjs = append(adjs, i)
        }
    }
    return adjs
}

func solution(n int, sweets []int) {
    graph := createGraph(sweets)
    marked = make([]bool, len(sweets))
    for i := 0; i < n; i++ {
        if marked[i] == false {
            count = 0
            DFS(i, graph)
            if count > max {
                max = count
            }
        }
    }
}

6. 總結

這次筆試的慘敗,反映出我數據結構和算法的薄弱(要達到對於任何數據結構的代碼都爛熟於心的程度)。其次,是秋招准備的不充分。最后是智商的局限。牛客上很多人都AC了2道或者3道題目。總之任重而道遠。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM