這道題目來源於我正在尋找實習機會的弟弟,他筆試的時候發揮不是很好,這道題目一直超時,就把題目拿過來問我有沒有比較高效的做法。廢話不多說,直接看題目吧
題目描述
有一款叫做空間回廊的游戲,游戲中有着n個房間依次相連,如圖,1號房間可以走到2號房間,以此類推,n號房間可以走到1號房間。

這個游戲的最終目的是為了在這些房間中留下盡可能多的烙印,在每個房間里留下烙印所花費的法力值是不相同的,已知他共有m點法力值,這些法力是不可恢復的。
小明剛接觸這款游戲,所以只會耿直的玩,所以他的每一個行動都是可以預料的:
-
一開始小明位於1號房間。
-
如果他剩余的法力能在當前的房間中留下一個烙印,那么他就會毫不猶豫的花費法力值。
-
無論是否留下了烙印,下一個時刻他都會進入下一個房間,如果當前位於i房間,則會進入i+1房間,如果在n號房間則會進入1號房間。
-
當重復經過某一個房間時,可以再次留下烙印。
很顯然,這個游戲是會終止的,即剩余的法力值不能在任何房間留下烙印的時候,游戲終止。請問他共能留下多少個烙印。
輸入要求
輸入第一行有兩個正整數n和m,分別代表房間數量和小明擁有的法力值。(1<=n<=100000,1<=m<=10^18)
輸入第二行有n個正整數,分別代表1~n號房間留下烙印的法力值花費。(1<=a_i<=10^9)
輸出要求
輸出僅包含一個整數,即最多能留下的烙印。
算法效率要求
具體要求不太清楚,但是肯定是有限制的,因為我弟弟在做的時候一直超時。因此我們就盡可能的找出時間效率高的做法。
輸入輸出案例
# 輸入
4 21
2 1 4 3
# 走過兩圈,在每個房間各留下兩個烙印后,跳過房間1,在房間2多留下一個烙印,因此輸出為 9
輸入輸出不是算法題的精髓,我不打算去處理這個輸入格式,直接簡化一下題目:
編寫一個函數 mark(energy, rooms),energy 為小明擁有的法力值,rooms 為房間隊列,每個元素代表一個房間,值為房間留下烙印所需要消耗的法力值,函數輸出小明最終能留下的烙印數。
解析
首先我們不考慮時間與空間的復雜度,最直觀、最簡單的算法,就是老老實實繞着這個圈一個一個的走房間,直到一整圈下來沒有留下烙印或者法力值已經用完。實現起來非常簡單:
// 算法 v1.0
function mark (energy, rooms) { let result = 0 let marked, i const n = rooms.length while (true) { marked = 0 for (i = 0; i < n; i++) { if (energy >= rooms[i]) { result++ marked = 1 energy -= rooms[i] } } if (!marked || energy === 0) { break } } return result }
上面就是最基礎的 v1.0 版本的算法了,很明顯,這個算法效率非常一般,在大數據量下必然會超時,我們在這個基礎上,一步步的提高算法效率。
這時候問題就來了,優化思路從哪里來?其實做算法題時,有一個很簡單但很有用的思考角度,我們可以擬定一些特殊的輸入數據,從特殊的輸入數據入手:
法力值 energy 非常大
最容易想到的特殊輸入情況就是這個,如果按照算法 v1.0 ,擁有非常大的法力值時,循環次數就會非常大,耗時自然也會很大。那么有沒有方法簡化?當然有。
觀察一下題目和程序,我們很容易發現,在法力值很大時,前面有很多圈留下的烙印數其實是一樣的,可以通過數學求值,並不需要真的進入循環:
// 算法 v2.0
function mark (energy, rooms) { let result = 0 let marked, i const n = rooms.length let sum = 0 for (i = 0; i < n; i++) { sum += rooms[i] } if (energy >= sum) { result += n * parseInt(energy / sum) energy %= sum } while (true) { marked = 0 for (i = 0; i < n; i++) { if (energy >= rooms[i]) { result++ marked = 1 energy -= rooms[i] } } if (!marked || energy === 0) { break } } return result }
根據上面這個算法,在法力值很大的情況下,通過計算完整一圈的法力消耗值和取模,可以減少很多圈不必要的循環,效率自然也就提高了不少。
但是,這樣就夠了嗎?
我們再從另一個特殊的輸入數據來看:
小明法力值為 100 萬,房間數量為 1 萬,其中 9999 個房間所需法力值為 1000001,剩下的 1 個房間所需法力值為 1
那么,我們仍然需要為這一個房間跑 100 萬次回廊,這顯然是不能接受的。我們來看看這種情況如何優化
房間法力值相差很大
當房間法力值相差很大時,我們可以注意到,其實大部分所需法力值比較大的房間在比較后面的循環中根本不需要考慮,因為它已經超過了當前法力值,每一圈之后,一圈消耗的法力值都有可能變更。很顯然,我們可以改進一下算法,每一圈都計算新的 sum 值,並對剩余法力值取模:
// 算法 v3.0
function mark (energy, rooms) { let result = 0 let i, marked, sum, lastResult while (true) { // 重置變量 marked = false sum = 0 lastResult = result for (i = 0; i < rooms.length; i++) { if (energy >= rooms[i]) { result++ marked = true // 標記一圈下來是否有留下烙印 sum += rooms[i] // 計算一圈下來消耗的總法力值 energy -= rooms[i] } } // 每一圈后根據消耗法力值對剩余法力值取模並更新烙印數 if (energy >= sum) { result += (result - lastResult) * parseInt(energy / sum) energy %= sum } // 當一圈下來沒有房間留下烙印或法力值為空,則游戲結束 if (!marked || energy === 0) { break } } return result }
到這里,這個算法其實已經有點樣子了,大部分情況下都可以表現的不錯,那還有沒有可以優化的地方?答案是有的,當然這個地方不是很容易想到。
壓縮數據
思考一個點,在回廊中行走時,我們真的有必要每個房間都走嗎?其實並不需要,事實上,在上一輪走回廊的過程中,我們已經知道了哪些房間已經超過當前剩余法力值,完全可以將其剔除。因此,我們可以維護一個新的房間列表,來儲存實際上需要進入的那些房間,這樣,房間列表的長度會越來越小,效率自然也就越來越高。
// 算法 v4.0
function mark(energy, rooms) { let result = 0 let i, j, marked, sum, lastResult while (true) { // 重置變量 marked = false i = 0 j = 0 sum = 0 lastResult = result for (j = 0; j < rooms.length; j++) { if (energy >= rooms[j]) { result++ marked = true // 標記一圈下來是否有留下烙印 sum += rooms[j] // 計算一圈下來消耗的總法力值 energy -= rooms[j] rooms[i++] = rooms[j] } } // 每一圈后 // 1. 根據消耗法力值對剩余法力值取模並更新烙印數 // 2. 更新房間長度 if (energy >= sum) { result += (result - lastResult) * parseInt(energy / sum) energy %= sum rooms.length = i } // 當一圈下來沒有房間留下烙印或法力值為空,則游戲結束 if (!marked || energy === 0) { break } } return result }
現在算法看起來就很完美了,那還有沒有優化的地方?
沒有了,我真的一滴都不剩了(污
最后
算法很多人第一印象就是難,尤其是前端同學,聞算法色變,但是算法是很重要的,從算法、編譯原理、計算機網絡到編譯原理等基礎知識,決定了你在技術這條路上的上限,還是需要好好學習的。算法難歸難,用心去學習整理,還是有規律可循的。
關於這道題目的完整思路就是這些了,如果你對於上面的解法有新的想法或者你有更好的解決方案,歡迎評論區留下你的見解。
