《Hello 演算法》最大容量問題:雙指標貪婪策略的推導、實現與正確性證明
《Hello 演算法》最大容量問題雙指標貪婪策略的推導、實現與正確性證明【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo本篇技術指南圍繞《Hello 演算法》貪婪章節中的經典例題「最大容量問題」展開系統講解如何從暴力窮舉 $O(n^2)$ 優化到雙指標貪婪的 $O(n)$ 解法並結合 hello-algo 倉庫內的多語言源碼C、Python、Java、Go、TypeScript 等展示完整可運行的實作最後給出嚴謹的貪婪正確性證明。讀完本文你將掌握「何時可以安全地跳過狀態」這類貪婪選擇性質的判斷方法並能在真實程式碼中複現該演算法。問題描述與數學建模!!! question輸入一個陣列 $ht$ 其中的每個元素代表一個垂直隔板的高度。陣列中的任意兩個隔板以及它們之間的空間可以組成一個容器。 容器的容量等於高度和寬度的乘積面積其中高度由較短的隔板決定寬度是兩個隔板的陣列索引之差。 請在陣列中選擇兩個隔板使得組成的容器的容量最大返回最大容量。示例如下圖所示。狀態定義一對隔板索引容器由任意兩個隔板圍成因此本題的狀態為兩個隔板的索引記為 $[i, j]$。與「最長上升子序列」等需要記錄單一位置狀態的題目不同本題的任何候選解都是一個二元組狀態空間是陣列中所有索引對的集合。容量計算公式根據題意容量等於高度乘以寬度其中高度由短板決定寬度是兩隔板的陣列索引之差。設容量為 $cap[i, j]$ 則可得計算公式$$ cap[i, j] \min(ht[i], ht[j]) \times (j - i) $$這個公式包含兩個關鍵觀察高度取短板$ht[i]$ 與 $ht[j]$ 中的較小值決定了容器的有效高度長板多餘的部分對容量沒有任何貢獻寬度取索引差兩個隔板之間的水平距離等於 $j - i$這裡約定 $i j$。暴力窮舉$O(n^2)$ 的樸素解法設陣列長度為 $n$ 兩個隔板的組合數量狀態總數為 $C_n^2 \frac{n(n - 1)}{2}$ 個。最直接地我們可以窮舉所有狀態從而求得最大容量時間複雜度為 $O(n^2)$ 。具體做法是使用雙重迴圈遍歷所有 $i j$ 的索引對逐一計算容量並更新最大值。當 $n$ 較大時$\frac{n(n-1)}{2}$ 個狀態的計算量會快速膨脹例如 $n 10^4$ 時就需要約 $5 \times 10^7$ 次計算。這說明需要尋找更高效率的解法。貪婪策略確定為什麼「移動短板」才是關鍵這道題還有更高效率的解法。如下圖所示現選取一個狀態 $[i, j]$ 其滿足索引 $i j$ 且高度 $ht[i] ht[j]$ 即 $i$ 為短板、$j$ 為長板。內移長板容量一定變小如下圖所示若此時將長板 $j$ 向短板 $i$ 靠近則容量一定變小。這是因為在移動長板 $j$ 後寬度 $j-i$ 肯定變小而高度由短板決定因此高度只可能不變 $i$ 仍為短板或變小移動後的 $j$ 成為短板。寬度嚴格遞減、高度非增兩者相乘的容量必然不會超過原值——內移長板是一個「只虧不賺」的操作。內移短板容量有可能變大反向思考我們只有向內收縮短板 $i$ 才有可能使容量變大。因為雖然寬度一定變小但高度可能會變大移動後的短板 $i$ 可能會變長。例如在下圖中移動短板後面積變大。由此便可推出本題的貪婪策略初始化兩指標使其分列容器兩端每輪向內收縮短板對應的指標直至兩指標相遇。貪婪策略的執行過程下圖展示了貪婪策略的執行過程每一步都對應一個具體的視覺化狀態初始狀態下指標 $i$ 和 $j$ 分列陣列兩端。計算當前狀態的容量 $cap[i, j]$ 並更新最大容量。比較板 $i$ 和板 $j$ 的高度並將短板向內移動一格。迴圈執行第2.步和第3.步直至 $i$ 和 $j$ 相遇時結束。整個過程共需 $n - 1$ 輪比較每輪收縮一格兩指標從相距 $n-1$ 到相遇每一輪只做常數次運算。程式碼實現與複雜度分析原文件通過max_capacity函式引用代碼。在 hello-algo 倉庫中該演算法已在多種語言中完整實現並配有可執行的 Driver Code核心邏輯完全一致維護左右指標與當前最優值每輪更新容量後移動短板。以 Python 為例codes/python/chapter_greedy/max_capacity.pydef max_capacity(ht: list[int]) - int: 最大容量贪心 # 初始化 i, j使其分列数组两端 i, j 0, len(ht) - 1 # 初始最大容量为 0 res 0 # 循环贪心选择直至两板相遇 while i j: # 更新最大容量 cap min(ht[i], ht[j]) * (j - i) res max(res, cap) # 向内移动短板 if ht[i] ht[j]: i 1 else: j - 1 return res以 C 語言為例codes/c/chapter_greedy/max_capacity.c/* 最大容量贪心 */ int maxCapacity(int ht[], int htLength) { // 初始化 i, j使其分列数组两端 int i 0; int j htLength - 1; // 初始最大容量为 0 int res 0; // 循环贪心选择直至两板相遇 while (i j) { // 更新最大容量 int capacity myMin(ht[i], ht[j]) * (j - i); res myMax(res, capacity); // 向内移动短板 if (ht[i] ht[j]) { i; } else { j--; } } return res; }需要注意的實現細節高度相等時的移動規則當ht[i] ht[j]時程式碼走入else分支移動j即j--。這只是實現上的約定移動任一指標都不影響正確性因為此時無論移動哪一側寬度都會減少且高度不可能增加短路更新res max(res, capacity)保證res始終記錄已掃描狀態中的歷史最大值即使某輪容量變小也不會被遺漏。多語言實現清單該演算法的邏輯在以下語言中均有對應實現可對照閱讀Pythoncodes/python/chapter_greedy/max_capacity.pyJavacodes/java/chapter_greedy/max_capacity.javaCcodes/c/chapter_greedy/max_capacity.cGocodes/go/chapter_greedy/max_capacity.goTypeScriptcodes/typescript/chapter_greedy/max_capacity.ts其餘語言C、C#、Dart、Kotlin、Ruby、Rust、Swift、Zig 等位於 codes 對應目錄的chapter_greedy子目錄下各版本 Driver Code 均使用測試陣列ht [3, 8, 5, 2, 7, 7, 3, 4]可直接編譯運行驗證輸出結果例如 C 版本通過printf(最大容量为 %d\n, res)輸出答案。複雜度分析時間複雜度 $O(n)$程式碼迴圈最多 $n$ 輪每輪只執行常數次比較與運算因此時間複雜度為 $O(n)$ 。相比窮舉法的 $O(n^2)$這是一步數量級的提升空間複雜度 $O(1)$變數 $i$、$j$、$res$ 使用常數大小的額外空間因此空間複雜度為 $O(1)$ 。演算法不需要任何與 $n$ 相關的輔助資料結構。正確性證明被「跳過」的狀態都是次優的之所以貪婪比窮舉更快是因為每輪的貪婪選擇都會「跳過」一些狀態。要證明演算法正確就必須證明被跳過的狀態不可能是最優解。比如在狀態 $cap[i, j]$ 下$i$ 為短板、$j$ 為長板。若貪婪地將短板 $i$ 向內移動一格會導致下圖所示的狀態被「跳過」。這意味著之後無法驗證這些狀態的容量大小。$$ cap[i, i1], cap[i, i2], \dots, cap[i, j-2], cap[i, j-1] $$觀察發現這些被跳過的狀態實際上就是將長板 $j$ 向內移動的所有狀態。前面我們已經證明內移長板一定會導致容量變小。也就是說被跳過的狀態都不可能是最優解跳過它們不會導致錯過最優解。以上分析說明移動短板的操作是「安全」的貪婪策略是有效的。整個證明可以總結為兩步引理固定短板 $i$ 時任何與 $i$ 搭配且索引更靠近的長板位置即 $cap[i, k]$其中 $i k j$容量都小於等於 $cap[i, j]$——因為寬度更小且高度由同一塊短板 $i$ 決定不會更大歸納每一輪移動短板後未被檢驗的狀態集合中永遠不包含最優解因此最後一次更新得到的res就是全域最優容量。與貪婪演算法一般性質的聯繫本題是貪婪演算法「可以保證找到最優解」的典型代表與同章的零錢兌換問題形成鮮明對比。在 greedy_algorithm.md 中可以看到零錢兌換的貪婪策略在部分硬幣組合下如 $coins [1, 20, 50]$、$amt 60$無法得到最優解這說明貪婪選擇性質只有當局部最優選擇始終可以導致全域性最優解時貪婪演算法才能保證得到最優解。最大容量問題正是通過「內移長板必變小」這一關鍵性質滿足了貪婪選擇性質最優子結構原問題的最優解包含子問題的最優解。移動短板後剩下的區間構成規模更小的同構子問題這為歸納證明提供了基礎。因此判斷一個問題能否使用貪婪演算法關鍵在於證明「每步的貪婪選擇是否安全」——即被跳過的狀態是否必然非最優。最大容量問題是練習這種證明思路的理想範例。更多貪婪章節內容可參閱 zh-hant/docs/chapter_greedy 下的 index.md 與 summary.md。總結最大容量問題的狀態是隔板索引對 $[i, j]$容量公式為 $cap[i, j] \min(ht[i], ht[j]) \times (j - i)$暴力窮舉需遍歷 $C_n^2$ 個狀態時間複雜度 $O(n^2)$雙指標貪婪策略僅需 $O(n)$ 時間與 $O(1)$ 空間核心貪婪策略是「每輪移動短板」其正確性依賴於「內移長板容量必然變小」的引理以及「被跳過狀態皆非最優」的歸納證明該演算法在 hello-algo 倉庫中已覆蓋十餘種語言實現均可直接編譯運行驗證。【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考