## b332: 賽伊法則 ### 實作模擬題 上面的廢話可以跳過跳到下面。 讀完題目可以發現要做的事情事先模擬一個圓環,每顆鍵會有不同分數由 `a~z` 表示,每次刷鍵有分左右手和順時針逆時針。 首先要做的事情是先把字母換成正常分數。 基本上方法很多,可以拿 `ascii` 碼轉換,或是你直接特判下去也都可以,我個人方法是長這樣。 ![image](https://hackmd.io/_uploads/SJAMgY-kZx.png) 原理就是 `ascii` 字母會有自己的代碼,在已知只會出現小寫的情況字母會照順序遞增,已知只會出現小寫的情況字母會照順序遞增,然後一個有用的知識是把 `char` 的東西直接強制換成 `int` 會輸出他的 `ascii` 碼,所以只要把現在的字母減掉 `a` 那他現在轉換的數字就會表示是 `a` 後面的第幾個字母,最後只要再加一把數字補回去就完成了。 --- 把一開始的分數處理完,接下來就是如何模擬好這題了。 大方向其實很單純:但這題很多人遇到的問題是:手的位置不是每回合都回到 1。(雖然我考試的時候有一直講) - 初始時兩隻手都是 `1` (`pL=1`、`pR=1`),但每回合結束後,該手的位置要真的往前/往後走 `d` 格(取 `mod 8` )。 - 得分只算「最多一圈」(`t = min(8, |d|)`),但位置一定要照 d 的真實移動去更新,用 `d%8` 模擬不要真的跑完,不然會吃TLE(有人真的這樣寫)。 --- ### 大概流程 1) 準備分數陣列與整圈總和 - 8 顆鍵各自的分數:`val[1..8]`(前面用 ASCII 轉好的那個) - 一圈總和:`ring_sum = sum(val[1..8])` (方便算) --- 2) 兩隻手的狀態要記三件事(左右手各自獨立) - 目前位置 `pL / pR`(初始 1) - 上一次的方向 `dirL / dirR ∈ {-1,0,+1}`(初始 0) - combo 值 `comboL / comboR`(初始 1) --- 3) 來一回合 (h, d) 怎麼加分 - 如果 `d=0`:本回合加 `0` 分、combo/方向/位置全部不動(就是啥都不要動) - 否則: - 方向 `sgn` = `sign(d)` - 會「踩幾顆」`t = min(8, |d|)`(最多只算一圈的分) - 從該手「現在的位置」出發,往 `sgn` 方向走 t 步(一步一步加): - 注意:起點「當前那格」不計分,第一步是「移動到下一格」才開始算 - 如果 `t==8` (也就是 |d|≥8),這回合分數可以直接拿 `ring_sum`,不用逐格加 - 如果 `t<8`,就真的走 `t` 步、把走到的格子分數加起來 - combo 規則: - 如果 `sgn` 和該手的 `last_dir` 一樣:combo++ - 否則:`combo=1`,`last_dir=sgn` - 本回合得分 = 原始路徑分數 × combo - 最後,該手「位置」要真的移動 `d` 格: - 位移只需要用 d%8 的等效步數來更新位置: - :`mv = ((d % 8) + 8) % 8; pos = int(((pos - 1) + mv) % 8) + 1;` --- ### 為什麼要這樣做 - 「得分只算一圈」是題目限制,但實際上模擬手真的移動了那麼多格,所以「位置」還是要照實際的 `d` 來動。 --- ### 小總結 - `d=0`:分數 0、combo/方向/位置全部不動 - |d|≥8:分數直接用 `ring_sum`,但位置還是要照 `d%8` 更新 - 逐步加分時,記得是「先移動再計分」,起點那格不算 - 分數很大,記得用 long long --- ## b265: 同時身為mai眾和vt豚的我,注定是要破產一輩子的(._.`) 題意一句話可以解釋 - 每天要嘛付 `Si = Ai + Bi`,要嘛花一次能力把它壓成 `Mi = min(Ai, Bi)`;最多只能選 K 天用能力。 - 目標讓「最後所有天裡的最大那一天」越小越好。 --- 兩種解法(原本我是想出成一定要二分搜,然後考前發現可以用超簡單的純貪心過:P) #### 本題解中的 Si 表示第 i 天的原始花費,Mi 表示第 i 天用能力後的最低花費 --- ### A) 二分搜答案 + 線性檢查 --- - 先建立一個直覺:答案 `M` 其實就是「我希望每天最多能花到多少錢,讓整體都能塞進這個上限裡」,我們要把這個上限壓到最小。 - 做法:先「猜」一個門檻 `T`(例如 `0~max Si` 之間),問自己:「如果規定每天最後都不能超過 `T`,我有沒有辦法在最多 `K` 天使用能力的限制下做到?」 - 檢查的判準(逐天看過去): 1) 如果某天的「最低可能」`Mi` 都已經大於 `T(Mi > T)`: - 意思是:「就算我當天用了能力,也壓不進 `T`」→ 這個 T 完全不可能,直接判定不行。 2) 否則,如果「不降的花費」`Si` 大於 `T(Si > T)`而 `Mi ≤ T`: - 代表:「這一天非降不可」,因為不降就會爆掉。那我就把 need++(表示今天一定要用一次能力)。 3) 如果 `Si ≤ T`: - 代表今天不降也沒事,就先不降。 - 全部掃完,如果「必須要降的天數 need」不超過 `K(need ≤ K)`→ 代表 `T` 是可行的,反之不可行。 - 為什麼可以二分?因為 T 越大,越容易可行(單調性)。可行區間是一段連續的「大 T」,我們要找最小可行的那個 T。 - 二分邊界怎麼抓?: - 下界 `lo` = `max_i Mi`(因為每天最小也只能降到 `Mi`,再怎樣 `M` 也不能小於這個) - 上界 `hi` = `max_i Si`(全部不降時的最大值) - 具象一點的想法: - 「今天我把天花板設定在 `T`。」 - 「哪些天就算降也塞不進天花板?」→ `Mi` > `T`(這題 `T` 根本不行) - 「哪些天不降就會撞到天花板?」→ `Si` > `T`(這些天我非用能力不可) - 「最後我需要用幾次能力?」→ need - 「手上次數 `K` 夠不夠?」→ 夠就可行,不夠就不行 - 調整 `T` 再試一次,往「更小的可行 `T`」逼近。 - 一些零碎code: - `if (Mi > T) return false;` - `need += (Si > T); // 只要 Si 超過 T,就把它列為必須降一次` - 這整套邏輯的核心就是:把「複雜的選擇」轉成「只要塞進門檻 T 就算成功」,而塞不進去的那些天就是「必須操作的天」。因為單調性很明顯,二分就會自然收斂到最小可行的 T。 --- ### B) 純貪心(最大堆) - 先把每一天的 `(Si, Mi)` 都存到一個陣列(例如 `vector<pair<long long, long long>> days;`),記下 index。 - 開一個最大堆(`priority_queue<pair<long long,int>>`),把每一天的 `Si` 和 index 丟進去。 - 挑 K 個最大值:每次堆出來看到的就會是 sum 最大的,同時也可以獲得前 `K` 項最大的值的 index 那就再回去 vector 看把那幾項比較大的值減掉,那就會是我們要的最佳策略了。 - 零碎code:「`pq.push({Si, i});`」、`if (pick[i]) ans = max(ans, Mi); else ans = max(ans, Si);` --- - 看完可能有點卡,另一個更直覺的作法白話點就是說:我先開一個 `vector` 的 `pair` 陣列存每天的兩個值,接著用 `priority_queue` 包 `pair` 一邊存每天的 sum 跟那天的 index 可以很直覺的判斷到,我們使用能力應該要全部使用在總和前 `K` 大的那幾天,這樣存可以很輕易的找到哪幾天要使用能力,再回去把值減掉答案就出來了。再更白話點就是把所有能力丟在最大值那幾天,最後一掃最大值就是答案。 --- ### 邊界 tips - K=0:答案就是 `max Si` - K=N:答案就是 `max Mi` - `Si == Mi`:降不降都一樣 - 數值上限到 1e9,記得用 long long --- ### 複雜度 - 二分:O(n log C)(C 大概到 2e9) - 堆貪心:O(n log n) ---