參加了個(gè)算法比賽,真是一言難盡啊

hello大家好呀,我是小樓。

上周參加了一個(gè)區(qū)的程序員技能比賽的初賽,其實(shí)就是算法比賽,雖然最后結(jié)果是過(guò)了初賽,但過(guò)程真是一言難盡啊。

這次的算法比賽和ACM非常類似,雖然我大學(xué)是數(shù)學(xué)專業(yè),雖然大學(xué)也修過(guò)ACM這門課,但是我的算法是真的不行,很菜很菜的那種。


好在這次比賽是組(抱大腿)隊(duì)模式,3人一組,3個(gè)小時(shí)時(shí)間,一共7道算法題,1入門,2簡(jiǎn)單,2中等,2困難。

10分鐘寫出入門題,但...

由于我知道我比較菜,所以比賽一開(kāi)始,我就挑了一個(gè)看起來(lái)最簡(jiǎn)單的題目做,難題交給隊(duì)友。

結(jié)果是3個(gè)小時(shí)過(guò)去,這個(gè)看起來(lái)最簡(jiǎn)單的題目,愣是沒(méi)有做出來(lái),下面就結(jié)合這道題講講我的心路歷程。

這道題的描述是這樣的:

看起來(lái)文字很多,其實(shí)要表達(dá)的很簡(jiǎn)單,就是輸入一些成績(jī),每個(gè)成績(jī)輸進(jìn)去時(shí),如果超過(guò)全班最好成績(jī)則輸出prefect,如果超過(guò)自己的最好成績(jī)則輸出great,如果沒(méi)超過(guò)自己最好成績(jī)則輸出bad。

是不是很簡(jiǎn)單?用一個(gè)max變量保存全班最好成績(jī),用一個(gè)map保存每個(gè)人的最好成績(jī),不就解決了嗎?

不過(guò)這是我第一次用這個(gè)oj系統(tǒng),連用戶都是剛注冊(cè)的,所以我還特地看了一會(huì)輸入輸出的demo,這次比賽只能使用ACM的輸入輸出模式,例如如果用的是Go語(yǔ)言,輸入輸出應(yīng)該是這樣:

學(xué)會(huì)了輸入輸出之后,一口氣寫入如下的解法:

package main

import (
    "fmt"
)

func main() {
    var n int
    var name string
    var x float32
    var max float32
    scores := make(map[string]float32, n)

    fmt.Scan(&n)
    for i := 0; i < n; i++ {
        fmt.Scan(&name, &x)

        if x > max || i == 0 {
            fmt.Println("perfect")
            max = x
            scores[name] = x
        } else {
            if s, ok := scores[name]; ok {
                if x > s {
                    fmt.Println("great")
                    scores[name] = x
                } else {
                    fmt.Println("bad")
                }
            } else {
                fmt.Println("great")
                scores[name] = x
            }
        }
    }
}

在我正得意,覺(jué)得這題10分鐘就能解決的時(shí)候,提交上去的代碼竟然超時(shí)了,在比賽時(shí)沒(méi)有截圖,提交后顯示有少數(shù)用例超過(guò)了2秒,oj的判定原理是準(zhǔn)備一堆測(cè)試用例,如果全部通過(guò)則判定為通過(guò),當(dāng)然這批測(cè)試用例肯定不是那么好通過(guò)的,設(shè)計(jì)者會(huì)出各種極端的case。

優(yōu)化map性能

調(diào)轉(zhuǎn)頭去仔細(xì)審題,果然時(shí)間和空間都有限制:

  • 時(shí)間限制:C/C++ 1秒,其他語(yǔ)言2秒
  • 空間限制:C/C++ 262144K,其他語(yǔ)言524288K
  • 64bit IO Format: %lld

這題除了有map的讀寫,其他都是O(1)復(fù)雜度,性能不夠難道是map性能不夠?

如果是map的性能不夠,不夠在哪里呢?眾所周知,map的原理一般是這樣:

當(dāng)一些key存入map時(shí),會(huì)先對(duì)key計(jì)算hash值,在map中找到對(duì)應(yīng)的hash槽,這個(gè)槽之后一般是個(gè)鏈表(有的語(yǔ)言也會(huì)做一些優(yōu)化成樹(shù)狀,這里我們簡(jiǎn)化為鏈表),因?yàn)椴煌膋ey的hash值可能會(huì)重復(fù)(沖突),沖突了只能把key排成一個(gè)鏈表,每次查找時(shí)都要遍歷鏈表。

所以有沒(méi)有可能,設(shè)計(jì)者給出了一堆hash值重復(fù)的name,數(shù)量又多,導(dǎo)致每次插入、查找時(shí)都要遍歷鏈表,性能下降,導(dǎo)致超時(shí)?

于是再仔細(xì)審題,我發(fā)現(xiàn)輸入的姓名和成績(jī)是有限制的:

  • name保證長(zhǎng)度不超過(guò)6,僅由小寫英文字母組成,每個(gè)名字代表唯一一個(gè)同學(xué)
  • x為1位小數(shù),0≤x≤300

name最長(zhǎng)為6,且為小寫字母,這點(diǎn)給了我一點(diǎn)啟發(fā),能不能讓查詢map變成O(1)復(fù)雜度?

顯然可以的,小寫字母范圍為a~z,如果看成數(shù)字就是1-26,也就是27進(jìn)制,所以每個(gè)name可以表示為一個(gè)27進(jìn)制的數(shù),這樣就可以把所有人的成績(jī)放到一個(gè)大數(shù)組里去,按name的27進(jìn)制進(jìn)行O(1)的查找。

為什么是27進(jìn)制而不是26,因?yàn)閚ame沒(méi)說(shuō)是多少位,比如只有5位,那空出的一位怎么表示?只能用0表示了,a-z就是1-26,合起來(lái)是27進(jìn)制。

參考10進(jìn)制計(jì)算法則,27進(jìn)制應(yīng)該這樣計(jì)算(以roshi為例):

計(jì)算出的值即為數(shù)組的下標(biāo),那么這個(gè)數(shù)組的最大值是多少呢?

26 * 27^5 + 26 * 27^4 + 26 * 27^3 + 26 * 27^2 + 26 * 27^1 + 26 * 27^0

很容易算出來(lái)是:

387420488

需要這么大個(gè)數(shù)組,大概3億多,輸入成績(jī)是個(gè)1位小數(shù),可以轉(zhuǎn)換為int,大概4個(gè)字節(jié),掐指一算得 1513361KB,好像比要求的524288K多,先不管空間,寫一版跑跑看,萬(wàn)一能過(guò)呢?

很簡(jiǎn)單寫出代碼:

package main

import (
    "fmt"
)

func main() {
    var n int
    var name string
    var x int32

    fmt.Scan(&n)

    var scores [387420488]int32
    var exist [387420488]int32

    var max int32
    for i := 0; i < n; i++ {
        fmt.Scan(&name, &x)

        idx := mapIndex(name)

        if x > max || i == 0 {
            fmt.Printf("perfect\n")
            max = x
            scores[idx] = x
            exist[idx] = 1
        } else {
            if exist[idx] == 0 || x > scores[idx] {
                fmt.Printf("great\n")
                scores[idx] = x
                exist[idx] = 1
            } else {
                fmt.Printf("bad\n")
            }
        }
    }
}

var index27 = [6]int32{1, 27, 27 * 27, 27 * 27 * 27, 27 * 27 * 27 * 27}

func mapIndex(x string) int32 {
    var index int32
    for i := len(x) - 1; i >= 0; i-- {
        index = index + int32(x[i]-96)*index27[len(x)-1-i]
    }
    return index
}

結(jié)果竟然報(bào)錯(cuò)了,我當(dāng)時(shí)不理解,事后理解了,我們暫且不說(shuō),后面會(huì)說(shuō)到原因。

就是因?yàn)檫@個(gè)報(bào)錯(cuò)不明不白,明明能測(cè)試通過(guò),到底哪里理解有偏差?亦或是內(nèi)存超了?

優(yōu)化內(nèi)存占用

上面的代碼用到了2個(gè)數(shù)組,一個(gè)存最大值,一個(gè)存值是否存在,一個(gè)數(shù)組是1513361KB,2個(gè)就是3026722KB,是最大內(nèi)存限制的5.7倍

var scores [387420488]int32
var exist [387420488]int32

exist數(shù)組可以用boolean類型,分?jǐn)?shù)最大值0<=0<=300,int16足矣

大小 范圍
int8 1字節(jié) -128 ~ 127
int16 2字節(jié) -32768 ~ 32767
int32 4字節(jié) -2147483648 ~ 2147483647

如果是這個(gè)組合,將占用 1135020KB,是上限的2倍多,還是有點(diǎn)超,先試試:

還是一樣,難道是我算法有問(wèn)題?沒(méi)道理啊。到這里我實(shí)在是沒(méi)招了,3小時(shí)也耗盡了,比賽結(jié)束。

賽后思考

賽后,我拿著這道題去找了一位剛?cè)肼氉止?jié)的朋友,想著剛?cè)プ止?jié)應(yīng)該刷過(guò)不少題吧,果然大佬就是大佬,給出了一個(gè)有新意的思路,用前綴樹(shù)做:

每一個(gè)name都構(gòu)造出一個(gè)前綴樹(shù),查找時(shí)最多只需要查找6次,內(nèi)存使用應(yīng)該也不會(huì)太多,算是時(shí)間與空間的一個(gè)平衡。

大佬還補(bǔ)充了一句:比賽還是比較特殊的,可能就是某一個(gè)case卡主了,而你要做的就是如何能把這個(gè)特殊的case也ac掉。

大佬的話似乎很有道理,于是我寫了一個(gè)前綴樹(shù)的版本:

package main

import (
    "fmt"
)

type treeNode struct {
    max  float32
    next [26]*treeNode
}

func main() {
    var n int
    var name string
    var x float32
    fmt.Scan(&n)

    var max float32
    tree := new(treeNode)
    for i := 0; i < n; i++ {
        fmt.Scan(&name, &x)

        if x > max || i == 0 {
            fmt.Println("perfect")
            max = x
            insert(tree, name, x)
        } else {
            if tmp := searchAndStoreMax(tree, name, x); tmp != -1 {
                if x > tmp {
                    fmt.Println("great")
                    insert(tree, name, x)
                } else {
                    fmt.Println("bad")
                }
            } else {
                fmt.Println("great")
                insert(tree, name, x)
            }
        }
    }
}

func insert(node *treeNode, name string, x float32) {
    for i := 0; i < len(name); i++ {
        idx := int32(name[i] - 'a')
        if node.next[idx] == nil {
            node.next[idx] = new(treeNode)
        }
        node = node.next[idx]
    }
    node.max = x
}

func searchAndStoreMax(node *treeNode, name string, x float32) float32 {
    for i := 0; i < len(name); i++ {
        idx := int32(name[i] - 'a')
        if node.next[idx] == nil {
            return -1
        }
        node = node.next[idx]
    }
    if x > node.max {
        tmp := node.max
        node.max = x
        return tmp
    }
    return node.max
}

結(jié)果又又又是超時(shí),我服了。

終于發(fā)現(xiàn)問(wèn)題

后來(lái)我又嘗試了很多方法都不行,比如懷疑是不是Go的map性能不行,換成Java試試,結(jié)果還是不行。

最后我在網(wǎng)上搜索??途W(wǎng)時(shí)發(fā)現(xiàn)了一個(gè)突破口(對(duì),沒(méi)錯(cuò),這次比賽是在牛客網(wǎng)上舉辦的)。

簡(jiǎn)單說(shuō),??途W(wǎng)的ACM模式輸入可能需要讀入一行然后再自己處理成想要的數(shù)據(jù)。

抱著懷疑的態(tài)度我試了下,果然,淦!用最開(kāi)始的map就能ac掉!雖然我也不知道這兩種輸入有什么區(qū)別。關(guān)鍵我還是用的網(wǎng)站上提示的輸入方式,確實(shí)太坑了。

正確的輸入方式如下:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func main() {
    var n int
    var name string
    var x float64
    input := bufio.NewScanner(os.Stdin)
    if input.Scan() {
        n, _ = strconv.Atoi(input.Text())
    }

    scores := make(map[string]float64, n)
    var max float64
    for i := 0; i < n; i++ {
        if input.Scan() {
            arr := strings.Split(input.Text(), " ")
            name = arr[0]
            x, _ = strconv.ParseFloat(arr[1], 32)
        }

        ...
    }
}

之前的想法屬于強(qiáng)行增加難度了~害!想了好幾天的題竟然敗在了輸入上,真是一言難盡!

之前的方法能行嗎

我把幾個(gè)版本的輸入改了之后,看看通過(guò)后的耗時(shí)和內(nèi)存

版本 是否通過(guò) 耗時(shí) 內(nèi)存
map版 315ms 10096KB
27進(jìn)制版 - -
前綴樹(shù)版 433ms 43720KB

其中27進(jìn)制版本在改成正確的輸入后,露出了廬山真面目:內(nèi)存超了!

最后

過(guò)程雖然曲折,但最終還是解決了這個(gè)入門題,而且還嘗試著用幾種方法來(lái)解,雖然不盡如人意,但終究還是有點(diǎn)收獲。

當(dāng)然我們組的小伙伴也很給力,做出來(lái)3道題,我們最終的成績(jī)是排名進(jìn)了前10%,雖然我只貢獻(xiàn)了一點(diǎn)點(diǎn)(沒(méi)完全做出來(lái)也有得分,按通過(guò)的用例算,我這題大概拿到了90%的分),也算是可以了,而且還有一道題也可能是因?yàn)檫@個(gè)輸入被卡了,所以如果這兩道卡的題都做出來(lái),估計(jì)排名能進(jìn)前三。

初賽算是過(guò)了,接下來(lái)準(zhǔn)備復(fù)賽,如果復(fù)賽還有好玩的事情,我再來(lái)寫一篇文章,哈哈。

這一言難盡的比賽,大家給個(gè)贊鼓勵(lì)下吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 導(dǎo)讀:2019年的5月,所有娛樂(lè)圈明星們的熱搜,都不及賭王兒子何猷君求娶奚夢(mèng)瑤的熱搜來(lái)得勁爆;2020年的5月,所...
    婪卿閱讀 265評(píng)論 0 0
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,995評(píng)論 0 11
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 34,625評(píng)論 18 399
  • 小康是我的一個(gè)客戶。她和丈夫都是做銷售工作的,就是給公司推銷快消品的。 兩人都是從農(nóng)村出來(lái)的,因?yàn)樵谝黄鸸ぷ饔辛烁?..
    塞上有丹橘閱讀 275評(píng)論 1 7
  • 第一部分 Python基礎(chǔ)篇(80題) 1、為什么學(xué)習(xí)Python? Python相對(duì)于其他編程語(yǔ)言有很多優(yōu)點(diǎn): ...
    清清子衿木子水心閱讀 1,825評(píng)論 0 1

友情鏈接更多精彩內(nèi)容