CSP-S 2024 初賽解析

StelaYuri發表於2024-09-22

時間緊任務重,可能有誤,煩請指正 QwQ

題目內程式碼可能有些許錯誤,應該不大影響檢視吧,這個難改就不改哩


第1題 (2分)

在Linux系統中,如果你想顯示當前工作目錄的路徑,應該使用哪個命令?( )

A. pwd

B. cd

C. ls

D. echo

pwd 可以顯示當前的工作路徑

cd 表示切換工作路徑

ls 表示列出當前工作路徑下的所有檔案和目錄

echo 表示將輸入的字串進行標準輸出

第2題 (2分)

假設一個長度為n 的整數陣列中每個元素值互不相同,且這個陣列是無序的。要找到這個陣列中最大元素的時間複雜度是多少?( )

A. O(n)

B. O(log⁡n)

C. O(nlog⁡n)

D. O(1)

無序陣列找最大值

打擂臺,\(O(n)\)

第3題 (2分)

在 C++ 中,以下哪個函式呼叫會造成棧溢位?( )

A. int foo() { return 0; }

B. int bar() { int x = 1; return x; }

C. void baz() { int a[1000]; baz(); }

D. void qux() { return ; }

明顯 C 選項遞迴沒有設定邊界,並且每一層遞迴都開了一個無用的陣列,會造成棧溢位

第4題 (2分)

在一場比賽中,有 10 名選手參加,前三名將獲得金、銀、銅牌。若不允許並列、且每名選手只能獲得一枚獎牌,則不同的頒獎方式共有多少種?( )

A. 120

B. 720

C. 504

D. 1000

相當於從 10 名選手中選出三名,然後分配金銀銅牌

選出順序不同所表示的方案也是不同的,所以求的是排列數

\(A_{10}^3 = 720\)

第5題 (2分)

下面哪個資料結構最適合實現先進先出(FIFO)的功能?( )

A. 棧

B. 佇列

C. 線性表

D. 二叉搜尋樹

先進先出為佇列

第6題 (2分)

已知 f(1)=1,且對於 n >= 2 有 f(n) = f(n - 1) + f(⌊n/2⌋),則 f(4) 的值為( )

A. 4

B. 5

C. 6

D. 7

發現在 \(n \ge 2\) 時,遞推式只跟下標比自己小的項有關係,因此順序遞推即可

\(f(1) = 1\)

\(f(2) = f(1) + f(1) = 2\)

\(f(3) = f(2) + f(1) = 3\)

\(f(4) = f(3) + f(2) = 5\)

第7題 (2分)

假設有一個包含 n 個頂點的無向圖,且該圖是尤拉圖,以下關於該圖的描述中哪一項不一定正確?

A. 所有頂點的度數均為偶數

B. 該圖連通

C. 該圖存在一個尤拉回路

D. 該圖的邊數是奇數

首先發現 AB 兩個選項同時成立時,與 C 選項是等價的

尤拉圖就是擁有尤拉回路的圖

只擁有尤拉通路,不擁有尤拉回路的圖叫做半尤拉圖

所以 ABC 三個選項必選

至於 D 選項,造一個 4 個點 4 條邊組成的環,明顯可以排除

第8題 (2分)

對陣列進行二分查詢的過程中,以下哪個條件必須滿足?( )

A. 陣列必須是有序的

B. 陣列必須是無序的

C. 陣列長度必須是2的冪

D. 陣列中的元素必須是整數

因為二分查詢要根據中間項來確定答案在哪一邊

所以二分查詢的前提條件是陣列必須有序

第9題 (2分)

考慮一個自然數 n 以及一個模數 m。你需要計算 n 的逆元(即 n 在模 m 意義下的乘法逆元)。下列哪種演算法最為適合?( )

A. 使用暴力法依次嘗試

B. 使用擴充套件歐幾里得演算法

C. 使用快速冪法

D. 使用線性篩法

如果題目中有說明 \(m\) 是質數,那麼可以根據費馬小定理推出 \(n^{-1} \equiv n^{m-2}(\bmod m)\),這時候是可以藉助快速冪演算法求解的

但題目並沒有給定這個限制,因此費馬小定理不能直接套用,只能藉助擴充套件歐幾里得求解線性同餘方程來求逆元

擴充套件歐幾里得還可以用於判斷逆元是否存在,即 \(n, m\) 互質時才存在逆元

第10題 (2分)

在設計一個雜湊表時,為了減少衝突,需要使用適當的雜湊函式和衝突解決策略。已知某雜湊表中有 n 個鍵值對,裝的裝載因子為 α (0 < α <= 1)。在使用開放地址法解決衝 突的過程中,最壞情況下查詢一個元素的時間複雜度為( )。

A. O(1)
B. O(log⁡n)
C. O(1/(1−α))
D. O(n)

開放地址法也可以當作線性探測法

裝載因子表示表中元素個數與表長度的比值,在 \((0, 1]\) 之間說明一定存在元素,且每個元素一定能找到對應的存入位置

想查詢一個元素,最壞情況下就是表被全部佔滿了,並且在存入時每個元素都被雜湊到了同一個位置上,然後按順序往後線性探測,一個個存進後面第一個空位上

然後在查詢時,每次想找一個數字,就得像插入過程一樣,從前往後一個一個找,所以最壞情況就是 \(O(n)\) 把整張雜湊表找一遍才找到對應數字

第11題 (2分)

假設有一棵 h 層的完全二叉樹,該樹最多包含多少個結點?( )

A. 2^h-1

B. 2^(h+1)-1

C. 2^h

D. 2^(h+1)

\(h\) 層的完全二叉樹包含的結點總數應當是 \(2^0 + 2^1 + 2^2 + \dots + 2^{h-1} = 2^h - 1\)

第12題 (2分)

設有一個 10 個頂點的完全圖,每兩個頂點之間都有一條邊。有多少個長度為 4 的環?( )

A. 120

B. 210

C. 630

D. 5040

考慮先從 \(10\) 個點中選出 \(4\) 個點,求組合方案,即 \(C_{10}^4 = 210\)

對於任意四個點,需要考慮其圓排列總數,即 \((4-1)! = 6\),但因為是圖論題,因此順時針和逆時針當作相同的方案,因此排列數應該是 \(\dfrac{(4-1)!}2 = 3\)

或者直接列舉,假設四個點是 ABCD,明顯能組成的不同環排列有 ABCD, ACBD, ACDB 三種

最終答案即 \(210 \times 3 = 630\)

第13題 (2分)

對於一個整數 n ,定義 f(n) 為 n 的各位數字之和。問使 f(f(x)) = 10 的最小自然數 x 是多少?( )

A. 29

B. 199

C. 299

D. 399

滿足 \(f(x) = 10\)\(x\) 從小到大有 \(19, 28, 37, 46, 55, \dots\)

考慮 \(19\),再套一層,也就是找 \(f(x) = 19\)\(x\),發現最小的 \(x\) 就是 \(199\)

(或者,反正這是一道選擇題而不是填空題,直接從小往大一個個選項試一遍就行)

第14題 (2分)

設有一個長度為 n 的 01 字串。其中有 k 個 1。每次操作可以交換相鄰兩個字元,在最壞情況下將這 k 個 1 移到字串最右邊所需要的交換次數是多少?( )

A. k

B. k*(k-1)/2

C. (n-k)*k

D. (2n-k-1)*k/2

相當於給 01 串進行一個排序,每次交換相鄰兩個字元,可以當作讓逆序對數減少 1

所以交換次數就等於 01 串的逆序對數

為了讓逆序對數最多,肯定是讓 1 全在左,0 全在右

左邊 \(k\) 個 1 和右邊 \(n-k\) 個 0 組成的逆序對總數即 \(k \times (n-k)\)

第15題 (2分)

如圖是一張包含 7 個頂點的有向圖,如果要刪除其中一些邊,使得從節點 1 到節點 7 沒有可行路徑,且刪除的邊數最少,請問總共有多少種可行的刪除邊的集合?( )

A. 1

B. 2

C. 3

D. 4

明顯最少刪除的邊數為 2

這題可以假設先刪一條邊,然後畫出剩下的圖中的強連通分量,然後考慮方案數,會更方便一些

刪法分別為:

  • \(5-7, 6-7\)
  • \(4-6, 5-7\)
  • \(2-5, 4-6\)
  • \(1-2, 4-6\)

第16題 (11.5 分)


首先發現 logic 函式有點麻煩,但內部全都是位運算,所以不妨針對每一位,先畫出真值表

| x | y | x&y | x^y | ~x&y | (x^y)|(~x&y) | logic |
| ---- | ---- | ----- | ----- | ------ | -------------- | ------- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 | 1 | 1 |
| 1 | 0 | 0 | 1 | 0 | 1 | 1 |
| 1 | 1 | 1 | 0 | 0 | 0 | 1 |

明顯 logic 函式是按位或運算

然後考慮程式碼,generate 就是生成 \(b\) 個數字放在 \(c\) 陣列的下標 \(0\sim b-1\) 的位置上,其中 c[i] = (a | i) % (b + 1)

recursion 函式就是快排模板,但是加了個深度限制 depth,也就是說這個快排如果到達指定深度,即使沒排完也會結束

第16 - 1題(1.5 分)

當 1000 ≥ d ≥ b 時,輸出的序列是有序的。

A.對

B.錯

\(b \le d \le 1000\) 時,因為數字數量 \(b\) 比限制深度 \(d\) 要小,並且快排在最壞情況下其實也就是有多少數字就遞迴多少層(\(O(n^2)\) 出現的情況)

因此這個深度限制就跟沒有一樣

不論什麼情況,快排都一定會執行結束,到最後所有數字都會有序

第16 - 2題(1.5 分)

當輸入"5 5 1" 時,輸出為 "1 1 5 5 5"。

A.對

B.錯

首先根據資料生成五個數字,如果只看按位或,得到的數是 5 5 7 7 5

generate 函式中位運算完成後會對 \(b+1\) 取模,因此得到的數是 5 5 1 1 5

因為 \(d=1\),因此快排只執行一次,模擬一遍快排

5 5 1 1 5
i       j

執行兩個 while

5 5 1 1 5
i       j

i <= j 成立,交換,然後 i++, j--

5 5 1 1 5
i   j

執行兩個 while

5 5 1 1 5
i   j

i <= j 成立,交換,然後 i++, j--

5 1 1 5 5
 i
 j

執行兩個 while

5 1 1 5 5
 j i

i <= j 不成立,結束

發現最終答案是 5 1 1 5 5

第16 - 3題(1.5 分)

假設陣列 c 長度無限制,該程式所實現的演算法的時間複雜度是 O(b) 的。

A.對

B.錯

時間複雜度主要還是取決於快排,但注意本題還有一個深度限制

如果深度限制 \(d\) 較小,那麼時間複雜度應該是 \(O(b \times d)\)

如果深度限制 \(d\) 較大,最好情況下時間複雜度是 \(O(b \log b)\),最壞情況下就是 \(O(b^2)\)

第16 - 4題(3 分)

函式 int logic(int x, int y) 的功能是( )。

A.按位與

B.按位或

C.按位異或

D.以上都不是

根據上面的分析,logic 函式是按位或

第16 - 5題(4 分)

當輸入為 "10 100 100" 時,輸出的第 100 個數是( )。

A.91

B.94

C.95

D.98

首先考慮到 \(96 = 64 + 32 = 2^6 + 2^5\)

也就是說 \(96_{10} = 1100000_2\)

那麼 \(95_{10} = 1011111_2\)

又因為 \(10_{10} = 1010_2\)

發現 \(95\) 的後五位都是 \(1\),所以 \(95\) 以下的所有數字都按位或上 \(10\) 之後得到的數字大小都不會超過 \(95\)

\(96, 97, 98, 99\) 這四個整數,按位或 \(10\) 之後一定都超過了 \(100\),因此對 \(b+1 = 101\) 取模後的結果就變小了

所以生成的數字最大值就是 \(95\)

而本題給定的數字中 \(b = d\),所以深度限制可以忽略,快排一定能夠把所有數字都排完,因此第 \(100\) 個數就是生成過程中出現的最大的數字,即 \(95\)

第17題 (14 分)


假設輸入的 s 是包含 n 個字元的 01 串,完成下面的判斷題和單選題:

本題先從 solve2 函式入手,觀察到 \(i\) 迴圈從 \(0\)\(2^n-1\),也就是把所有位數為 \(n\) 的二進位制都列舉了一遍,然後 \(j\) 迴圈從 \(0\)\(n\)i & (1 << j) 即判斷 \(i\)\(2^j\) 這一位上是否為 \(1\),如果是 \(1\) 就執行 num = num * 2 + (s[j] - '0') 並且 cnt++

先單獨看 num = num * 2 + (s[j] - '0') 這一句,相當於把 \(i\) 的二進位制上為 \(1\) 的那些位置的字元單獨取出,然後二進位制數位拼接成一個新數字,再結合前面 \(i\) 迴圈的列舉,不難看出這裡相當於是把字串的每個子序列都單獨提了出來,當作一個二進位制數,然後轉為十進位制的 num 變數

cnt++ 則相當於是在統計當前取出的這個子序列的長度

cnt <= m 時,會把 num 加到 ans

所以不難發現,solve2 就是在把字串中所有長度不超過 \(m\) 的子序列全部取出來,當作二進位制數,轉為十進位制後再全部相加

然後看 solve 函式,有了上面的推導,我們不妨猜想這個函式和前面那個函式所求的內容應該有所關聯

\(14\) 行的 k = (j << 1) | (s[i] - '0'),因為字串是一個 01 串,所以這句話不妨轉化為 k = j * 2 + (s[i] - '0'),發現也就是在 \(j\) 的基礎上,把 s[i] 代表的數字拼到 \(j\) 的二進位制後面

然後在 j != 0 || s[i] == '1' 時,執行了 dp[k] += dp[j],也就是避免了從 \(0\) 轉移到 \(0\) 的情況,不難看出 dp[k] 應該是在計數,統計的是選出的子序列二進位制為 \(k\) 的方案總數。因此在 \(j\) 的方案後面拼上一個 s[i] 變成 \(k\) 這個數字後,\(k\) 的總方案數就會加上 \(j\) 原本的方案數

然後 \(21\) 行,i * dp[i] 就是在把值和數量乘起來相加,相當於就是在求所有二進位制數為 \(i\) 的數字總和,這和 solve2 函式是相同的

唯一的不同點在於,該函式不能從 \(0\) 轉移到 \(0\),因此選出的子序列第一個數必須為 \(1\)

綜上,solve 函式就是在把字串中所有長度不超過 \(m\) **且開頭為 \(1\) **的子序列全部取出來,當作二進位制數,轉為十進位制後再全部相加

第17 - 1題(1.5 分)

假設陣列 dp 長度無限制,函式 solve() 所實現的演算法的時間複雜度是 O(n*2^m)。

A.對

B.錯

明顯根據迴圈可以得出時間複雜度為 \(O(n\times 2^m)\)

第17 - 2題(1.5 分)

輸入 "11 2 10000000001" 時,程式輸出兩個數 32 和 23。

A.對

B.錯

相當於在找所有長度不超過 \(2\) 的子序列當作二進位制數後的總和

當長度為 \(1\),對答案有貢獻的子序列只有 1,出現了 \(2\)

當長度為 \(2\),對答案有貢獻的子序列有 01 / 10 / 11,出現次數分別為 \(9, 9, 1\)

對於 solve2 函式,因為該函式允許第一個選出的數為 \(0\),因此答案為 \(1_2 \times 2 + 01_2 \times 9 + 10_2 \times 9 + 11_2 \times 1 = 2 + 9 + 18 + 3 = 32\)

對於 solve1 函式,因為該函式不允許第一個選出的數為 \(0\),因此排除 01 的情況,答案為 \(1_2 \times 2 + 10_2 \times 9 + 11_2 \times 1 = 23\)

第17 - 3題(2 分)

在 n≤10 時,solve() 的返回值始終小於 4^10。

A.對

B.錯

不論對於哪個函式,在字串全為 \(1\),且 \(n = m\) 時一定可以取得最大值

因此假設 \(n = m = 10, s = 1111111111\)

答案為 \(C_{10}^1 \times 1 + C_{10}^2 \times 3 + C_{10}^3 \times 7 + \dots + C_{10}^{10} \times 1023 = 58025 \lt 4^{10}\)

第17 - 4題(3 分)

當 n=10 且 m=10 時,有多少種輸入使得兩行的結果完全一致?

A.1024

B.11

C.10

D.0

如果兩個函式結果一致,只能說明不存在任何以 \(0\) 開頭的子序列

也就是說所有 \(0\) 都必須出現在 \(1\) 之後

\(n = m = 10\) 時,方案只有 \(11\) 種:

  • 0000000000
  • 1000000000
  • 1100000000
  • 1110000000
  • 1111000000
  • \(\dots\)
  • 1111111111

第17 - 5題(3 分)

當 n <= 6 時,solve() 的最大可能返回值為( )。

A.65

B.211

C.665

D.2059

同上面最後一道判斷,最大可能返回值即字串全 \(1\) 的情況

\(C_6^1 \times 1_2 + C_6^2 \times 11_2 + C_6^3 \times 111_2 + \dots + C_6^6 \times 111111_2 = 665\)

第17 - 6題(3 分)

若 n = 8,m = 8,solve 和 solve2 的返回值的最大可能的差值為( )。

A.1477

B.1995

C.2059

D.2187

兩個函式的差值就等於以 \(0\) 開頭的子序列對答案的貢獻

\(n=m=8\) 時,為了能讓子序列當作二進位制時的權值儘可能大,因此此時字串方案應該是 01111111

考慮不同長度子序列的數量及其貢獻:

  • 長度為 \(2\) 時,選 \(1\)\(1\),答案為 \(C_7^1 \times 01_2\)
  • 長度為 \(3\) 時,選 \(2\)\(1\),答案為 \(C_7^2 \times 011_2\)
  • 長度為 \(4\) 時,選 \(3\)\(1\),答案為 \(C_7^3 \times 0111_2\)
  • \(\dots\)
  • 長度為 \(8\) 時,選 \(7\)\(1\),答案為 \(C_7^7 \times 01111111_2\)

最終答案即總和,求解可得 \(2059\)

第18題 (14.5 分)



這是一道樹上雙雜湊 + 埃氏篩的題目

首先關注 init 函式

  • p 陣列就是埃氏篩,表示每個位置的數字是否是素數
  • p1[i] 表示 \(B1^i \bmod P1\)
  • p2[i] 表示 \(B2^i \bmod P2\)

然後觀察 H 這個結構體的內部,其中有三個成員變數 h1, h2, l

初始化函式中,l = 1 恆成立,h1h2 會根據傳入的 b 不同而出現差值為 \(1\) 的區別

在加法運算子的過載函式中,發現加法得到的 l 為兩個結構體的 l 相加,而 h1h2 兩成員變數的變化過程相似

對於 h1,相當於將加法左側結構體的 h1 乘上 \(B1^{\text{右側結構體的 l}}\) 之後,再加上右側結構體的 h1

明顯這裡 B1 表示的就是雜湊的底數,l 表示雜湊多項式的長度(即數字個數),加法操作相當於是把兩個雜湊多項式合併在了一起,因此左側的多項式每一項都需要乘上 底數 的 右側多項式項數 次方,再把右側多項式加上

因此 h1h2 兩個數字只是在不同雜湊底數和模數下得到的兩個雜湊值,這裡做的是雙雜湊

然後等號的運算子過載函式只是為了判斷兩個雜湊值是否相等,小於號則定義了雜湊結構體的排序方法

最後回到 solve 函式,這個 for 迴圈是倒著做的,根據 h[i] = H(p[i]) 可以發現,初始時 h[i] 只會出現兩種情況:

  • 一種是 \(i\) 是質數時,h1 = K1+1 = 1h2 = K2+1 = 14l = 1
  • 一種是 \(i\) 不是質數時,h1 = K1 = 0h2 = K2 = 13l = 1

然後觀察第 59 和 61 行的轉移,發現 h[i] 的變化只會跟 h[2 * i]h[2 * i + 1] 這兩個位置有關係

透過 \(i, 2i, 2i+1\) 三個數字的關係,不難想到陣列模擬二叉樹時的編號順序,\(2i\)\(2i+1\) 分別是 \(i\) 的左右兒子

因此第 \(58\)2 * i + 1 <= n 就是表示 \(i\) 有左右兒子,而第 \(60\)2 * i <= n 表示 \(i\) 只有左兒子

然後發現在左右兒子都存在時,h[i] = h[2*i] + h[i] + h[2*i+1],先左兒子,再根結點,再右兒子;當只有左兒子時,h[i] = h[2*i] + h[i],即先左兒子,再根節點

所以雜湊過程就是把當前子樹的中序遍歷序列給雜湊掉

後面 h[1].h1 就是根結點的雜湊值

最後的排序+去重,輸出去重後不同雜湊結構體的數量,其實就是在求二叉樹上存在多少棵不同的子樹

第18 - 1題(1.5 分)

假設程式執行前能自動將 maxn 改為 n+1,所實現的演算法的時間複雜度是 O(n log n)。

A.對

B.錯

埃氏篩的時間複雜度為 \(O(n\log\log n)\),排序函式的時間複雜度是 \(O(n\log n)\),排序函式的時間複雜度級別更高,所以整體時間複雜度為 \(O(n\log n)\)

第18 - 2題(1.5 分)

時間開銷的瓶頸是 init() 函式。

A.對

B.錯

同上一題,時間複雜度取決於排序函式,所以瓶頸在 solve 函式

第18 - 3題(1.5 分)

若修改常數 B1 或 K1 的值,該程式可能會輸出不同的結果。

A.對

B.錯

修改常數 B1 或 K1,相當於把第一個雜湊值的雜湊過程給修改了

第 64 行輸出的是整棵樹的第一個雜湊值,因此這個值很可能會發生變化

第18 - 4題(3 分)

在 solve() 函式中,h[] 的合併順序可以看作是:

A.二叉樹的 BFS 序

B.二叉樹的先序遍歷

C.二叉樹的中序遍歷

D.二叉樹的後序遍歷

根據上面的分析,h 的合併可以看作是在合併子樹的中序遍歷序列的雜湊多項式

第18 - 5題(3 分)

輸入 "10",輸出的第一行是( )?

A.83

B.424

C.54

D.110101000

問的是輸出的第一行,因此需要求出根結點 h1 的值

但根據上面的分析,根結點 h1 的雜湊值其實就是整棵樹每個結點 h1 的中序遍歷序列的雜湊值

這個雜湊值乘的底數是 \(B1 = 2\),一開始每個結點 h1 的值取決於該結點所在編號的數字是否是素數,如果是則為 \(1\),不是則為 \(0\)

所以 h1 最終的結果可以簡單看作是整棵樹中序遍歷序列當作二進位制數,轉為十進位制之後的結果

我們可以先把 \(n=10\) 時的這棵樹畫出來,其中黑色數字表示編號,藍色數字表示 h1 的初始值

這棵樹的中序遍歷為 \(8,4,9,2,10,5,1,6,3,7\),對應的值分別是 \(0,0,0,1,0,1,0,0,1,1\)

\(0001010011\) 轉為二進位制,得 \(83\)

第18 - 6題(4 分)

(4分)輸出 "16",輸出的第二行是( )?

A.7

B.9

C.10

D.12

第二行表示的是有多少個不同的雜湊結構體,換句話說也就是這棵樹上有多少棵本質不同的子樹

\(n=16\) 的樹畫出,藍色數字表示 h1 的初始值

接下來以每個結點作為根結點,看一遍有多少棵本質不同的子樹即可

下面打勾的就是本質不同的子樹的根結點方案之一,總共 \(10\)

第19題 (15 分)

(序列合併)有兩個長度為 N 的單調不降序列 A 和 B,序列的每個元素都是小於 10^9 的非負整數。在 A 和 B 中各取一個數相加可以得到 N^2 個和,求其中第 K 小的和。上述引數滿足 N <= 10^5 和 1 <= K <= N^2。


首先可以考慮完成 upper_bound 函式,這其實是一個 STL 函式,但這裡讓你手動實現其內部的二分。upper_bound(l, r, x) 函式的含義是在一段連續地址 \([l, r)\) 中查詢出第一個 \(\gt x\) 的位置。但如果說你比較擔心這道題的 upper_bound 和我們平時接觸的二分函式含義不一樣,可以先看下面的分析:

根據題意,要找到排名為 \(k\) 的和,因此考慮二分答案 mid,然後找出有多少個和是不超過 mid 的,根據得到的數量來判斷答案要往大了找還是往小了找

先看第五個空,這裡的條件一旦滿足就會執行 l = mid + 1,說明答案一定大於此時的 mid,所以可以判斷是因為 <= mid 的和的數量嚴格小於 k,即 get_rank(mid) < k

然後就可以推匯出 get_rank(sum) 這個函式就是在求 <= sum 的和的數量。只需要對於每個 a[i],求出有多少個 b[j] 滿足 a[i] + b[j] <= sum,移項得 b[j] <= sum - a[i],因此只需要在 b 陣列裡找不超過 sum - a[i] 的數字有多少個就可以,所以第 26 行程式碼應該要透過 upper_bound 函式找到 b 陣列中 > sum - a[i] 的第一個地址,然後減去 b 的首地址,才能夠獲得想要的答案

所以從這裡可以推出 upper_bound(l, r, x) 是在一段連續地址 \([l, r)\) 中二分查詢出第一個 \(\gt x\) 的位置,注意左閉右開

第19 - 1題(3 分)

①處應填

A.an-a

B.an-a-1

C.ai

D.ai+1

這裡是 upper_bound 函式的右邊界,因為要進行二分,所以 r 一開始應該表示的是這段左閉右開的地址內共有多少個數字,即尾地址 an 減首地址 a

第19 - 2題(3 分)

②處應填

A.a[mid] > ai

B.a[mid] >= ai

C.a[mid] < ai

D.a[mid] <= ai

不妨先看 else,想想什麼時候會讓 l = mid + 1

因為現在要找第一個 > ai 的數字,所以只有噹噹前位置的數字 a[mid] <= ai 時,才能斷定答案一定在 mid 右側

所以反過來,if 裡就應該填 a[mid] > ai

第19 - 3題(3 分)

③處應填

A.a+l

B.a+l+1

C.a+l-1

D.an-l

最後要返回一個地址,所以應該是從首地址 a 開始往後移動 l/r 個位置的地址

其實這裡寫法有很多,像是 a + la + r&a[l]&a[r] 都可以

第19 - 4題(3 分)

④處應填

A.a[n-1]+b[n-1]

B.a[n]+b[n]

C.2 * maxn

D.maxn

solve 函式二分的是最終答案,如果 \(k = n^2\),那麼答案就是最大和,應該是兩陣列最大值之和,即 a[n-1] + b[n-1]

第19 - 5題(3 分)

⑤處應填

A.get_rank(mid) < k

B.get_rank(mid) <= k

C.get_rank(mid) > k

D.get_rank(mid) >= k

詳見上面的分析

第20 題 (15 分)

(次短路)

已知一個有 n 個點 m 條邊的有向圖 G,並且給定圖中的兩個點 s 和 t,求次短路(長度嚴格大於最短路的最短路徑)。如果不存在,輸出一行 "-1"。如果存在,輸出兩行,第一行表示次短路的長度,第二行表示次短路的一個方案。


(一道模板次短路演算法題,但寫法略微抽象了些,但也還好)

首先明確什麼是次短路:除了最短路以外的最短路(除最優解以外的最優解就是次優解)

所以次短路演算法的總體邏輯就是在最短路演算法的基礎上分類討論

假設現在正在進行最短路演算法的鬆弛,假設 \(a\)\(b\) 有一條長度為 \(w\) 的邊,現在要根據 \(a\) 的答案來更新 \(b\) 的答案:

  • 如果 dis[b] > dis[a] + c,說明現在能夠藉助 \(a\) 來鬆弛 \(b\) 的最短路答案,目前走到 \(b\) 點的最優解變成了從 \(a\) 點經過 \(a \rightarrow b\) 這條邊過來,所以在這一步鬆弛之前dis[b] 有可能作為 \(b\) 點的次優解
  • 如果 dis[b] < dis[a] + c,說明現在無法藉助 \(a\) 來鬆弛 \(b\) 的最短路答案,但此時從 \(a\) 點經過 \(a \rightarrow b\) 這條邊過來的方案也是有可能作為 \(b\) 點的次優解的
  • 如果 dis[b] == dis[a] + c,題幹中有提到次短路是“長度嚴格大於最短路的最短路徑”,所以相等的情況不用管

整個演算法就是在這個分類討論的基礎上執行的,在所有可能的次優解中找一個長度最小的方案,就是次短路

接下來看程式碼本身

add 函式很明顯是鏈式前向星加一條 \(a\rightarrow b\) 的長度為 \(c\) 的邊

out 函式是找到次短路之後輸出這條路徑的遞迴函式

然後觀察 main 函式,所有點的下標都 \(-1\) 後變成 \(0 \sim n-1\) 的範圍了,然後進入 solve 函式,一個普通的 Dijkstra 堆最佳化寫法,然後只要佇列不為空就不斷鬆弛出去,重點在於第 37、38 行和第 46 到 51 行這一段

先看第 37、38 行,dis2pre2 兩個指標分別指向了 dis + npre + n 的位置,這也是為什麼 dispre 陣列要開兩倍空間,相當於前面 \(0 \sim n-1\) 的下標給自己用,而後面 \(n \sim 2n-1\) 的下標交給了 dis2pre2 兩個指標用。所以可以猜測 dis2pre2 就是用來記錄次短路的答案和路徑的

就相當於這裡建立了一張分層圖,\(0 \sim n-1\) 在正常跑最短路,而 \(n \sim 2n-1\) 則是記錄上一層每個對應的點的次短路,同一個點在兩層間的編號差值為 \(n\)

然後看 47 行的引數傳遞格式,可以知道 upd(a, b, d, q) 相當於是把鬆弛的過程寫成了一個函式,在看 \(a\)\(b\) 這兩個點的最短路能否更新為 \(d\),透過函式返回值來判斷是否成功鬆弛

如果成功鬆弛,根據上面的分類討論,原本到 \(b\) 點的答案就成了次優解,所以第一個空這裡 b < n 就是在判斷現在 \(b\) 這個點是否還在求最短路的這一層,如果是,則可以更新 \(b\) 的次短路;如果不是,說明這一次鬆弛本來就是在更新 \(b\) 的次短路,就不用管了

而如果沒有成功鬆弛,根據上面的分類討論,當前從 \(a\) 出發的這種方案可能成為次優解,所以第四個空就是在這種情況下更新 \(b\) 的次短路的

最後如果說存在次短路,則從第二層的終點 n+t 開始往回找出整條路徑,遞迴輸出即可

第20 - 1題(3 分)

①處應填

A.upd(pre[b], n + b, dis[b], q)

B.upd(a, n + b, d, q)

C.upd(pre[b], b, dis[b], q)

D.upd(a, b, d, q)

根據上面的分析,這裡是在 \(b\) 進行鬆弛之前,把它原本的最短路當作次短路來更新答案的

原本到 \(b\) 的最短路的前一個點可以透過 pre[b] 得到,變成次短路後就應該成為第二層的點,即 \(n+b\),此時次短路長度為 dis[b]

第20 - 2題(3 分)

②處應填

A.make_pair(-d, b)

B.make_pair(d, b)

C.make_pair(b, d)

D.make_pair(-b, d)

這裡就是考 Dijkstra 堆最佳化情況下,優先佇列要怎麼放元素

pair 會預設根據 first 關鍵詞從小到大排,first 相同時按照 second 從小到大排

但因為程式碼裡的優先佇列是用的大根堆,所以會按 first 從大到小排先

而 Dijkstra 堆最佳化更多的是用小根堆來求出距離起點最近的未訪問過的點進行鬆弛

所以為了能在大根堆中實現小根堆的功能,這裡應該把 first 關鍵詞取反,並且 first 關鍵詞表示當前點與起點之間的距離,second 關鍵詞表示當前點的編號

所以這裡的 make_pair 的引數為 \(-d\)\(b\)

第20 - 3題(3 分)

③處應填

A.0xff

B.0x1f

C.0x3f

D.0x7f

這裡是在初始化 dis 陣列,即記錄最短路的答案陣列,一般是要初始化成一個無窮大的數字的

因為 int 型別最大隻能存到 \(2^{31} - 1\),所以 memset 函式不能直接使用 0xff,所以排除 A

因為 dis 陣列在鬆弛過程中會和某條邊的長度相加求和,所以如果直接用 0x7f 有機率超出範圍,一般也不用,排除 D

正常情況下都是選 0x3f 最為保險,但這題請注意第 74 行(上面截圖裡是第 72 行)以及第 7 行程式碼中的 inf 的定義

最後在終點次短路為 inf 時,直接判斷不存在,因此我們 memset 應該這個無窮大的數字設定為與 inf 相同

\(522133279_{10} = 1f1f1f1f_{16}\)

所以這題只能選 B

第20 - 4題(3 分)

④處應填

A.upd(a, n + b, dis[a] + c, q)

B.upd(n + a, n + b, dis2[a] + c, q)

C.upd(n + a, b, dis2[a] + c, q)

D.upd(a, b, dis[a] + c, q)

根據上面的分析,這裡是在鬆弛失敗之後,把當前從 \(a\)\(b\) 且經過長度為 \(c\) 的邊的這種方案當作次優解,來更新次短路的

所以這裡很明顯就是從 \(a\) 出發,到 \(b\) 點對應的次優解點 \(n+b\),嘗試更新答案為 dis[a] + c

第20 - 5題(3 分)

⑤處應填

A.pre2[a%n]

B.pre[a%n]

C.pre2[a]

D.pre[a%n]+1

第 58 行的 else 表示的是 \(a\) 點現在是次短路這一層中的點,所以我們應該從 pre2 陣列裡把 \(a\) 點的上一個點找出來

明顯現在 \(a \ge n\) 是成立的,所以不能直接寫 pre2[a],不然會越界,只能把 \(a\) 先轉回原本的編號,可以用 a % n 也可以直接 a - n

所以這個空答案有多種,可以是 pre2[a % n],可以是 pre2[a - n],也可以直接是 pre[a]