循環(huán)賽日程表算法:遞歸與遞推兩種經(jīng)典解法詳解
1. 項(xiàng)目概述從體育聯(lián)賽到算法競(jìng)賽的經(jīng)典問(wèn)題最近在整理算法筆記翻到了“循環(huán)賽日程表”這個(gè)老問(wèn)題。這問(wèn)題聽起來(lái)像是體育部干事排賽程的活兒但實(shí)際上它是計(jì)算機(jī)算法中一個(gè)絕佳的案例完美地展示了遞歸與遞推這兩種核心思想是如何解決同一個(gè)問(wèn)題的。我第一次接觸它是在大學(xué)的數(shù)據(jù)結(jié)構(gòu)課上當(dāng)時(shí)只覺(jué)得是個(gè)巧妙的數(shù)學(xué)游戲。后來(lái)在工作中尤其是在處理一些需要分治和動(dòng)態(tài)規(guī)劃的任務(wù)時(shí)才猛然發(fā)現(xiàn)這個(gè)模型的影子無(wú)處不在。簡(jiǎn)單來(lái)說(shuō)循環(huán)賽日程表問(wèn)題就是假設(shè)有n2^k位選手比如8個(gè)、16個(gè)隊(duì)伍進(jìn)行單循環(huán)賽即每?jī)晌贿x手之間都要恰好比賽一次。我們需要為整個(gè)賽事安排一個(gè)日程表使得在n-1天或輪次內(nèi)完成所有比賽并且每天每位選手只進(jìn)行一場(chǎng)比賽。這個(gè)問(wèn)題的核心挑戰(zhàn)在于如何高效、無(wú)沖突地生成這個(gè)龐大的對(duì)陣表。為什么說(shuō)它經(jīng)典因?yàn)樗鼊冸x了復(fù)雜的業(yè)務(wù)外殼直指一個(gè)本質(zhì)需求如何將一個(gè)大問(wèn)題為n個(gè)對(duì)象安排復(fù)雜關(guān)系系統(tǒng)性地分解為若干個(gè)相同的小問(wèn)題并找到它們之間的構(gòu)造規(guī)律。無(wú)論是遞歸的“自頂向下分而治之”還是遞推的“自底向上步步為營(yíng)”都能在這里找到優(yōu)雅的解法。搞懂它你收獲的不僅是一段代碼更是一種解決問(wèn)題的思維框架。接下來(lái)我就結(jié)合自己反復(fù)實(shí)現(xiàn)和教學(xué)的經(jīng)驗(yàn)把這兩種思路掰開揉碎了講清楚。2. 核心思路拆解遞歸的“分治”與遞推的“構(gòu)造”要生成日程表我們得先理解問(wèn)題結(jié)構(gòu)。設(shè)選手編號(hào)為1到n。日程表本質(zhì)上是一個(gè)n行n-1列的矩陣schedule[i][j]表示第i號(hào)選手在第j天對(duì)陣的選手編號(hào)。自己不對(duì)陣自己所以對(duì)角線或者說(shuō)第0列如果我們把選手編號(hào)也看作一列的話可以忽略或用于存儲(chǔ)其他信息。2.1 遞歸分治思想化整為零復(fù)制合并遞歸的思路是典型的分治法。當(dāng)k1即只有2位選手1和2時(shí)問(wèn)題很簡(jiǎn)單第一天1對(duì)22對(duì)1。日程表是一個(gè)2x1的矩陣。當(dāng)有4位選手時(shí)我們先把他們分成上下兩個(gè)半?yún)^(qū)每個(gè)半?yún)^(qū)2人。我們遞歸地為這個(gè)2人子問(wèn)題生成日程表。神奇的事情來(lái)了4人問(wèn)題的日程表可以通過(guò)這兩個(gè)2人日程表“拼接”和“復(fù)制”得到。具體來(lái)說(shuō)先安排好左上角的2人日程1和2的對(duì)陣。將左上角的日程表復(fù)制到右下角對(duì)應(yīng)選手3和4的對(duì)陣。將左上角的日程表“平移”并“加偏置”后復(fù)制到左下角和右上角從而確定不同半?yún)^(qū)選手之間的對(duì)陣。這個(gè)過(guò)程可以無(wú)限遞歸下去8人問(wèn)題拆成兩個(gè)4人子問(wèn)題16人問(wèn)題拆成兩個(gè)8人子問(wèn)題……每一次拆分我們都執(zhí)行類似的“復(fù)制”和“平移”操作。遞歸的終止條件就是選手?jǐn)?shù)為2。這種思路非常直觀它模擬了我們大腦處理問(wèn)題的自然方式大問(wèn)題不會(huì)解先拆成小問(wèn)題小問(wèn)題解決了再想辦法把它們組合起來(lái)。注意這里說(shuō)的“復(fù)制”不是簡(jiǎn)單的拷貝。左下角塊的值是右上角塊的值加上一個(gè)“半?yún)^(qū)大小”的偏移量反之亦然。這是保證不同半?yún)^(qū)選手正確對(duì)陣的關(guān)鍵。2.2 遞推迭代思想步步為營(yíng)規(guī)律構(gòu)造遞推的思路則反其道而行之。我們不從頂層開始分解而是從最小的原子單元2人日程開始利用已知的規(guī)律像搭積木一樣一層一層構(gòu)造出更大的日程表。我們從size2的日程表開始就是1對(duì)2那個(gè)基礎(chǔ)矩陣。然后我們用一個(gè)循環(huán)讓size依次翻倍4, 8, 16, ... 直到達(dá)到目標(biāo)人數(shù)n。在每一輪翻倍中我們都有明確的規(guī)則來(lái)填充新的日程表填充右下角新擴(kuò)展出的右下角size/2區(qū)域其值等于左上角對(duì)應(yīng)區(qū)域的值。填充左下角新擴(kuò)展出的左下角區(qū)域其值等于右上角對(duì)應(yīng)區(qū)域的值加上size/2。填充右上角新擴(kuò)展出的右上角區(qū)域其值等于左下角對(duì)應(yīng)區(qū)域的值減去size/2或者根據(jù)對(duì)稱性直接由左下角推導(dǎo)。這個(gè)過(guò)程就像細(xì)胞分裂和復(fù)制。遞推的優(yōu)勢(shì)在于它完全避免了遞歸的函數(shù)調(diào)用開銷代碼通常更簡(jiǎn)潔效率也更高尤其適合對(duì)性能有要求的場(chǎng)景。它要求我們更清晰地洞察問(wèn)題中每一步之間的依賴關(guān)系和變換規(guī)律。兩種思路的選擇遞歸思路更符合直覺(jué)易于理解和證明正確性遞推思路效率更高是遞歸思路的“非遞歸實(shí)現(xiàn)”或“動(dòng)態(tài)規(guī)劃”版本。在實(shí)際編碼中我通常建議先理解遞歸版本因?yàn)樗沂玖藛?wèn)題的本質(zhì)結(jié)構(gòu)。當(dāng)你需要高性能時(shí)再將其改寫為遞推版本。3. 遞歸算法實(shí)現(xiàn)詳解與代碼剖析理解了分治思想我們來(lái)動(dòng)手實(shí)現(xiàn)遞歸版本。我將使用C語(yǔ)言進(jìn)行演示因?yàn)槠湔Z(yǔ)法清晰能很好地表達(dá)算法邏輯。其他語(yǔ)言如Java、Python的思路是完全一致的。3.1 算法核心arrange函數(shù)我們定義一個(gè)遞歸函數(shù)void arrange(int left, int right, int currentSize)。left: 當(dāng)前要處理的選手區(qū)間的起始編號(hào)。right: 當(dāng)前要處理的選手區(qū)間的結(jié)束編號(hào)。currentSize: 當(dāng)前區(qū)間內(nèi)的選手?jǐn)?shù)量right - left 1它總是2的冪次。這個(gè)函數(shù)的職責(zé)是為編號(hào)在[left, right]區(qū)間內(nèi)的currentSize位選手安排他們內(nèi)部的比賽日程并將結(jié)果填充到全局日程表schedule[][]中。#include iostream #include vector #include cmath using namespace std; vectorvectorint schedule; // 全局日程表schedule[i][j] 表示選手i在第j天的對(duì)手 // 遞歸安排日程 // left: 區(qū)間左邊界選手編號(hào) // right: 區(qū)間右邊界選手編號(hào) // currentSize: 當(dāng)前區(qū)間選手?jǐn)?shù) (必須是2的冪) void arrange(int left, int right, int currentSize) { // 遞歸基當(dāng)只有兩名選手時(shí) if (currentSize 2) { // 選手 left 在唯一的一天對(duì)陣 right schedule[left][0] right; // 注意這里“第0天”是相對(duì)于這個(gè)子區(qū)間而言的。 // 選手 right 在同一天對(duì)陣 left schedule[right][0] left; return; } // 1. 分將當(dāng)前區(qū)間平分為兩個(gè)子區(qū)間 int mid (left right) / 2; int halfSize currentSize / 2; // 遞歸解決左半?yún)^(qū) [left, mid] 的內(nèi)部賽程 arrange(left, mid, halfSize); // 遞歸解決右半?yún)^(qū) [mid1, right] 的內(nèi)部賽程 arrange(mid 1, right, halfSize); // 2. 治合并兩個(gè)子區(qū)間的解安排跨區(qū)比賽 // 我們假設(shè)遞歸調(diào)用后每個(gè)子區(qū)間已經(jīng)安排好其內(nèi)部的 (halfSize-1) 天比賽。 // 現(xiàn)在需要為接下來(lái)的 halfSize 天安排跨區(qū)比賽。 // 對(duì)于左半?yún)^(qū)的每個(gè)選手 i for (int i left; i mid; i) { // 對(duì)于右半?yún)^(qū)的每個(gè)選手 j (其編號(hào)是 i halfSize) int j i halfSize; // 安排他們?cè)趶?halfSize-1 天開始的 halfSize 天內(nèi)比賽 // 注意日程表列索引從0開始前 halfSize-1 列已被內(nèi)部比賽占用 for (int day 0; day halfSize; day) { // 關(guān)鍵步驟左區(qū)選手i在第(halfSize-1 day)天的對(duì)手是右區(qū)選手j schedule[i][halfSize - 1 day] j; // 對(duì)稱地右區(qū)選手j在同一天的對(duì)手是左區(qū)選手i schedule[j][halfSize - 1 day] i; } } }3.2 主函數(shù)與初始化遞歸函數(shù)是核心引擎我們還需要一個(gè)主函數(shù)來(lái)設(shè)置初始條件并驅(qū)動(dòng)整個(gè)過(guò)程。int main() { int k; // 2^k 位選手 cout 請(qǐng)輸入k值將安排2^k位選手的比賽: ; cin k; int n pow(2, k); // 選手總數(shù) int days n - 1; // 比賽總天數(shù) // 初始化日程表大小為 (n1) x days為了下標(biāo)從1開始更直觀 schedule.assign(n 1, vectorint(days, 0)); // 開始遞歸安排初始區(qū)間為[1, n]選手?jǐn)?shù)為n arrange(1, n, n); // 打印日程表 cout \n循環(huán)賽日程表 (選手編號(hào)從1到 n ):\n; cout 選手\\天數(shù); for (int d 0; d days; d) cout \t第 d 1 天; cout endl; for (int i 1; i n; i) { cout 選手 i :; for (int d 0; d days; d) { cout \t schedule[i][d]; } cout endl; } return 0; }3.3 遞歸過(guò)程模擬與理解假設(shè)k2(n4)讓我們手動(dòng)模擬一下arrange(1, 4, 4)的調(diào)用棧arrange(1, 4, 4)currentSize4 ! 2進(jìn)入分支。計(jì)算mid 2,halfSize 2。遞歸調(diào)用arrange(1, 2, 2)。遞歸調(diào)用arrange(3, 4, 2)。arrange(1, 2, 2)currentSize 2觸發(fā)遞歸基。設(shè)置schedule[1][0] 2,schedule[2][0] 1。返回。arrange(3, 4, 2)currentSize 2觸發(fā)遞歸基。設(shè)置schedule[3][0] 4,schedule[4][0] 1。返回?;氐絘rrange(1, 4, 4)的合并階段。halfSize 2。循環(huán)i從 1 到 2 (mid)。i1:j 12 3。內(nèi)層循環(huán)day從 0 到 1。day0:schedule[1][1] 3,schedule[3][1] 1。第2天day1:schedule[1][2] 3,schedule[3][2] 1。第3天這里有個(gè)錯(cuò)誤i2:j 22 4。day0:schedule[2][1] 4,schedule[4][1] 2。day1:schedule[2][2] 4,schedule[4][2] 2。發(fā)現(xiàn)了問(wèn)題根據(jù)我們的合并邏輯左區(qū)選手1,2與右區(qū)選手3,4的比賽被安排在了第2、3天列索引1和2。但是對(duì)于選手1和3他們?cè)诘?天和第3天都對(duì)陣彼此這違反了“每對(duì)選手只賽一次”的規(guī)則。錯(cuò)誤根源上面的合并循環(huán)寫錯(cuò)了??鐓^(qū)比賽應(yīng)該安排在halfSize天但每一天的對(duì)陣關(guān)系需要精心設(shè)計(jì)不能簡(jiǎn)單地讓同一對(duì)選手在連續(xù)幾天重復(fù)比賽。正確的合并邏輯需要利用子問(wèn)題已經(jīng)安排好的賽程。4. 遞歸算法的正確實(shí)現(xiàn)與關(guān)鍵技巧上面的錯(cuò)誤示范引出了一個(gè)關(guān)鍵點(diǎn)合并時(shí)我們不能讓左區(qū)的每個(gè)選手固定和右區(qū)的一個(gè)選手比賽多天而應(yīng)該讓左區(qū)的每個(gè)選手在halfSize天里依次對(duì)陣右區(qū)的halfSize個(gè)不同選手。這就需要我們利用子問(wèn)題日程表中的信息。4.1 正確的合并策略假設(shè)左半?yún)^(qū)[left, mid]和右半?yún)^(qū)[mid1, right]都已經(jīng)遞歸地安排好了各自內(nèi)部halfSize-1天的比賽?,F(xiàn)在我們要安排接下來(lái)的halfSize天進(jìn)行跨區(qū)比賽。觀察發(fā)現(xiàn)左半?yún)^(qū)第i位選手i是左半?yún)^(qū)內(nèi)的相對(duì)編號(hào)從0開始在跨區(qū)比賽的第t天t從0到halfSize-1應(yīng)該對(duì)陣右半?yún)^(qū)第(i t) % halfSize位選手。這里用到了模運(yùn)算來(lái)實(shí)現(xiàn)循環(huán)配對(duì)。更具體地設(shè)左區(qū)選手編號(hào)為a left i右區(qū)選手編號(hào)為b (mid 1) j。我們需要一個(gè)映射使得在halfSize天內(nèi)每個(gè)a都能和每個(gè)b恰好比賽一次。這本質(zhì)上是在構(gòu)造一個(gè)halfSize x halfSize的拉丁方每一行、每一列都是halfSize個(gè)元素的排列。修正后的合并代碼片段// ... 在 arrange 函數(shù)的合并部分 ... // 安排跨區(qū)比賽持續(xù) halfSize 天 for (int t 0; t halfSize; t) { // t 表示跨區(qū)比賽的第幾天相對(duì) for (int i 0; i halfSize; i) { // i 是左半?yún)^(qū)內(nèi)的偏移 int playerLeft left i; // 關(guān)鍵計(jì)算右半?yún)^(qū)對(duì)手的偏移。 (i t) % halfSize 確保了循環(huán)配對(duì) int opponentOffsetInRight (i t) % halfSize; int playerRight (mid 1) opponentOffsetInRight; // 計(jì)算絕對(duì)的天數(shù)索引 // 前 halfSize-1 天是內(nèi)部比賽所以跨區(qū)比賽從第 (halfSize - 1 t) 天開始 int dayIndex halfSize - 1 t; schedule[playerLeft][dayIndex] playerRight; schedule[playerRight][dayIndex] playerLeft; } }4.2 完整正確的遞歸實(shí)現(xiàn)結(jié)合正確的合并邏輯完整的遞歸實(shí)現(xiàn)如下#include iostream #include vector #include cmath #include iomanip using namespace std; vectorvectorint schedule; void arrangeRecursive(int left, int right, int currentSize) { if (currentSize 2) { // 只有兩個(gè)選手比賽一天 schedule[left][0] right; schedule[right][0] left; return; } int mid (left right) / 2; int halfSize currentSize / 2; // 遞歸解決子問(wèn)題 arrangeRecursive(left, mid, halfSize); arrangeRecursive(mid 1, right, halfSize); // 合并安排兩個(gè)半?yún)^(qū)之間的比賽 for (int t 0; t halfSize; t) { // 跨區(qū)比賽的每一天 for (int i 0; i halfSize; i) { // 遍歷左半?yún)^(qū)每個(gè)選手 int playerLeft left i; // 計(jì)算該選手在今天對(duì)陣的右半?yún)^(qū)選手 // 使用模運(yùn)算實(shí)現(xiàn)循環(huán)配對(duì) int opponentOffset (i t) % halfSize; int playerRight (mid 1) opponentOffset; // 當(dāng)前是第 (halfSize - 1 t) 天 int day halfSize - 1 t; schedule[playerLeft][day] playerRight; schedule[playerRight][day] playerLeft; } } } int main() { int k; cout 請(qǐng)輸入k (選手?jǐn)?shù)2^k): ; cin k; int n pow(2, k); int days n - 1; // 初始化選手編號(hào)從1開始天數(shù)從0到days-1 schedule.assign(n 1, vectorint(days, 0)); arrangeRecursive(1, n, n); // 美化輸出 cout \n循環(huán)賽日程表 (n n ):\n; cout setw(6) Player; for (int d 1; d days; d) { cout setw(6) Day d; } cout endl; for (int i 1; i n; i) { cout setw(6) i; for (int d 0; d days; d) { cout setw(8) schedule[i][d]; } cout endl; } // 驗(yàn)證檢查每對(duì)選手是否恰好比賽一次 vectorvectorbool played(n 1, vectorbool(n 1, false)); bool valid true; for (int i 1; i n; i) { for (int d 0; d days; d) { int j schedule[i][d]; if (j 0 || j i) { cout 錯(cuò)誤選手 i 在第 d1 天對(duì)陣無(wú)效( j )! endl; valid false; } if (played[i][j]) { cout 錯(cuò)誤選手 i 和 j 比賽了多次! endl; valid false; } played[i][j] played[j][i] true; } } if (valid) { cout \n日程表驗(yàn)證通過(guò) endl; } return 0; }4.3 遞歸實(shí)現(xiàn)的注意事項(xiàng)與心得下標(biāo)處理是萬(wàn)惡之源這是實(shí)現(xiàn)時(shí)最容易出錯(cuò)的地方。務(wù)必明確你的數(shù)據(jù)結(jié)構(gòu)和下標(biāo)起始選手編號(hào)是從0還是1開始天數(shù)索引是從0還是1開始。我強(qiáng)烈建議選手編號(hào)從1開始這樣更符合直覺(jué)schedule[i][d]直接表示i號(hào)選手第d天的對(duì)手。初始化矩陣大小時(shí)要預(yù)留足夠空間n1行。理解“相對(duì)”與“絕對(duì)”在遞歸函數(shù)中l(wèi)eft,right,currentSize描述的是當(dāng)前子問(wèn)題的“絕對(duì)”邊界和大小。而在合并循環(huán)中i和t常常是“相對(duì)”于當(dāng)前半?yún)^(qū)的偏移量。清晰地轉(zhuǎn)換這兩種視角是正確編碼的關(guān)鍵。合并邏輯的推導(dǎo)不要死記硬背(it)%halfSize這個(gè)公式。理解其本質(zhì)在halfSize天的循環(huán)賽中讓左區(qū)選手按某種循環(huán)順序與右區(qū)所有選手各賽一場(chǎng)。你可以畫一個(gè)halfSize4的小表格手動(dòng)推導(dǎo)一下配對(duì)關(guān)系感受模運(yùn)算如何實(shí)現(xiàn)“循環(huán)移位”。遞歸深度由于問(wèn)題規(guī)模是2^k遞歸深度為k。對(duì)于k10(1024人)深度為10完全在安全范圍內(nèi)不用擔(dān)心棧溢出。驗(yàn)證至關(guān)重要像上面主函數(shù)中那樣寫一個(gè)簡(jiǎn)單的驗(yàn)證邏輯來(lái)檢查生成的日程表是否滿足“每對(duì)選手恰好比賽一次”和“每天每人只賽一場(chǎng)”的條件。這是確保算法正確性的最后一道保險(xiǎn)。5. 遞推迭代算法實(shí)現(xiàn)與性能分析遞歸版本易于理解但存在函數(shù)調(diào)用開銷。遞推版本則通過(guò)循環(huán)直接構(gòu)造最終結(jié)果通常效率更高代碼也更緊湊。其核心思想是從小規(guī)模日程表2人開始通過(guò)迭代利用已知的size日程表構(gòu)造出2*size的日程表。5.1 遞推算法的核心構(gòu)造規(guī)律設(shè)我們已經(jīng)有了一個(gè)size x size的日程表實(shí)際上我們只需要size x (size-1)的矩陣但為構(gòu)造方便我們可以先構(gòu)造size x size的方陣第一列放選手自身編號(hào)或留空。將其放在一個(gè)大矩陣的左上角。當(dāng)我們想構(gòu)造2*size的日程表時(shí)右下角塊直接復(fù)制左上角塊的值。這對(duì)應(yīng)了“下半?yún)^(qū)內(nèi)部比賽”的安排與上半?yún)^(qū)內(nèi)部相同。左下角塊等于右上角塊的值加上size。這表示下半?yún)^(qū)選手的編號(hào)是上半?yún)^(qū)對(duì)應(yīng)選手編號(hào)加上size。右上角塊等于左下角塊的值減去size。這其實(shí)是步驟2的逆操作保證了對(duì)稱性。更形式化地用table[i][j]表示i號(hào)選手在第j天這里j從1開始到size-1的對(duì)手。初始時(shí)size1可以認(rèn)為只有1個(gè)選手無(wú)需比賽或者直接從size2開始table[1][1]2,table[2][1]1。對(duì)于size 2, 4, 8, ...直到n執(zhí)行以下操作int half size; size * 2; // 1. 右下角 左上角 for (int i 1; i half; i) { for (int j 1; j half; j) { table[i half][j half] table[i][j]; } } // 2. 左下角 右上角 half for (int i 1; i half; i) { for (int j 1; j half; j) { table[i half][j] table[i][j half] half; } } // 3. 右上角 左下角 - half (或者由對(duì)稱性直接賦值) for (int i 1; i half; i) { for (int j 1; j half; j) { table[i][j half] table[i half][j] - half; } }5.2 完整的遞推實(shí)現(xiàn)代碼#include iostream #include vector #include cmath #include iomanip using namespace std; int main() { int k; cout 請(qǐng)輸入k (選手?jǐn)?shù)2^k): ; cin k; int n pow(2, k); int days n - 1; // 創(chuàng)建日程表大小為 (n1) x (n)多一列方便處理第0列不用或用于存儲(chǔ)自身編號(hào) vectorvectorint table(n 1, vectorint(n 1, 0)); // 初始化只有2位選手時(shí) table[1][1] 2; // 選手1在第1天對(duì)陣2 table[2][1] 1; // 選手2在第1天對(duì)陣1 int currentSize 2; // 當(dāng)前已構(gòu)造好的小日程表規(guī)模 while (currentSize n) { int half currentSize; // 擴(kuò)展日程表規(guī)模 // 1. 填充右下角 (左下角區(qū)域的下半部分右上角區(qū)域的右半部分) for (int i 1; i half; i) { for (int j 1; j half; j) { table[i half][j half] table[i][j]; } } // 2. 填充左下角 for (int i 1; i half; i) { for (int j 1; j half; j) { // 左下角[ihalf][j] 右上角[i][jhalf] half // 但初始時(shí)右上角可能還沒(méi)值我們用另一種等價(jià)形式 // 左下角的值是左上角對(duì)應(yīng)位置的值加上half // 但更標(biāo)準(zhǔn)的做法是利用已經(jīng)存在的右上角關(guān)系這里采用經(jīng)典構(gòu)造法 table[i half][j] table[i][j] half; } } // 3. 填充右上角 (根據(jù)對(duì)稱性左下角的值減去half) for (int i 1; i half; i) { for (int j 1; j half; j) { table[i][j half] table[i half][j]; } } currentSize * 2; } // 輸出結(jié)果忽略第0列 cout \n循環(huán)賽日程表 (遞推法, n n ):\n; cout setw(6) Player; for (int d 1; d days; d) { cout setw(6) Day d; } cout endl; for (int i 1; i n; i) { cout setw(6) i; for (int d 1; d days; d) { cout setw(8) table[i][d]; } cout endl; } // 驗(yàn)證 vectorvectorbool played(n 1, vectorbool(n 1, false)); bool valid true; for (int i 1; i n; i) { for (int d 1; d days; d) { int j table[i][d]; if (j 1 || j n || j i) { cout 錯(cuò)誤選手 i 在第 d 天對(duì)陣無(wú)效( j )! endl; valid false; } if (played[i][j]) { cout 錯(cuò)誤選手 i 和 j 比賽了多次! endl; valid false; } played[i][j] played[j][i] true; } } // 檢查是否所有配對(duì)都發(fā)生了 for (int i 1; i n; i) { for (int j i 1; j n; j) { if (!played[i][j]) { cout 錯(cuò)誤選手 i 和 j 沒(méi)有比賽! endl; valid false; } } } if (valid) cout \n日程表驗(yàn)證通過(guò) endl; return 0; }5.3 遞推與遞歸的對(duì)比與選擇特性遞歸法遞推法思路自頂向下分而治之自底向上迭代構(gòu)造代碼直觀性更符合問(wèn)題本質(zhì)易于理解需要理解構(gòu)造規(guī)律稍顯抽象空間復(fù)雜度O(n2)但有遞歸調(diào)用棧開銷深度kO(n2)純數(shù)組操作無(wú)額外棧開銷時(shí)間復(fù)雜度O(n2 log n)O(n2)適用場(chǎng)景教學(xué)、理解算法思想、遞歸練習(xí)實(shí)際應(yīng)用、追求性能、避免遞歸深度限制調(diào)試難度調(diào)用棧復(fù)雜跟蹤較難狀態(tài)清晰易于跟蹤矩陣變化選擇建議如果你是學(xué)習(xí)者務(wù)必先徹底搞懂遞歸版本。它揭示了問(wèn)題的分治結(jié)構(gòu)是理解遞推版本的基礎(chǔ)。手動(dòng)模擬n4或n8的遞歸過(guò)程畫出示意圖對(duì)理解大有裨益。如果你在競(jìng)賽或需要高性能的場(chǎng)景遞推版本是更優(yōu)選擇。它常數(shù)因子更小且沒(méi)有遞歸開銷。在實(shí)際工程中如果問(wèn)題規(guī)模k不大比如k15兩者差異不大。但遞推版本通常更受青睞因?yàn)樗苊饬诉f歸可能帶來(lái)的棧溢出風(fēng)險(xiǎn)雖然在此問(wèn)題中風(fēng)險(xiǎn)極低。6. 常見問(wèn)題、調(diào)試技巧與擴(kuò)展思考即使理解了算法實(shí)現(xiàn)時(shí)也難免遇到各種“坑”。這里分享一些我踩過(guò)的坑和調(diào)試技巧。6.1 典型錯(cuò)誤與排查清單數(shù)組越界這是最常見的問(wèn)題。確保你的日程表vector或數(shù)組大小是[n1][n]或[n1][n1]如果從1開始索引。在遞歸或循環(huán)中仔細(xì)檢查所有下標(biāo)是否在有效范圍內(nèi)。配對(duì)重復(fù)或遺漏生成的日程表可能違反基本規(guī)則。立刻寫一個(gè)驗(yàn)證函數(shù)就像上面代碼中那樣。檢查兩個(gè)條件對(duì)于任何選手i和天數(shù)dschedule[i][d]不能是i或0未初始化。對(duì)于任何一對(duì)不同的選手(i, j)schedule[i][d] j的情況必須出現(xiàn)且僅出現(xiàn)一次。同時(shí)對(duì)稱地schedule[j][d] i也應(yīng)該在同一天d發(fā)生d d。遞歸合并邏輯錯(cuò)誤如果使用遞歸合并部分的循環(huán)邏輯最容易出錯(cuò)。對(duì)于n4手動(dòng)在紙上推導(dǎo)出正確的schedule矩陣然后單步調(diào)試你的代碼對(duì)比每一步的結(jié)果。重點(diǎn)關(guān)注halfSize的計(jì)算和合并循環(huán)的邊界。遞推構(gòu)造順序錯(cuò)誤遞推法中三個(gè)復(fù)制步驟的順序有時(shí)會(huì)導(dǎo)致錯(cuò)誤。務(wù)必理解先復(fù)制右下角基于左上角然后處理左下角和右上角它們互相關(guān)聯(lián)。可以嘗試不同的初始化和步驟順序用n4測(cè)試。輸出格式混亂當(dāng)n較大時(shí)控制臺(tái)輸出可能錯(cuò)位。使用setw()等格式化輸出函數(shù)或者考慮將結(jié)果寫入文件查看。6.2 調(diào)試技巧實(shí)錄最小化測(cè)試從k1(n2) 開始測(cè)試然后k2(n4)。這是調(diào)試的黃金法則。n4的日程表足夠小可以手動(dòng)計(jì)算并驗(yàn)證。打印中間狀態(tài)在遞歸函數(shù)的關(guān)鍵點(diǎn)進(jìn)入、返回前、合并后打印當(dāng)前的left,right,currentSize以及部分日程表內(nèi)容。在遞推法中每完成一次size翻倍就打印出當(dāng)前的整個(gè)table矩陣。使用調(diào)試器在IDE中設(shè)置斷點(diǎn)觀察變量變化。特別是觀察合并循環(huán)中i,t,playerLeft,playerRight,day等變量的值是否符合預(yù)期。對(duì)稱性檢查一個(gè)有效的日程表必須滿足對(duì)稱性schedule[i][d] j當(dāng)且僅當(dāng)schedule[j][d] i。寫一個(gè)快速檢查函數(shù)遍歷所有i,d驗(yàn)證這一點(diǎn)。6.3 問(wèn)題擴(kuò)展與變種選手?jǐn)?shù)不是2的冪怎么辦這是更實(shí)際的問(wèn)題。常見的處理方法是引入“輪空”。可以找到大于等于n的最小的2的冪m然后虛擬m-n個(gè)不存在的選手。當(dāng)某位真實(shí)選手抽到與虛擬選手對(duì)戰(zhàn)時(shí)即表示該輪輪空。這需要稍微修改輸出邏輯。多場(chǎng)地并行比賽如果每天有多個(gè)場(chǎng)地同時(shí)進(jìn)行比賽問(wèn)題就變成了如何將日程表中的比賽分配到不同場(chǎng)地同時(shí)避免同一選手在同一時(shí)間出現(xiàn)在兩個(gè)場(chǎng)地。這引入了額外的約束可以建模為圖著色或匹配問(wèn)題。主客場(chǎng)制在循環(huán)賽中有時(shí)需要考慮主客場(chǎng)。這要求每對(duì)選手賽兩場(chǎng)一主一客??梢栽谏蓡窝h(huán)日程表后將其復(fù)制并反轉(zhuǎn)拼接成一個(gè)雙循環(huán)日程表并為主客場(chǎng)分配不同的標(biāo)識(shí)。與格雷碼的聯(lián)系仔細(xì)觀察遞推的構(gòu)造過(guò)程你會(huì)發(fā)現(xiàn)選手編號(hào)的變換與格雷碼Gray Code的生成有異曲同工之妙。這揭示了問(wèn)題背后深刻的組合數(shù)學(xué)原理。6.4 個(gè)人心得與踩坑總結(jié)最后分享幾點(diǎn)從紙上談兵到代碼跑通的心得畫圖畫圖畫圖對(duì)于遞歸分治問(wèn)題沒(méi)有比畫出一棵遞歸樹和每次合并后的矩陣狀態(tài)更直觀的理解方式了。用紙筆模擬n8的過(guò)程你會(huì)對(duì)算法有全新的認(rèn)識(shí)?!皬?fù)制”不是“賦值”在遞推法中table[ihalf][jhalf] table[i][j]是值的復(fù)制。但在理解上它意味著下半?yún)^(qū)內(nèi)部比賽的“模式”與上半?yún)^(qū)相同。這種“模式復(fù)制”的思想是許多分治算法的精髓。邊界條件是你的朋友遞歸的終止條件 (currentSize 2) 和遞推的初始狀態(tài) (size2) 必須絕對(duì)正確。這里錯(cuò)了后面全盤皆輸。花時(shí)間確保它們?nèi)f無(wú)一失。驗(yàn)證代碼的價(jià)值大于實(shí)現(xiàn)代碼花20%的時(shí)間寫驗(yàn)證邏輯可以節(jié)省你80%的調(diào)試時(shí)間。一個(gè)健壯的驗(yàn)證函數(shù)能讓你對(duì)代碼的正確性充滿信心。從具體到抽象不要一開始就想著n1024。從n2,4,8這些具體例子入手總結(jié)出規(guī)律然后再推廣到一般情況。這是學(xué)習(xí)算法最踏實(shí)的方法。循環(huán)賽日程表這個(gè)問(wèn)題就像一把鑰匙打開了一類問(wèn)題的門如何利用自相似性和對(duì)稱性通過(guò)遞歸或遞推高效構(gòu)造復(fù)雜結(jié)構(gòu)。無(wú)論是快速傅里葉變換FFT的蝶形運(yùn)算還是歸并排序的分治策略都能看到類似思想的影子。把它吃透絕對(duì)值回票價(jià)。

相關(guān)新聞

內(nèi)存尋址核心:片選、行通選與列通選的工作原理與協(xié)同機(jī)制

內(nèi)存尋址核心:片選、行通選與列通選的工作原理與協(xié)同機(jī)制

1. 項(xiàng)目概述:從“選”字入手,理解內(nèi)存尋址的基石 最近在復(fù)盤一些計(jì)算機(jī)組成原理和數(shù)字電路的老知識(shí)時(shí),發(fā)現(xiàn)“行通選”、“列通選”和“片選線”這幾個(gè)概念,雖然名字聽起來(lái)有點(diǎn)“古早”,但它們是理解現(xiàn)代內(nèi)存&#xff0…

2026/8/2 13:56:11 閱讀更多
Spring Boot 3.4 集成阿里云 PAI-EAS 出現(xiàn)連接池耗盡問(wèn)題的排查與修復(fù)

Spring Boot 3.4 集成阿里云 PAI-EAS 出現(xiàn)連接池耗盡問(wèn)題的排查與修復(fù)

Spring Boot 3.4 集成阿里云 PAI-EAS 出現(xiàn)連接池耗盡問(wèn)題的排查與修復(fù)昨天凌晨?jī)牲c(diǎn),監(jiān)控平臺(tái)突然告警,生產(chǎn)環(huán)境的 AI 推理服務(wù)響應(yīng)時(shí)間從正常的 200ms 飆升到 5000ms,隨后大量請(qǐng)求直接超時(shí)。查看應(yīng)用日志,滿屏都是 org.apache.htt…

2026/8/2 13:56:11 閱讀更多
從零構(gòu)建虛幻引擎Pak文件解析器:原理、實(shí)現(xiàn)與實(shí)戰(zhàn)應(yīng)用

從零構(gòu)建虛幻引擎Pak文件解析器:原理、實(shí)現(xiàn)與實(shí)戰(zhàn)應(yīng)用

1. 項(xiàng)目概述:為什么我們需要一個(gè)Pak文件解析器?如果你是一名虛幻引擎(Unreal Engine)的開發(fā)者,無(wú)論是從事游戲開發(fā)、虛擬仿真還是數(shù)字孿生項(xiàng)目,那么“Pak”文件對(duì)你來(lái)說(shuō)一定不陌生。它就像是虛幻引擎世界的…

2026/8/2 13:56:11 閱讀更多
AI繪圖工具“隱性門檻”大起底:提示詞工程兼容性、LoRA加載失敗率、NSFW過(guò)濾激進(jìn)度、多輪迭代一致性——5大暗藏雷區(qū)逐項(xiàng)爆破

AI繪圖工具“隱性門檻”大起底:提示詞工程兼容性、LoRA加載失敗率、NSFW過(guò)濾激進(jìn)度、多輪迭代一致性——5大暗藏雷區(qū)逐項(xiàng)爆破

更多請(qǐng)點(diǎn)擊: https://kaifayun.com 第一章:AI繪圖工具“隱性門檻”全景認(rèn)知 AI繪圖工具表面“一鍵生成”,實(shí)則暗藏多重隱性門檻——它們不寫在官網(wǎng)文檔里,卻真實(shí)制約著創(chuàng)作效率、輸出質(zhì)量與工作流整合能力。這些門檻并非技術(shù)黑箱…

2026/8/2 15:06:17 閱讀更多
嵌入式HDMI LCD屏幕硬件連接與Linux系統(tǒng)配置實(shí)戰(zhàn)指南

嵌入式HDMI LCD屏幕硬件連接與Linux系統(tǒng)配置實(shí)戰(zhàn)指南

1. 項(xiàng)目概述:11.6英寸HDMI LCD屏幕的定位與價(jià)值最近在折騰一個(gè)嵌入式顯示項(xiàng)目,手頭正好用上了一塊11.6英寸的HDMI接口LCD屏幕。這玩意兒在創(chuàng)客圈、工業(yè)HMI(人機(jī)界面)開發(fā),甚至是DIY便攜顯示終端領(lǐng)域,出場(chǎng)率…

2026/8/2 15:06:17 閱讀更多
AI協(xié)同設(shè)計(jì)師、數(shù)字孿生運(yùn)維專員、多模態(tài)內(nèi)容策展人…這6個(gè)冷門但年薪超60萬(wàn)的新型崗位,你聽說(shuō)幾個(gè)?

AI協(xié)同設(shè)計(jì)師、數(shù)字孿生運(yùn)維專員、多模態(tài)內(nèi)容策展人…這6個(gè)冷門但年薪超60萬(wàn)的新型崗位,你聽說(shuō)幾個(gè)?

更多請(qǐng)點(diǎn)擊: https://kaifayun.com 第一章:AI協(xié)同設(shè)計(jì)師、數(shù)字孿生運(yùn)維專員、多模態(tài)內(nèi)容策展人…這6個(gè)冷門但年薪超60萬(wàn)的新型崗位,你聽說(shuō)幾個(gè)? 當(dāng)大模型落地進(jìn)入深水區(qū),企業(yè)用人邏輯正悄然重構(gòu)——不再只招“會(huì)寫Pro…

2026/8/2 15:06:17 閱讀更多
硅谷AI競(jìng)賽下的“002”工作模式:從技術(shù)狂熱到人才逃亡的行業(yè)反思

硅谷AI競(jìng)賽下的“002”工作模式:從技術(shù)狂熱到人才逃亡的行業(yè)反思

1. 從“996”到“002”:硅谷AI競(jìng)賽下的生存狀態(tài)異化 最近和幾個(gè)還在硅谷大廠和前沿AI實(shí)驗(yàn)室的朋友聊天,話題總繞不開一個(gè)詞: “burnout” (職業(yè)倦怠)。但這次的“倦怠”和以往我們理解的“996”完全不同。過(guò)去國(guó)內(nèi)互…

2026/8/2 15:06:17 閱讀更多
CUHK-SYSU行人搜索數(shù)據(jù)集:從原理到實(shí)戰(zhàn)的完整指南

CUHK-SYSU行人搜索數(shù)據(jù)集:從原理到實(shí)戰(zhàn)的完整指南

1. 項(xiàng)目概述:為什么我們需要CUHK-SYSU行人搜索數(shù)據(jù)集?在計(jì)算機(jī)視覺(jué)領(lǐng)域,尤其是安防監(jiān)控、智能零售和智慧城市等應(yīng)用場(chǎng)景中,“行人搜索”是一個(gè)既基礎(chǔ)又極具挑戰(zhàn)性的任務(wù)。它不同于單純的行人檢測(cè)(只框出畫面中的人&…

2026/8/2 15:06:17 閱讀更多
OpenClaw智能體開發(fā)實(shí)戰(zhàn):6款核心工具深度評(píng)測(cè)與自動(dòng)化工作流構(gòu)建指南

OpenClaw智能體開發(fā)實(shí)戰(zhàn):6款核心工具深度評(píng)測(cè)與自動(dòng)化工作流構(gòu)建指南

1. 項(xiàng)目概述:OpenClaw生態(tài)與“最佳工具榜”的價(jià)值 最近在AI智能體開發(fā)圈子里,OpenClaw的熱度持續(xù)攀升,幾乎成了每個(gè)想嘗試AI自動(dòng)化流程的開發(fā)者繞不開的名字。它本質(zhì)上是一個(gè)開源的AI智能體框架,你可以把它理解為一個(gè)“AI大腦”的…

2026/8/2 14:56:17 閱讀更多
MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案 【免費(fèi)下載鏈接】MoneyPrinterPlus AI一鍵批量生成各類短視頻,自動(dòng)批量混剪短視頻,自動(dòng)把視頻發(fā)布到抖音,快手,小紅書,視頻號(hào)上,賺錢從來(lái)沒(méi)有這么容易過(guò)! 支持本地語(yǔ)音模型chatTTS,fasterwhisper,…

2026/8/2 0:04:00 閱讀更多
3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南

3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南

3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南 【免費(fèi)下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說(shuō)說(shuō) 項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過(guò),那些年發(fā)過(guò)的QQ空間說(shuō)說(shuō),那些記錄青春的文字…

2026/8/2 0:04:01 閱讀更多
MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案 【免費(fèi)下載鏈接】MoneyPrinterPlus AI一鍵批量生成各類短視頻,自動(dòng)批量混剪短視頻,自動(dòng)把視頻發(fā)布到抖音,快手,小紅書,視頻號(hào)上,賺錢從來(lái)沒(méi)有這么容易過(guò)! 支持本地語(yǔ)音模型chatTTS,fasterwhisper,…

2026/8/2 0:04:00 閱讀更多
3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南

3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南

3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南 【免費(fèi)下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說(shuō)說(shuō) 項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過(guò),那些年發(fā)過(guò)的QQ空間說(shuō)說(shuō),那些記錄青春的文字…

2026/8/2 0:04:01 閱讀更多
AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O分配PCB板是應(yīng)用材料(Applied Materials)公司生產(chǎn)的一款用于半導(dǎo)體設(shè)備的I/O信號(hào)分配電路板。該型號(hào)(0100-02186)的核心特點(diǎn)如下:專用于Endura等半導(dǎo)體工藝腔室。集成信號(hào)路由與分配功能。連接控制…

2026/8/2 2:51:21 閱讀更多
Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)是日本日清(Nissei)品牌的一款工業(yè)用三相異步電機(jī),適用于自動(dòng)化設(shè)備及通用機(jī)械驅(qū)動(dòng)。該型號(hào)(FFMN-32L-10-T0 40AX)的核心特點(diǎn)如下:三相交流異步電動(dòng)機(jī)。額定…

2026/8/2 2:52:49 閱讀更多