美團杯2020 簡要題解


update:忘記寫測試賽題解了,雖然只有一道題(

  • 測試賽A

考慮最后的序列長什么樣子。顯然它的長度是 \(2^n-1\),並且關於第 \(2^{n-1}\) 個字符是左右對稱的。將中間的字符去掉之后,兩邊就都成了原串的后 \(n-1\) 個字符如此操作后的樣子。因此,設 \(f_{i, j}\) 表示前 \(i\) 個字符中,以 \(j\) 結尾的不同子序列個數,轉移的時候我們令所有相同的子序列在它最后一次出現的位置被取到,因此 \(f_{i+1, s_{i+1}}=\sum_{j\in \mathrm{alphabet}} f_{i, j}, f_{i+1, k}=f_{i, k}\)。由此可以寫出轉移矩陣,直接從后往前倍增地轉移即可。


由於我水平有限(雖然兩位隊友都穩得不行),所以並不能寫出全部題目的題解(

題目按照通過人數排序,這樣可以讓咕掉的題盡量排在后面(

  • N

熱身題,隊長說直接手推即可。實在不行就猜幾個上去,實際上可能要猜的很少。

  • A

簽到題,標程的做法是 dp,根據每個位置的字符判斷從哪個方向轉移。實際上,注意到字符串的結構是前面三個 x,后面兩個 l,可以枚舉第三個 x 的位置,只要保證后面的 l 數量不超過 \(1\) 即可,預處理前綴和后可以 \(O(1)\) 判斷。注意要特判 l 總數不超過 \(2\) 的情況。

  • M

注意到一次詢問 \(\{a, b\}\) 的時候,結果只會是 \(1\)\(2\),對應了它們在排列中位置的兩種先后情況。

實際上,這個操作等價於比較兩個元素的大小(指它們在排列中的位置),因此只需要在 \(O(n\log n)\) 次比較內將序列排序即可。如果直接用 sort,由於它在數據比較小時采用了一些 \(O(n^2)\) 次比較的排序方式,在 \(n=100\) 時可能比較次數會超過 \(650\)

一個解決方法是使用 stable_sort 等較穩定的排序方法,或者用我們隊研究的一個奇怪方式:

每次要將一個給定集合排序時,先隨機一個元素作為中間位置,然后只需要區分出每個元素和中間位置元素的大小,就可以分別遞歸。雖然隨機笛卡爾樹的深度是 \(O(\log n)\),但是由於樹的形態並非充分平衡,常數比較大,如果仍然逐個比較會超出限制。但是在比較的時候,每次可以拿出兩個元素,分別放到中間元素的前、后再詢問。如果返回值是 \(1\)\(3\),可以直接確定這兩個元素所屬集合,否則這兩個元素要么都在中間元素前面,要么都在后面。只需要再詢問其中一個即可。可以發現,這樣確定一個元素的位置的期望代價只有 \(\frac{3}{4}\),即可通過本題。

  • B

好像有點丟人啊,我們隊沒人會畫 large 的圖(

small 比較簡單,只需要用 excel 打開,調整列寬后縮放到最小,就可以看到字母了。《大局觀》就是指作為一個整體去看(

large 的意圖可以說比較明確,給出的數字都是秒數,只需要用某些方式將秒數轉化為鍾表的分針和時針,即可看出大致的字母。官方是用 Python 畫出的圖,實際上 cpp 也可以讀寫 PPM 圖像,只不過文件體積會比較……

  • F

small 其實不算難,只需要憑直覺猜出第一行是 #include <iostream> 即可。然后就可以通過換行符、空格、制表符和一些已知的字符替換,猜出 using namespace std;int main() { 等信息。唯一比較難猜的是運算中的 ^,但是可以直接試樣例得到。

large 開頭放了一個迷惑性的 /* this is a comment */,但是如果能看出成對出現的反向字符,猜出注釋也不難。后面的部分其實大同小異,只有兩個問題:weight[] 前的運算符,和數表的每一位對應的值。

由於絕大多數運算符都是單調的,但是答案在沒有取模的情況下有大有小,運算符只能是 ^。最后要確定數表的值,發現 \(0, 1\) 都已經確定好了,只需要爆搜 \(8!\) 個情況。判斷時不需要跑完 \(4000\) 組樣例,只要跑前兩組就可以唯一確定答案了。

  • E

非常有趣的字符串題,雖然我全靠隊友帶(

對於這種本質不同的字符串計數問題,通常都是將某個串在它第一次或最后一次出現時統計。本題中,如果將每個字符串在第一次出現的前綴中(即盡可能縮短用到的前綴長度),那么看起來就不太能做。但是如果將每一個串在最后一次出現的前綴中統計,就會非常簡單:對於一個串,假設它可以表示為 \(s[1\ldots i]+s[j\ldots k]\)\(s[1\ldots i+a]+s[j+a\ldots k]\),那么有 \(s_{i+1}=s_j\)。並且,如果它不能表示為 \(s[1\ldots i+1]+s[j+1\ldots k]\) 的形式,必然也不能表示成 \(s[1\ldots i+a]+s[j+a\ldots k], a>0\),而且有 \(s_{i+1}\ne s_j\)。這也就是說,對於一個串 \(t=s[1\ldots i]+s[j\ldots k]\),它是最后一次出現在某個前綴中,當且僅當 \(s_{i+1}\ne s_j\)。所以我們要做的事情實際上就是對於每個后綴 \(s[i+1\ldots n]\),求出它包含的本質不同子串個數,減掉以 \(s_{i+1}\) 開頭的不同子串個數即可。這個操作可以用 SAM 簡單維護。

  • G

本場比賽中我最喜歡的一道題。

首先將題意抽象一下:對於 \(n+1\) 是質數的 \(n\),需要構造 \((1, p_1), (2, p_2), \ldots, (n, p_n)\),滿足 \(p\) 是一個長度為 \(n\) 的排列,並且任意的 \(i<j\),二元組 \((j-i, p_j-p_i)\) 都是獨一無二的。

貌似有很多神仙看到 \(n+1\) 是質數和 \(p\) 是排列兩個條件,就想到了原根的冪……但是我沒這么強,所以只能用一些比較暴力的辦法:

對比較小的 \(n\),枚舉排列求出字典序最小的解。如果你運氣比較好或者觀察力比較敏銳,就可以發現當 \(n=10\) 時,字典序最小的解恰好是 \(\left(i, 2^{i-1}\bmod (n+1)\right)\)。但是如果這么寫的話,在 \(n=6\) 的時候就會出現重復的數字。這個時候,原根的冪就呼之欲出了;\(n=10\) 的情況只不過是因為 \(2\) 恰好是 \(11\) 的原根而已,但它的字典序在所有解中是最小的,這一點為解題提供了莫大的助力。對於 \(n=m-1\),只需要求出 \(m\) 的原根 \(g\),輸出 \(\left(i, g^{i-1}\bmod m\right)\) 即可。

至於這個做法的正確性證明也非常簡單。設 \(i< j, a\in[1, n-2]\) 時(注意 \(a\) 更大的時候,\(j+a\) 會超過 \(n\)),有 \(g^{i-1+a}-g^{i-1}\equiv g^{j-1+a}-g^{j-1}\pmod m\),也就是說 \(g^{i-1}(g^{a}-1)\equiv g^{j-1}(g^{a}-1)\pmod m\),移項得到 \((g^{i-1}-g^{j-1})(g^a-1)\equiv 0\pmod m\)。由於 \(a\in[1, n-2]\)\(g^a\not \equiv 1\);由於 \(1\leq i<j\leq n\)\(g^{i-1}\not \equiv g^{j-1}\)。因此必然矛盾。

  • I

據某位神仙說是矩陣乘法練習題,然而我們做了 3h(

可以發現 small 中,\(f_0(x)\)\(f_1(x)\) 都可以表示成 \(a\cdot x+b\) 的形式,並且顯然它是具有結合律的。因此,每個函數都等價於它所屬的 \(f_l, f_r\) 對應的一次函數復合后的結果。寫一個矩陣乘法,或者直接維護一次函數都可以。

large 中的 random 函數使用了 xor_shift 來生成隨機數,並且 \(f_0\) 中對 \(a\) 的操作只有 \(\operatorname{xor}\),因此不難想到按位處理貢獻。設 \(g_{i, j}\) 表示 \(x\) 在 xor_shift 迭代一次后,第 \(i\) 位的 \(1\) 對結束后的 \(x\)\(j\) 位的影響(即是否會給第 \(j\)\(\operatorname{xor}\) 上原本的第 \(i\) 位),這個矩陣可以由 xor_shift 的流程直接寫出來,並且任意次 xor_shift 后的結果都可以由這個矩陣的冪來表示。記錄答案時需要另一個矩陣 \(h_{i, j}\),表示在經過這個函數后,初始的 \(x\) 的第 \(i\) 位的 \(1\)\(a\) 的第 \(j\) 位的影響。對於每個函數要維護它的 \(g^k\)\(h\),復合的時候有 \(g=g_l\cdot g_r, h=h_l+g_l\cdot h_r\)。由於矩陣都是 \(0/1\),實際上可以用位運算優化,但是似乎也沒什么用(

PS:很多人說 small 過於有提示性,但是我們隊似乎反而被 small 迷惑了,因為我們覺得不可能兩個 task 都考矩陣乘法,於是一直在找 random 函數的周期(

  • K

本場比賽中我第二喜歡的一道題。不得不說 jls 玩梗水平和出題水平都是一流,把垃圾梗變成了題目中不可或缺的一部分(

考慮從后往前掃描,每次遇到一個字符,就選擇一個串,並將這個字符接到它前面。不難發現,這種條件下,每個串具體是哪些字符並不重要,重要的是這個串已有的字符個數。因此,每個時刻的狀態都可以用一個長度為 \(7\) 的序列來描述每個長度的字符串有多少個。這個時候可以發現,遇到 5 的時候是完全不需要決策的,它能,也只能將長度為 \(2\) 的一個串變為 \(3\)(注意我們是倒着掃描);但是 14 的時候,就會產生決策,從而有可能導向無解的結果。

這個時候再來看 small,由於所有的字符串,它倒着掃描時,開頭的 4 都是最后的 \(m=\frac{|S|}{6}\) 位中的某一個,我們可以直接從 \(|S|-m\) 的位置開始掃描,並把所有字符串的初始狀態設為 \(1\)。這個時候,再遇到 4 就不會產生決策了,只有 1 仍然有決策的可能。但是,在 1 的三種決策中,有兩種決策(\(4\rightarrow 5, 5\rightarrow 6\))進行后,都不會使可選的局面變多。換句話說就是,如果 \(1\rightarrow 2\)1 不選,可能會導致后面的 5 無法匹配,但是另外兩種決策的 1 不選,並不會產生無法匹配的情況。

large 中雖然去掉了這個條件,但是 small 中的貪心提醒我們,如果能創造一種未來可選的局面相對單調的狀態,就可以用這種方式貪心出解。這個時候,有些慧眼如炬的神仙可能已經發現了,4 在任何情況下,它左邊緊鄰着的字符都是 1。這提醒我們,也許可以將 14 先匹配起來,讓它們作為一個字符(不妨稱為 A)整體參與匹配。這樣,需要匹配的串就變成了 1A5A,看起來只要從左往右正着掃,就可以套用 small 里的貪心。

但是,首先需要證明將 14 先匹配起來是對的。考慮一種方案里,每個串里的兩個 14 ,可以發現,對於某個 14\(S\) 中間隔的這一段,對它們所屬的這個串是不能產生任何貢獻的。換句話說,假如它是 514,那么這個 5 不可能來自 14 中間間隔的這一段;114 同理。這樣的話,14 之間的間隔越長,對我們的匹配越不利。因此,不難證明,從右往左考慮每一個 4,向左找到最近的未被匹配的 1 並將它們匹配起來之后,對於任意一個位置,不存在方案,使得這個位置隔斷的 14 數量嚴格更少。

於是,我們就可以將 14 看做一個整體(掛在 14 原來的位置都是可以的,因為在這個串里,它們的中間不可能再選中其他字符)進行匹配。唯一要注意的一點就是,在將 1 變成 114 的時候,應該取當前位置最靠左的 1,以避免它出現在 14 的空檔中;其他的位置同理,都需要取最左邊的。

(很可惜,我們在場上對這道題貪心的各種性質認識還不夠完整,導致出現了一些調試不出來的 bug,最后沒有通過 large)

  • D

在講題的時候,我很難相信這題居然這么簡單。但是由於時間不足、題面較為復雜和榜上較少的通過人數,我們在場上認為這道題超出了我們的能力范圍而沒有仔細研究它,非常可惜。

首先有一個重要的觀察:每方同時最多只有一個魚沒有聖盾。而對於進攻方來說,我們只關心只有負責攻擊的魚有沒有聖盾;因為其他位置的魚,不管當前有沒有聖盾,在這一輪后都會擁有聖盾。同樣地,對於防守方,由於所有魚在此時是等概率受攻擊的,並且除了受到攻擊的魚,其他所有魚在下一輪都會擁有聖盾,我們只關心是否存在沒有聖盾的魚。因此,可以列出狀態 \(f_{n, m, 0/1, 0/1}\),表示當前進攻、防守方各有多少魚,\(01\) 表示雙方的狀態。

轉移時,只需要枚舉一下這次進攻時,選擇的防守方的魚是否擁有聖盾,如果是有聖盾的,是否恰好是下一次防守方用來進攻的魚,每種情況對應的概率都很容易計算。理一下轉移關系可以發現,這些狀態構成了一個包含自環的 “DAG”,解一下每個自環處的方程即可。

  • L

注意到對於 \(01\) 串來說,漢明距離等於歐氏距離的平方,因此只需要按照歐氏距離考慮即可。

因此,這里的問題變成了,求兩個 \(n\) 維空間內的點的歐氏距離的近似值,但是其中一個點的坐標是 \(n\) 過大而無法直接表示的常量。

根據 Johnson-Lindenstrauss Theorem,我們可以在多項式時間內將若干個 \(n\) 維空間內的點映射為若干個 \(k\) 維空間內的點,同時保持它們兩兩之間的歐氏距離滿足 \(d_{\mathrm{new}}\in \left[(1-\epsilon) d_{\mathrm{old}}, (1+\epsilon) d_{\mathrm{old}}\right]\)。具體做法和證明可以自行查閱,簡單敘述就是將這若干個 \(n\) 維向量同時右乘某個 \(n\times k\) 的隨機矩陣並乘上一個和 \(n, k\) 有關的常數。隨機矩陣可以用隨機數生成器來 \(O(1)\) 表述,而我們只需要選擇合適的 \(k\),來保證不超過碼長限制的同時盡可能精確地表達 \(\pi\) 即可。

  • C

手玩!沖就完了!(

實際上,由於玩家的經驗會逐漸豐富,在經過若干次摸索后,很容易就能在 \(10^5\) 步內通過 \(100\) 關,需要的只是耐心,但是顯然正解不能這么做(

要讓 AI 來完成這個任務,首先要會讓可執行文件進行 IO 交互,這點可以通過搜索引擎完成。

然后是 AI 的設計。一種設計是降低重復獲得的鑰匙的價值來進行估價和貪心,但是通過 \(100\) 關大約需要 \(1.5\times 10^5\) 步。

事實上,? 需要盡量優先拿到,而確定的 rgb 的價值顯然更高(因為它們可以作為缺乏某種鑰匙時的備用資源)。並且,由於存在形如 RGB?????# 的塊和出口前 RRRGGGBBB 的門,可以發現我們大部分時間都在最大化擁有數量最少的鑰匙。因此,對於每個狀態和決策的估價,可以基於數量最少的鑰匙的數量;如果當前狀態估價過低,就推倒重來。這種貪心大幅度優化了步數,大約只需要 \(5\times 10^4\) 步即可。當然,期望步數最優的做法是狀壓每個狀態,但是時間復雜度比較高,代碼也比較難寫。

  • J

本場比賽中我第三喜歡的一道題。但是很可惜我場上完全不會做(

首先需要特判 \(x_l=x_r\)\(y_l=y_r\) 的情況:由於在任意一層分形中,都不包含兩個相鄰的 x,且任意一層 o 的最外面一圈 \(0\) 級分形都是 o,所以可以用數學歸納法證明,任意的 \(0\)x 都不與其他 \(0\)x 相鄰。因此,對於這種情況,只需要求出一行/列中 x 的個數和首、尾位置是否是 x,就可以求出 o 連通塊的數量。這個問題是經典的分形,可以直接遞歸解決。

注意到,任意一個 \(0\)o,如果它所在的連通塊大小不小於 \(2\),那么它要么連接在一個 \(1\)o 上,要么本身就是一個 \(1\)o 的一部分。既然這樣,統計大小不小於 \(2\)\(0\)o 連通塊個數就等價於統計 \(1\)o 連通塊個數(注意如果一個 \(1\) 級分形只被圈到了一部分,也要完全統計進去),可以遞歸處理。

剩下的只有每層零散的 \(0\)o。可以發現,這樣的 o 只會在我們切開了某個 \(1\)xo(即 x|o)的時候出現,實際上就是對於四條邊的每一條,如果是完整切開的,就計算這一條邊上的 \(1\)x 數量(因為每一個 \(1\)x 周圍都全是 \(1\)o,所以每個 x 在這個方向都恰有一個 \(0\)o 單獨成塊),發現就是上面 \(x_l=x_r\)\(y_l=y_r\) 的問題。

這樣,就在單次詢問 \(O(n^2)\) 的復雜度內解決了問題(外層遞歸的每一層,都會有 \(O(1)\)\(O(n)\) 的內層遞歸)。

  • H

麻將題,咕了(


免責聲明!

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



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