国产精品久久久aaaa,日日干夜夜操天天插,亚洲乱熟女香蕉一区二区三区少妇,99精品国产高清一区二区三区,国产成人精品一区二区色戒,久久久国产精品成人免费,亚洲精品毛片久久久久,99久久婷婷国产综合精品电影,国产一区二区三区任你鲁

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

關于回溯算法的介紹與運用

算法與數據結構 ? 來源:CSDN技術社區 ? 作者:labuladong ? 2021-03-25 13:42 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

之前說過回溯算法是筆試中最好用的算法,只要你沒什么思路,就用回溯算法暴力求解,即便不能通過所有測試用例,多少能過一點。

回溯算法的技巧也不難,前文 回溯算法框架套路 說過,回溯算法就是窮舉一棵決策樹的過程,只要在遞歸之前「做選擇」,在遞歸之后「撤銷選擇」就行了。

但是,就算暴力窮舉,不同的思路也有優劣之分。

本文就來看一道非常經典的回溯算法問題,子集劃分問題,可以幫你更深刻理解回溯算法的思維,得心應手地寫出回溯函數。

題目非常簡單:

給你輸入一個數組nums和一個正整數k,請你判斷nums是否能夠被平分為元素和相同的k個子集。

函數簽名如下:

boolean canPartitionKSubsets(int[] nums, int k);

我們之前 背包問題之子集劃分 寫過一次子集劃分問題,不過那道題只需要我們把集合劃分成兩個相等的集合,可以轉化成背包問題用動態規劃技巧解決。

但是如果劃分成多個相等的集合,解法一般只能通過暴力窮舉,時間復雜度爆表,是練習回溯算法和遞歸思維的好機會。

一、思路分析

把裝有n個數字的數組nums分成k個和相同的集合,你可以想象將n個數字分配到k個「桶」里,最后這k個「桶」里的數字之和要相同。

前文 回溯算法框架套路 說過,回溯算法的關鍵在哪里?

關鍵是要知道怎么「做選擇」,這樣才能利用遞歸函數進行窮舉。

那么回想我們這個問題,將n個數字分配到k個桶里,我們可以有兩種視角:

視角一,如果我們切換到這n個數字的視角,每個數字都要選擇進入到k個桶中的某一個。

視角二,如果我們切換到這k個桶的視角,對于每個桶,都要遍歷nums中的n個數字,然后選擇是否將當前遍歷到的數字裝進自己這個桶里。

你可能問,這兩種視角有什么不同?

用不同的視角進行窮舉,雖然結果相同,但是解法代碼的邏輯完全不同;對比不同的窮舉視角,可以幫你更深刻地理解回溯算法,我們慢慢道來。

二、以數字的視角

用 for 循環迭代遍歷nums數組大家肯定都會:

for (int index = 0; index 《 nums.length; index++) {

System.out.println(nums[index]);

}

遞歸遍歷數組你會不會?其實也很簡單:

void traverse(int[] nums, int index) {

if (index == nums.length) {

return;

}

System.out.println(nums[index]);

traverse(nums, index + 1);

}

只要調用traverse(nums, 0),和 for 循環的效果是完全一樣的。

那么回到這道題,以數字的視角,選擇k個桶,用 for 循環寫出來是下面這樣:

// k 個桶(集合),記錄每個桶裝的數字之和

int[] bucket = new int[k];

// 窮舉 nums 中的每個數字

for (int index = 0; index 《 nums.length; index++) {

// 窮舉每個桶

for (int i = 0; i 《 k; i++) {

// nums[index] 選擇是否要進入第 i 個桶

// 。..

}

}

如果改成遞歸的形式,就是下面這段代碼邏輯:

// k 個桶(集合),記錄每個桶裝的數字之和

int[] bucket = new int[k];

// 窮舉 nums 中的每個數字

void backtrack(int[] nums, int index) {

// base case

if (index == nums.length) {

return;

}

// 窮舉每個桶

for (int i = 0; i 《 bucket.length; i++) {

// 選擇裝進第 i 個桶

bucket[i] += nums[index];

// 遞歸窮舉下一個數字的選擇

backtrack(nums, index + 1);

// 撤銷選擇

bucket[i] -= nums[index];

}

}

雖然上述代碼僅僅是窮舉邏輯,還不能解決我們的問題,但是只要略加完善即可:

// 主函數

public boolean canPartitionKSubsets(int[] nums, int k) {

// 排除一些基本情況

if (k 》 nums.length) return false;

int sum = 0;

for (int v : nums) sum += v;

if (sum % k != 0) return false;

// k 個桶(集合),記錄每個桶裝的數字之和

int[] bucket = new int[k];

// 理論上每個桶(集合)中數字的和

int target = sum / k;

// 窮舉,看看 nums 是否能劃分成 k 個和為 target 的子集

return backtrack(nums, 0, bucket, target);

}

// 遞歸窮舉 nums 中的每個數字

boolean backtrack(

int[] nums, int index, int[] bucket, int target) {

if (index == nums.length) {

// 檢查所有桶的數字之和是否都是 target

for (int i = 0; i 《 bucket.length; i++) {

if (bucket[i] != target) {

return false;

}

}

// nums 成功平分成 k 個子集

return true;

}

// 窮舉 nums[index] 可能裝入的桶

for (int i = 0; i 《 bucket.length; i++) {

// 剪枝,桶裝裝滿了

if (bucket[i] + nums[index] 》 target) {

continue;

}

// 將 nums[index] 裝入 bucket[i]

bucket[i] += nums[index];

// 遞歸窮舉下一個數字的選擇

if (backtrack(nums, index + 1, bucket, target)) {

return true;

}

// 撤銷選擇

bucket[i] -= nums[index];

}

// nums[index] 裝入哪個桶都不行

return false;

}

有之前的鋪墊,相信這段代碼是比較容易理解的。這個解法雖然能夠通過,但是耗時比較多,其實我們可以再做一個優化。

主要看backtrack函數的遞歸部分:

for (int i = 0; i 《 bucket.length; i++) {

// 剪枝

if (bucket[i] + nums[index] 》 target) {

continue;

}

if (backtrack(nums, index + 1, bucket, target)) {

return true;

}

}

如果我們讓盡可能多的情況命中剪枝的那個 if 分支,就可以減少遞歸調用的次數,一定程度上減少時間復雜度。

如何盡可能多的命中這個 if 分支呢?要知道我們的index參數是從 0 開始遞增的,也就是遞歸地從 0 開始遍歷nums數組。

如果我們提前對nums數組排序,把大的數字排在前面,那么大的數字會先被分配到bucket中,對于之后的數字,bucket[i] + nums[index]會更大,更容易觸發剪枝的 if 條件。

所以可以在之前的代碼中再添加一些代碼:

public boolean canPartitionKSubsets(int[] nums, int k) {

// 其他代碼不變

// 。..

/* 降序排序 nums 數組 */

Arrays.sort(nums);

int i = 0, j = nums.length - 1;

for (; i 《 j; i++, j--) {

// 交換 nums[i] 和 nums[j]

int temp = nums[i];

nums[i] = nums[j];

nums[j] = temp;

}

/*******************/

return backtrack(nums, 0, bucket, target);

}

由于 Java 的語言特性,這段代碼通過先升序排序再反轉,達到降序排列的目的。

三、以桶的視角

文章開頭說了,以桶的視角進行窮舉,每個桶需要遍歷nums中的所有數字,決定是否把當前數字裝進桶中;當裝滿一個桶之后,還要裝下一個桶,直到所有桶都裝滿為止。

這個思路可以用下面這段代碼表示出來:

// 裝滿所有桶為止

while (k 》 0) {

// 記錄當前桶中的數字之和

int bucket = 0;

for (int i = 0; i 《 nums.length; i++) {

// 決定是否將 nums[i] 放入當前桶中

bucket += nums[i] or 0;

if (bucket == target) {

// 裝滿了一個桶,裝下一個桶

k--;

break;

}

}

}

那么我們也可以把這個 while 循環改寫成遞歸函數,不過比剛才略微復雜一些,首先寫一個backtrack遞歸函數出來:

boolean backtrack(int k, int bucket,

int[] nums, int start, boolean[] used, int target);

不要被這么多參數嚇到,我會一個個解釋這些參數。如果你能夠透徹理解本文,也能得心應手地寫出這樣的回溯函數。

這個backtrack函數的參數可以這樣解釋:

現在k號桶正在思考是否應該把nums[start]這個元素裝進來;目前k號桶里面已經裝的數字之和為bucket;used標志某一個元素是否已經被裝到桶中;target是每個桶需要達成的目標和。

根據這個函數定義,可以這樣調用backtrack函數:

public boolean canPartitionKSubsets(int[] nums, int k) {

// 排除一些基本情況

if (k 》 nums.length) return false;

int sum = 0;

for (int v : nums) sum += v;

if (sum % k != 0) return false;

boolean[] used = new boolean[nums.length];

int target = sum / k;

// k 號桶初始什么都沒裝,從 nums[0] 開始做選擇

return backtrack(k, 0, nums, 0, used, target);

}

實現backtrack函數的邏輯之前,再重復一遍,從桶的視角:

1、需要遍歷nums中所有數字,決定哪些數字需要裝到當前桶中。

2、如果當前桶裝滿了(桶內數字和達到target),則讓下一個桶開始執行第 1 步。

下面的代碼就實現了這個邏輯:

boolean backtrack(int k, int bucket,

int[] nums, int start, boolean[] used, int target) {

// base case

if (k == 0) {

// 所有桶都被裝滿了,而且 nums 一定全部用完了

// 因為 target == sum / k

return true;

}

if (bucket == target) {

// 裝滿了當前桶,遞歸窮舉下一個桶的選擇

// 讓下一個桶從 nums[0] 開始選數字

return backtrack(k - 1, 0 ,nums, 0, used, target);

}

// 從 start 開始向后探查有效的 nums[i] 裝入當前桶

for (int i = start; i 《 nums.length; i++) {

// 剪枝

if (used[i]) {

// nums[i] 已經被裝入別的桶中

continue;

}

if (nums[i] + bucket 》 target) {

// 當前桶裝不下 nums[i]

continue;

}

// 做選擇,將 nums[i] 裝入當前桶中

used[i] = true;

bucket += nums[i];

// 遞歸窮舉下一個數字是否裝入當前桶

if (backtrack(k, bucket, nums, i + 1, used, target)) {

return true;

}

// 撤銷選擇

used[i] = false;

bucket -= nums[i];

}

// 窮舉了所有數字,都無法裝滿當前桶

return false;

}

至此,這道題的第二種思路也完成了。

四、最后總結

本文寫的這兩種思路都可以通過所有測試用例,不過第一種解法即便經過了排序優化,也明顯比第二種解法慢很多,這是為什么呢?

我們來分析一下這兩個算法的時間復雜度,假設nums中的元素個數為n。

先說第一個解法,也就是從數字的角度進行窮舉,n個數字,每個數字有k個桶可供選擇,所以組合出的結果個數為k^n,時間復雜度也就是O(k^n)。

第二個解法,每個桶要遍歷n個數字,選擇「裝入」或「不裝入」,組合的結果有2^n種;而我們有k個桶,所以總的時間復雜度為O(k*2^n)。

當然,這是理論上的最壞復雜度,實際的復雜度肯定要好一些,畢竟我們添加了這么多剪枝邏輯。不過,從復雜度的上界已經可以看出第一種思路要慢很多了。

所以,誰說回溯算法沒有技巧性的?雖然回溯算法就是暴力窮舉,但窮舉也分聰明的窮舉方式和低效的窮舉方式,關鍵看你以誰的「視角」進行窮舉。

通俗來說,我們應該盡量「少量多次」,就是說寧可多做幾次選擇,也不要給太大的選擇空間;寧可「二選一」選k次,也不要 「k選一」選一次。

這道題我們從兩種視角進行窮舉,雖然代碼量看起來多,但核心邏輯都是類似的,相信你通過本文能夠更深刻地理解回溯算法。
編輯:lyn

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 函數
    +關注

    關注

    3

    文章

    4417

    瀏覽量

    67501
  • 回溯算法
    +關注

    關注

    0

    文章

    10

    瀏覽量

    6752

原文標題:回溯算法牛逼!

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數據結構】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關推薦
    熱點推薦

    小華半導體數字電源算法配置工具DPACT介紹

    小華半導體數字電源算法配置工具(DPACT)是一款基于公司豐富參考設計方案,專為電力電子控制算法開發而設計的圖形化開發工具。該工具深度集成XHCODE底層配置環境,支持用戶以圖形化方式,快速在現有
    的頭像 發表于 02-11 11:28 ?296次閱讀
    小華半導體數字電源<b class='flag-5'>算法</b>配置工具DPACT<b class='flag-5'>介紹</b>

    關于MT6901的直線DEMO介紹

    關于MT6901的直線DEMO介紹
    的頭像 發表于 01-30 10:54 ?398次閱讀
    <b class='flag-5'>關于</b>MT6901的直線DEMO<b class='flag-5'>介紹</b>

    【OFDR】實時感知、動態重構與歷史狀態回溯!昊衡科技-三維場重構軟件

    路徑映射三維螺旋路徑映射支持TCP實時數據傳輸,支持導入本地TXT數據,對試驗過程進行回溯分析,方便后期數據復盤與優化。數據回放功能界面從實時數據采集到三維場可視化,再
    的頭像 發表于 01-29 17:40 ?1295次閱讀
    【OFDR】實時感知、動態重構與歷史狀態<b class='flag-5'>回溯</b>!昊衡科技-三維場重構軟件

    回溯示波器的四次認知躍遷

    最近示波器圈熱鬧得有些“魔幻”,我抱著科普心態湊了幾次熱點,關于某款國產示波器的內容卻接連被申退。倒也理解,行業風口下的爭議本就難免。但比起糾結單一產品,或許我們更該靜下心來聊聊:這個被稱為電子
    的頭像 發表于 12-19 15:39 ?6501次閱讀
    <b class='flag-5'>回溯</b>示波器的四次認知躍遷

    關于NFC鎳鋅鐵氧體片的介紹

    關于NFC鎳鋅鐵氧體片的介紹
    的頭像 發表于 12-04 10:52 ?405次閱讀
    <b class='flag-5'>關于</b>NFC鎳鋅鐵氧體片的<b class='flag-5'>介紹</b>

    關于系統鏈接腳本的介紹

    一、隊伍介紹 本篇為蜂鳥E203系列分享第四篇,本篇介紹的內容是系統鏈接腳本。 二、如何實現不同的下載模式? 實現三種不同的程序運行方式,可通過makefile的命令行指定不同的鏈接腳本,從而實現
    發表于 10-30 08:26

    AES加解密算法邏輯實現及其在蜂鳥E203SoC上的應用介紹

    這次分享我們會簡要介紹AES加解密算法的邏輯實現,以及如何將AES算法做成硬件協處理器集成在蜂鳥E203 SoC上。 AES算法介紹 AE
    發表于 10-29 07:29

    基于E203 RISC-V的音頻信號處理系統 -ANC算法簡介

    ANC算法介紹 主動降噪系統在移動終端中應用最廣,例如摩托的麗音、三星Diamond Voice、蘋果的Micphone Array等。最早提出使用聲波干涉原理進行噪聲消除概念的是Rayleigh
    發表于 10-28 07:50

    國密系列算法簡介及SM4算法原理介紹

    保證,而國產密碼算法實現了密碼算法的自主可控,對于保障我國的國家安全具有重要意義。目前,我國大力推廣國密算法的應用,并涌現出一系列國家商用密碼應用的優秀案例。 本文將對SM4算法的原理
    發表于 10-24 08:25

    加密算法的應用

    加密是一種保護信息安全的重要手段,近年來隨著信息技術的發展,加密技術的應用越來越廣泛。本文將介紹加密算法的發展、含義、分類及應用場景。 1. 加密算法的發展 加密算法的歷史可以追
    發表于 10-24 08:03

    Montgomery模乘介紹

    Montgomery模乘介紹 Montgomery 模乘算法是最有效的大整數模乘算法之一它的一個顯著特點是消除了mod n 的除法運算。Montgomery 算法的基本思想是計算 ,設
    發表于 10-22 07:35

    DFT算法與FFT算法的優劣分析

    算法之間有什么不同,采用相關算法的依據。下面就來介紹一下兩種算法的不同以及適用的一些場合。 DFT算法,是連續傅里葉變換在時域和頻域上都離散
    的頭像 發表于 08-04 09:30 ?1396次閱讀

    生產線回溯追溯系統選型:中設智控方案如何破解行業痛點?

    中設智控產線回溯追溯方案,從硬件到功能,精準破解行業痛點,為電子制造、新能源等行業提供高效、可靠的生產管理工具,助力企業實現智能化生產升級,值得選型參考。
    的頭像 發表于 07-18 11:19 ?962次閱讀
    生產線<b class='flag-5'>回溯</b>追溯系統選型:中設智控方案如何破解行業痛點?

    基于FPGA實現FOC算法之PWM模塊設計

    哈嘍,大家好,從今天開始正式帶領大家從零到一,在FPGA平臺上實現FOC算法,整個算法的框架如下圖所示,如果大家對算法的原理不是特別清楚的話,可以先去百度上學習一下,本教程著重介紹實現
    的頭像 發表于 07-17 15:21 ?3490次閱讀
    基于FPGA實現FOC<b class='flag-5'>算法</b>之PWM模塊設計

    山東LP-SCADA故障回溯功能的好處

    關鍵字:LP-SCADA, LP-SCADA平臺 , LP-SCADA系統, 軟件回溯功能,藍鵬測控 得益于本平臺毫秒級的采集延遲,本平臺除了具有普通監控采集平臺的所有監控功能外,還可用于產線、設備
    發表于 05-29 14:42