99精品伊人亚洲|最近国产中文炮友|九草在线视频支援|AV网站大全最新|美女黄片免费观看|国产精品资源视频|精彩无码视频一区|91大神在线后入|伊人终合在线播放|久草综合久久中文

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線(xiàn)課程
  • 觀(guān)看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一道非常經(jīng)典的回溯算法問(wèn)題:子集劃分問(wèn)題

算法與數(shù)據(jù)結(jié)構(gòu) ? 來(lái)源:labuladong ? 作者:labuladong ? 2022-05-07 09:32 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

我經(jīng)常說(shuō)回溯算法是筆試中最好用的算法,只要你沒(méi)什么思路,就用回溯算法暴力求解,即便不能通過(guò)所有測(cè)試用例,多少能過(guò)一點(diǎn)。

回溯算法的技巧也不難,回溯算法就是窮舉一棵決策樹(shù)的過(guò)程,只要在遞歸之前「做選擇」,在遞歸之后「撤銷(xiāo)選擇」就行了。

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

本文就來(lái)看一道非常經(jīng)典的回溯算法問(wèn)題:子集劃分問(wèn)題。這道題可以幫你更深刻理解回溯算法的思維,得心應(yīng)手地寫(xiě)出回溯函數(shù)。

題目非常簡(jiǎn)單:

給你輸入一個(gè)數(shù)組nums和一個(gè)正整數(shù)k,請(qǐng)你判斷nums是否能夠被平分為元素和相同的k個(gè)子集。

函數(shù)簽名如下:

booleancanPartitionKSubsets(int[]nums,intk);

我們之前背包問(wèn)題之子集劃分寫(xiě)過(guò)一次子集劃分問(wèn)題,不過(guò)那道題只需要我們把集合劃分成兩個(gè)相等的集合,可以轉(zhuǎn)化成背包問(wèn)題用動(dòng)態(tài)規(guī)劃技巧解決。

但是如果劃分成多個(gè)相等的集合,解法一般只能通過(guò)暴力窮舉,時(shí)間復(fù)雜度爆表,是練習(xí)回溯算法和遞歸思維的好機(jī)會(huì)。

一、思路分析

首先,我們回顧一下以前學(xué)過(guò)的排列組合知識(shí):

1、P(n, k)(也有很多書(shū)寫(xiě)成A(n, k))表示從n個(gè)不同元素中拿出k個(gè)元素的排列(Permutation/Arrangement)總數(shù);C(n, k)表示從n個(gè)不同元素中拿出k個(gè)元素的組合(Combination)總數(shù)。

2、「排列」和「組合」的主要區(qū)別在于是否考慮順序的差異。

3、排列、組合總數(shù)的計(jì)算公式:

0a216334-cda4-11ec-bce3-dac502259ad0.png

好,現(xiàn)在我問(wèn)一個(gè)問(wèn)題,這個(gè)排列公式P(n, k)是如何推導(dǎo)出來(lái)的?為了搞清楚這個(gè)問(wèn)題,我需要講一點(diǎn)組合數(shù)學(xué)的知識(shí)。

排列組合問(wèn)題的各種變體都可以抽象成「球盒模型」,P(n, k)就可以抽象成下面這個(gè)場(chǎng)景:

0a325f5e-cda4-11ec-bce3-dac502259ad0.jpg

即,將n個(gè)標(biāo)記了不同序號(hào)的球(標(biāo)號(hào)為了體現(xiàn)順序的差異),放入k個(gè)標(biāo)記了不同序號(hào)的盒子中(其中n >= k,每個(gè)盒子最終都裝有恰好一個(gè)球),共有P(n, k)種不同的方法。

現(xiàn)在你來(lái),往盒子里放球,你怎么放?其實(shí)有兩種視角。

首先,你可以站在盒子的視角,每個(gè)盒子必然要選擇一個(gè)球。

這樣,第一個(gè)盒子可以選擇n個(gè)球中的任意一個(gè),然后你需要讓剩下k - 1個(gè)盒子在n - 1個(gè)球中選擇:

0a57cc80-cda4-11ec-bce3-dac502259ad0.jpg

另外,你也可以站在球的視角,因?yàn)椴⒉皇敲總€(gè)球都會(huì)被裝進(jìn)盒子,所以球的視角分兩種情況:

1、第一個(gè)球可以不裝進(jìn)任何一個(gè)盒子,這樣的話(huà)你就需要將剩下n - 1個(gè)球放入k個(gè)盒子。

2、第一個(gè)球可以裝進(jìn)k個(gè)盒子中的任意一個(gè),這樣的話(huà)你就需要將剩下n - 1個(gè)球放入k - 1個(gè)盒子。

結(jié)合上述兩種情況,可以得到:

0a732944-cda4-11ec-bce3-dac502259ad0.jpg

你看,兩種視角得到兩個(gè)不同的遞歸式,但這兩個(gè)遞歸式解開(kāi)的結(jié)果都是我們熟知的階乘形式:

0a8f2996-cda4-11ec-bce3-dac502259ad0.png

至于如何解遞歸式,涉及數(shù)學(xué)的內(nèi)容比較多,這里就不做深入探討了,有興趣的讀者可以自行學(xué)習(xí)組合數(shù)學(xué)相關(guān)知識(shí)。

回到正題,這道算法題讓我們求子集劃分,子集問(wèn)題和排列組合問(wèn)題有所區(qū)別,但我們可以借鑒「球盒模型」的抽象,用兩種不同的視角來(lái)解決這道子集劃分問(wèn)題。

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

前文回溯算法框架套路說(shuō)過(guò),回溯算法的關(guān)鍵在哪里?

關(guān)鍵是要知道怎么「做選擇」,這樣才能利用遞歸函數(shù)進(jìn)行窮舉。

那么模仿排列公式的推導(dǎo)思路,將n個(gè)數(shù)字分配到k個(gè)桶里,我們也可以有兩種視角:

視角一,如果我們切換到這n個(gè)數(shù)字的視角,每個(gè)數(shù)字都要選擇進(jìn)入到k個(gè)桶中的某一個(gè)。

0aa0fe0a-cda4-11ec-bce3-dac502259ad0.jpg

視角二,如果我們切換到這k個(gè)桶的視角,對(duì)于每個(gè)桶,都要遍歷nums中的n個(gè)數(shù)字,然后選擇是否將當(dāng)前遍歷到的數(shù)字裝進(jìn)自己這個(gè)桶里。

0ac805cc-cda4-11ec-bce3-dac502259ad0.jpg

你可能問(wèn),這兩種視角有什么不同?

用不同的視角進(jìn)行窮舉,雖然結(jié)果相同,但是解法代碼的邏輯完全不同,進(jìn)而算法的效率也會(huì)不同;對(duì)比不同的窮舉視角,可以幫你更深刻地理解回溯算法,我們慢慢道來(lái)。

二、以數(shù)字的視角

用 for 循環(huán)迭代遍歷nums數(shù)組大家肯定都會(huì):

for(intindex=0;index

遞歸遍歷數(shù)組你會(huì)不會(huì)?其實(shí)也很簡(jiǎn)單:

voidtraverse(int[]nums,intindex){
if(index==nums.length){
return;
}
System.out.println(nums[index]);
traverse(nums,index+1);
}

只要調(diào)用traverse(nums, 0),和 for 循環(huán)的效果是完全一樣的。

那么回到這道題,以數(shù)字的視角,選擇k個(gè)桶,用 for 循環(huán)寫(xiě)出來(lái)是下面這樣:

//k個(gè)桶(集合),記錄每個(gè)桶裝的數(shù)字之和
int[]bucket=newint[k];

//窮舉nums中的每個(gè)數(shù)字
for(intindex=0;index//窮舉每個(gè)桶
for(inti=0;i//nums[index]選擇是否要進(jìn)入第i個(gè)桶
//...
}
}

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

//k個(gè)桶(集合),記錄每個(gè)桶裝的數(shù)字之和
int[]bucket=newint[k];

//窮舉nums中的每個(gè)數(shù)字
voidbacktrack(int[]nums,intindex){
//basecase
if(index==nums.length){
return;
}
//窮舉每個(gè)桶
for(inti=0;i//選擇裝進(jìn)第i個(gè)桶
bucket[i]+=nums[index];
//遞歸窮舉下一個(gè)數(shù)字的選擇
backtrack(nums,index+1);
//撤銷(xiāo)選擇
bucket[i]-=nums[index];
}
}

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

//主函數(shù)
booleancanPartitionKSubsets(int[]nums,intk){
//排除一些基本情況
if(k>nums.length)returnfalse;
intsum=0;
for(intv:nums)sum+=v;
if(sum%k!=0)returnfalse;

//k個(gè)桶(集合),記錄每個(gè)桶裝的數(shù)字之和
int[]bucket=newint[k];
//理論上每個(gè)桶(集合)中數(shù)字的和
inttarget=sum/k;
//窮舉,看看nums是否能劃分成k個(gè)和為target的子集
returnbacktrack(nums,0,bucket,target);
}

//遞歸窮舉nums中的每個(gè)數(shù)字
booleanbacktrack(
int[]nums,intindex,int[]bucket,inttarget){

if(index==nums.length){
//檢查所有桶的數(shù)字之和是否都是target
for(inti=0;iif(bucket[i]!=target){
returnfalse;
}
}
//nums成功平分成k個(gè)子集
returntrue;
}

//窮舉nums[index]可能裝入的桶
for(inti=0;i//剪枝,桶裝裝滿(mǎn)了
if(bucket[i]+nums[index]>target){
continue;
}
//將nums[index]裝入bucket[i]
bucket[i]+=nums[index];
//遞歸窮舉下一個(gè)數(shù)字的選擇
if(backtrack(nums,index+1,bucket,target)){
returntrue;
}
//撤銷(xiāo)選擇
bucket[i]-=nums[index];
}

//nums[index]裝入哪個(gè)桶都不行
returnfalse;
}

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

主要看backtrack函數(shù)的遞歸部分:

for(inti=0;i//剪枝
if(bucket[i]+nums[index]>target){
continue;
}

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

如果我們讓盡可能多的情況命中剪枝的那個(gè) if 分支,就可以減少遞歸調(diào)用的次數(shù),一定程度上減少時(shí)間復(fù)雜度

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

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

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

booleancanPartitionKSubsets(int[]nums,intk){
//其他代碼不變
//...
/*降序排序nums數(shù)組*/
Arrays.sort(nums);
for(i=0,j=nums.length-1;i//交換nums[i]和nums[j]
inttemp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
/*******************/
returnbacktrack(nums,0,bucket,target);
}

由于 Java 的語(yǔ)言特性,這段代碼通過(guò)先升序排序再反轉(zhuǎn),達(dá)到降序排列的目的。

三、以桶的視角

文章開(kāi)頭說(shuō)了,以桶的視角進(jìn)行窮舉,每個(gè)桶需要遍歷nums中的所有數(shù)字,決定是否把當(dāng)前數(shù)字裝進(jìn)桶中;當(dāng)裝滿(mǎn)一個(gè)桶之后,還要裝下一個(gè)桶,直到所有桶都裝滿(mǎn)為止。

這個(gè)思路可以用下面這段代碼表示出來(lái):

//裝滿(mǎn)所有桶為止
while(k>0){
//記錄當(dāng)前桶中的數(shù)字之和
intbucket=0;
for(inti=0;i//決定是否將nums[i]放入當(dāng)前桶中
bucket+=nums[i]or0;
if(bucket==target){
//裝滿(mǎn)了一個(gè)桶,裝下一個(gè)桶
k--;
break;
}
}
}

那么我們也可以把這個(gè) while 循環(huán)改寫(xiě)成遞歸函數(shù),不過(guò)比剛才略微復(fù)雜一些,首先寫(xiě)一個(gè)backtrack遞歸函數(shù)出來(lái):

booleanbacktrack(intk,intbucket,
int[]nums,intstart,boolean[]used,inttarget);

不要被這么多參數(shù)嚇到,我會(huì)一個(gè)個(gè)解釋這些參數(shù)。如果你能夠透徹理解本文,也能得心應(yīng)手地寫(xiě)出這樣的回溯函數(shù)。

這個(gè)backtrack函數(shù)的參數(shù)可以這樣解釋?zhuān)?/span>

現(xiàn)在k號(hào)桶正在思考是否應(yīng)該把nums[start]這個(gè)元素裝進(jìn)來(lái);目前k號(hào)桶里面已經(jīng)裝的數(shù)字之和為bucket;used標(biāo)志某一個(gè)元素是否已經(jīng)被裝到桶中;target是每個(gè)桶需要達(dá)成的目標(biāo)和。

根據(jù)這個(gè)函數(shù)定義,可以這樣調(diào)用backtrack函數(shù):

booleancanPartitionKSubsets(int[]nums,intk){
//排除一些基本情況
if(k>nums.length)returnfalse;
intsum=0;
for(intv:nums)sum+=v;
if(sum%k!=0)returnfalse;

boolean[]used=newboolean[nums.length];
inttarget=sum/k;
//k號(hào)桶初始什么都沒(méi)裝,從nums[0]開(kāi)始做選擇
returnbacktrack(k,0,nums,0,used,target);
}

實(shí)現(xiàn)backtrack函數(shù)的邏輯之前,再重復(fù)一遍,從桶的視角:

1、需要遍歷nums中所有數(shù)字,決定哪些數(shù)字需要裝到當(dāng)前桶中。

2、如果當(dāng)前桶裝滿(mǎn)了(桶內(nèi)數(shù)字和達(dá)到target),則讓下一個(gè)桶開(kāi)始執(zhí)行第 1 步。

下面的代碼就實(shí)現(xiàn)了這個(gè)邏輯:

booleanbacktrack(intk,intbucket,
int[]nums,intstart,boolean[]used,inttarget){
//basecase
if(k==0){
//所有桶都被裝滿(mǎn)了,而且nums一定全部用完了
//因?yàn)閠arget==sum/k
returntrue;
}
if(bucket==target){
//裝滿(mǎn)了當(dāng)前桶,遞歸窮舉下一個(gè)桶的選擇
//讓下一個(gè)桶從nums[0]開(kāi)始選數(shù)字
returnbacktrack(k-1,0,nums,0,used,target);
}

//從start開(kāi)始向后探查有效的nums[i]裝入當(dāng)前桶
for(inti=start;i//剪枝
if(used[i]){
//nums[i]已經(jīng)被裝入別的桶中
continue;
}
if(nums[i]+bucket>target){
//當(dāng)前桶裝不下nums[i]
continue;
}
//做選擇,將nums[i]裝入當(dāng)前桶中
used[i]=true;
bucket+=nums[i];
//遞歸窮舉下一個(gè)數(shù)字是否裝入當(dāng)前桶
if(backtrack(k,bucket,nums,i+1,used,target)){
returntrue;
}
//撤銷(xiāo)選擇
used[i]=false;
bucket-=nums[i];
}
//窮舉了所有數(shù)字,都無(wú)法裝滿(mǎn)當(dāng)前桶
returnfalse;
}

這段代碼是可以得出正確答案的,但是效率很低,我們可以思考一下是否還有優(yōu)化的空間

首先,在這個(gè)解法中每個(gè)桶都可以認(rèn)為是沒(méi)有差異的,但是我們的回溯算法卻會(huì)對(duì)它們區(qū)別對(duì)待,這里就會(huì)出現(xiàn)重復(fù)計(jì)算的情況。

什么意思呢?我們的回溯算法,說(shuō)到底就是窮舉所有可能的組合,然后看是否能找出和為targetk個(gè)桶(子集)。

那么,比如下面這種情況,target = 5,算法會(huì)在第一個(gè)桶里面裝1, 4

0ae095a6-cda4-11ec-bce3-dac502259ad0.jpg

現(xiàn)在第一個(gè)桶裝滿(mǎn)了,就開(kāi)始裝第二個(gè)桶,算法會(huì)裝入2, 3

0af4f7da-cda4-11ec-bce3-dac502259ad0.jpg

然后以此類(lèi)推,對(duì)后面的元素進(jìn)行窮舉,湊出若干個(gè)和為 5 的桶(子集)。

但問(wèn)題是,如果最后發(fā)現(xiàn)無(wú)法湊出和為targetk個(gè)子集,算法會(huì)怎么做?

回溯算法會(huì)回溯到第一個(gè)桶,重新開(kāi)始窮舉,現(xiàn)在它知道第一個(gè)桶里裝1, 4是不可行的,它會(huì)嘗試把2, 3裝到第一個(gè)桶里:

0b0ae658-cda4-11ec-bce3-dac502259ad0.jpg

現(xiàn)在第一個(gè)桶裝滿(mǎn)了,就開(kāi)始裝第二個(gè)桶,算法會(huì)裝入1, 4

0b217cec-cda4-11ec-bce3-dac502259ad0.jpg

好,到這里你應(yīng)該看出來(lái)問(wèn)題了,這種情況其實(shí)和之前的那種情況是一樣的。也就是說(shuō),到這里你其實(shí)已經(jīng)知道不需要再窮舉了,必然湊不出來(lái)和為targetk個(gè)子集。

但我們的算法還是會(huì)傻乎乎地繼續(xù)窮舉,因?yàn)樵谒磥?lái),第一個(gè)桶和第二個(gè)桶里面裝的元素不一樣,那這就是兩種不一樣的情況呀。

那么我們?cè)趺醋屗惴ǖ闹巧烫岣撸R(shí)別出這種情況,避免冗余計(jì)算呢?

你注意這兩種情況的used數(shù)組肯定長(zhǎng)得一樣,所以used數(shù)組可以認(rèn)為是回溯過(guò)程中的「狀態(tài)」。

所以,我們可以用一個(gè)memo備忘錄,在裝滿(mǎn)一個(gè)桶時(shí)記錄當(dāng)前used的狀態(tài),如果當(dāng)前used的狀態(tài)是曾經(jīng)出現(xiàn)過(guò)的,那就不用再繼續(xù)窮舉,從而起到剪枝避免冗余計(jì)算的作用。

有讀者肯定會(huì)問(wèn),used是一個(gè)布爾數(shù)組,怎么作為鍵進(jìn)行存儲(chǔ)呢?這其實(shí)是小問(wèn)題,比如我們可以把數(shù)組轉(zhuǎn)化成字符串,這樣就可以作為哈希表的鍵進(jìn)行存儲(chǔ)了。

看下代碼實(shí)現(xiàn),只要稍微改一下backtrack函數(shù)即可:

//備忘錄,存儲(chǔ)used數(shù)組的狀態(tài)
HashMapmemo=newHashMap<>();

booleanbacktrack(intk,intbucket,int[]nums,intstart,boolean[]used,inttarget){
//basecase
if(k==0){
returntrue;
}
//將used的狀態(tài)轉(zhuǎn)化成形如[true,false,...]的字符串
//便于存入HashMap
Stringstate=Arrays.toString(used);

if(bucket==target){
//裝滿(mǎn)了當(dāng)前桶,遞歸窮舉下一個(gè)桶的選擇
booleanres=backtrack(k-1,0,nums,0,used,target);
//將當(dāng)前狀態(tài)和結(jié)果存入備忘錄
memo.put(state,res);
returnres;
}

if(memo.containsKey(state)){
//如果當(dāng)前狀態(tài)曾今計(jì)算過(guò),就直接返回,不要再遞歸窮舉了
returnmemo.get(state);
}

//其他邏輯不變...
}

這樣提交解法,發(fā)現(xiàn)執(zhí)行效率依然比較低,這次不是因?yàn)樗惴ㄟ壿嬌系娜哂嘤?jì)算,而是代碼實(shí)現(xiàn)上的問(wèn)題。

因?yàn)槊看芜f歸都要把used數(shù)組轉(zhuǎn)化成字符串,這對(duì)于編程語(yǔ)言來(lái)說(shuō)也是一個(gè)不小的消耗,所以我們還可以進(jìn)一步優(yōu)化。

注意題目給的數(shù)據(jù)規(guī)模nums.length <= 16,也就是說(shuō)used數(shù)組最多也不會(huì)超過(guò) 16,那么我們完全可以用「位圖」的技巧,用一個(gè) int 類(lèi)型的used變量來(lái)替代used數(shù)組。

具體來(lái)說(shuō),我們可以用整數(shù)used的第i位((used >> i) & 1)的 1/0 來(lái)表示used[i]的 true/false。

這樣一來(lái),不僅節(jié)約了空間,而且整數(shù)used也可以直接作為鍵存入 HashMap,省去數(shù)組轉(zhuǎn)字符串的消耗。

看下最終的解法代碼:

publicbooleancanPartitionKSubsets(int[]nums,intk){
//排除一些基本情況
if(k>nums.length)returnfalse;
intsum=0;
for(intv:nums)sum+=v;
if(sum%k!=0)returnfalse;

intused=0;//使用位圖技巧
inttarget=sum/k;
//k號(hào)桶初始什么都沒(méi)裝,從nums[0]開(kāi)始做選擇
returnbacktrack(k,0,nums,0,used,target);
}

HashMapmemo=newHashMap<>();

booleanbacktrack(intk,intbucket,
int[]nums,intstart,intused,inttarget){
//basecase
if(k==0){
//所有桶都被裝滿(mǎn)了,而且nums一定全部用完了
returntrue;
}
if(bucket==target){
//裝滿(mǎn)了當(dāng)前桶,遞歸窮舉下一個(gè)桶的選擇
//讓下一個(gè)桶從nums[0]開(kāi)始選數(shù)字
booleanres=backtrack(k-1,0,nums,0,used,target);
//緩存結(jié)果
memo.put(used,res);
returnres;
}

if(memo.containsKey(used)){
//避免冗余計(jì)算
returnmemo.get(used);
}

for(inti=start;i//剪枝
if(((used>>i)&1)==1){//判斷第i位是否是1
//nums[i]已經(jīng)被裝入別的桶中
continue;
}
if(nums[i]+bucket>target){
continue;
}
//做選擇
used|=1<//將第i位置為1
bucket+=nums[i];
//遞歸窮舉下一個(gè)數(shù)字是否裝入當(dāng)前桶
if(backtrack(k,bucket,nums,i+1,used,target)){
returntrue;
}
//撤銷(xiāo)選擇
used^=1<//使用異或運(yùn)算將第i位恢復(fù)0
bucket-=nums[i];
}

returnfalse;
}

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

四、最后總結(jié)

本文寫(xiě)的這兩種思路都可以算出正確答案,不過(guò)第一種解法即便經(jīng)過(guò)了排序優(yōu)化,也明顯比第二種解法慢很多,這是為什么呢?

我們來(lái)分析一下這兩個(gè)算法的時(shí)間復(fù)雜度,假設(shè)nums中的元素個(gè)數(shù)為n。

先說(shuō)第一個(gè)解法,也就是從數(shù)字的角度進(jìn)行窮舉,n個(gè)數(shù)字,每個(gè)數(shù)字有k個(gè)桶可供選擇,所以組合出的結(jié)果個(gè)數(shù)為k^n,時(shí)間復(fù)雜度也就是O(k^n)。

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

當(dāng)然,這是對(duì)最壞復(fù)雜度上界的粗略估算,實(shí)際的復(fù)雜度肯定要好很多,畢竟我們添加了這么多剪枝邏輯。不過(guò),從復(fù)雜度的上界已經(jīng)可以看出第一種思路要慢很多了。

所以,誰(shuí)說(shuō)回溯算法沒(méi)有技巧性的?雖然回溯算法就是暴力窮舉,但窮舉也分聰明的窮舉方式和低效的窮舉方式,關(guān)鍵看你以誰(shuí)的「視角」進(jìn)行窮舉。

通俗來(lái)說(shuō),我們應(yīng)該盡量「少量多次」,就是說(shuō)寧可多做幾次選擇,也不要給太大的選擇空間;寧可「二選一」選k次,也不要 「k選一」選一次。

好了,這道題我們從兩種視角進(jìn)行窮舉,雖然代碼量看起來(lái)多,但核心邏輯都是類(lèi)似的,相信你通過(guò)本文能夠更深刻地理解回溯算法。

審核編輯 :李倩

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀(guān)點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 回溯算法
    +關(guān)注

    關(guān)注

    0

    文章

    10

    瀏覽量

    6681
  • 數(shù)組
    +關(guān)注

    關(guān)注

    1

    文章

    420

    瀏覽量

    26540

原文標(biāo)題:集合劃分問(wèn)題:排列組合中的回溯思想

文章出處:【微信號(hào):TheAlgorithm,微信公眾號(hào):算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

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

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

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

    關(guān)鍵字:LP-SCADA, LP-SCADA平臺(tái) , LP-SCADA系統(tǒng), 軟件回溯功能,藍(lán)鵬測(cè)控 得益于本平臺(tái)毫秒級(jí)的采集延遲,本平臺(tái)除了具有普通監(jiān)控采集平臺(tái)的所有監(jiān)控功能外,還可用于產(chǎn)線(xiàn)、設(shè)備
    發(fā)表于 05-29 14:42

    聚徽制造業(yè)專(zhuān)屬工業(yè)觸摸屏:精準(zhǔn)控制每一道工序,提升生產(chǎn)精度

    在制造業(yè)競(jìng)爭(zhēng)日益激烈的當(dāng)下,產(chǎn)品質(zhì)量與生產(chǎn)效率成為企業(yè)立足市場(chǎng)的關(guān)鍵,而生產(chǎn)精度則是保障產(chǎn)品質(zhì)量的核心要素。制造業(yè)專(zhuān)屬工業(yè)觸摸屏憑借其獨(dú)特的功能與技術(shù)優(yōu)勢(shì),深度融入生產(chǎn)的每一道工序,實(shí)現(xiàn)對(duì)生
    的頭像 發(fā)表于 05-16 15:50 ?234次閱讀

    水文監(jiān)測(cè)中的雙軌纜小車(chē)和鉛魚(yú)纜小車(chē)

    一道堅(jiān)實(shí)的科技防線(xiàn),那么這兩個(gè)設(shè)備有什么區(qū)別呢,原理又是怎么樣的呢?本文將探究竟。 ? ? ? ? 雙軌纜小車(chē):通過(guò)兩根平行的軌道來(lái)引導(dǎo)小車(chē)的運(yùn)行,利用電機(jī)或其他動(dòng)力裝置驅(qū)動(dòng)小車(chē)在軌道上移動(dòng),小車(chē)通常配備有滑輪或滾輪,與
    的頭像 發(fā)表于 04-11 15:15 ?340次閱讀
    水文監(jiān)測(cè)中的雙軌纜<b class='flag-5'>道</b>小車(chē)和鉛魚(yú)纜<b class='flag-5'>道</b>小車(chē)

    成品電池綜合測(cè)試儀:電池品質(zhì)的最后一道把關(guān)人

    綜合測(cè)試儀便成為了電池生產(chǎn)線(xiàn)上的“最后一道把關(guān)人”,為電池品質(zhì)保駕護(hù)航。 成品電池綜合測(cè)試儀的重要性 成品電池綜合測(cè)試儀,是種集多種測(cè)試功能于體的專(zhuān)業(yè)設(shè)備,能夠?qū)﹄姵剡M(jìn)行全面的性能測(cè)試和評(píng)估。從電池的容量、
    的頭像 發(fā)表于 03-18 14:30 ?310次閱讀

    集成電路和光子集成技術(shù)的發(fā)展歷程

    本文介紹了集成電路和光子集成技術(shù)的發(fā)展歷程,并詳細(xì)介紹了鈮酸鋰光子集成技術(shù)和硅和鈮酸鋰復(fù)合薄膜技術(shù)。
    的頭像 發(fā)表于 03-12 15:21 ?822次閱讀
    集成電路和光<b class='flag-5'>子集</b>成技術(shù)的發(fā)展歷程

    PID控制算法的C語(yǔ)言實(shí)現(xiàn):PID算法原理

    在工業(yè)應(yīng)用中 PID 及其衍生算法是應(yīng)用最廣泛的算法,是當(dāng)之無(wú)愧的萬(wàn)能算法,如果能夠熟練掌握 PID 算法的設(shè)計(jì)與實(shí)現(xiàn)過(guò)程,對(duì)于
    發(fā)表于 02-26 15:24

    從數(shù)據(jù)中心到量子計(jì)算,光子集成電路引領(lǐng)行業(yè)變革

    來(lái)源:Yole Group 光子集成電路正在通過(guò)實(shí)現(xiàn)更快的數(shù)據(jù)傳輸、推進(jìn)量子計(jì)算技術(shù)、以及變革醫(yī)療行業(yè)來(lái)徹底改變多個(gè)領(lǐng)域。在材料和制造工藝的創(chuàng)新驅(qū)動(dòng)下,光子集成電路有望重新定義光學(xué)技術(shù)的能力,并在
    的頭像 發(fā)表于 01-13 15:23 ?506次閱讀

    KaiHongOS的南向適配工作是如何劃分的?

    KaiHongOS的南向適配開(kāi)發(fā)工作分為<驅(qū)動(dòng)子系統(tǒng)開(kāi)發(fā)>和<內(nèi)核子系統(tǒng)開(kāi)發(fā)>。 其中,<驅(qū)動(dòng)子系統(tǒng)開(kāi)發(fā)>分’外設(shè)驅(qū)動(dòng)(UHDF)‘和’平臺(tái)驅(qū)動(dòng)(KHDF)’。 簡(jiǎn)單劃分如下圖所示:
    發(fā)表于 01-10 10:10

    弘信電子集團(tuán)力爭(zhēng)成為AI驅(qū)動(dòng)的科技大廠(chǎng)

    在AI浪潮撲面而來(lái)的火熱時(shí)代,弘信電子集團(tuán)全面擁抱AI,邁步急進(jìn)AI超級(jí)賽道。日前,在上海虹橋阿里中心,弘信電子集團(tuán)上??偛眶逜I事業(yè)部全國(guó)營(yíng)銷(xiāo)總部正式入駐啟用,集團(tuán)在AI產(chǎn)業(yè)全國(guó)戰(zhàn)略布局上實(shí)現(xiàn)再落子。
    的頭像 發(fā)表于 11-22 14:08 ?973次閱讀

    ADS1256 8通依次采樣,數(shù)據(jù)不正確怎么解決?

    SPI總線(xiàn)速度1.40625MB/S,基于STM32的HAL庫(kù)下,對(duì)八通輸入同一道方波,方波頻率20HZ、40HZ、60HZ時(shí),會(huì)出現(xiàn)只有部分通道采樣的數(shù)據(jù)能顯示波形,輸入其他頻率的方波時(shí),會(huì)存在采樣到的數(shù)據(jù)顯示的波形占空比與輸入方波的占空比不相同,這種情況是屬于寄存器
    發(fā)表于 11-22 07:09

    RVBacktrace RISC-V極簡(jiǎn)棧回溯組件

    RVBacktrace組件簡(jiǎn)介個(gè)極簡(jiǎn)的RISC-V棧回溯組件。功能在需要的地方調(diào)用組件提供的唯API,開(kāi)始當(dāng)前環(huán)境的棧回溯支持輸出addr2line需要的命令,使用addr2lin
    的頭像 發(fā)表于 09-15 08:12 ?817次閱讀
    RVBacktrace RISC-V極簡(jiǎn)棧<b class='flag-5'>回溯</b>組件

    簡(jiǎn)述晶振的等級(jí)劃分

    今天與大家起談?wù)劸鏌@科技晶振的等級(jí)劃分,方便您進(jìn)行產(chǎn)品選型。讓我們起進(jìn)入今天的炬烜知識(shí)會(huì)吧。
    的頭像 發(fā)表于 09-06 11:17 ?828次閱讀

    IP地址與子網(wǎng)劃分

    子網(wǎng)的劃分,實(shí)際上就是設(shè)計(jì)子網(wǎng)掩碼的過(guò)程,它指的是指將個(gè)給定的IP網(wǎng)絡(luò)地址空間劃分為更小的子網(wǎng)絡(luò)。 在子網(wǎng)掩碼中用1和0來(lái)分別網(wǎng)絡(luò)號(hào)和主機(jī)號(hào),其中是1的表示是網(wǎng)絡(luò)部分,0表示的是主機(jī)部分,所有
    的頭像 發(fā)表于 09-06 09:46 ?809次閱讀

    AM263x器件命名規(guī)則和子集器件

    電子發(fā)燒友網(wǎng)站提供《AM263x器件命名規(guī)則和子集器件.pdf》資料免費(fèi)下載
    發(fā)表于 09-04 10:29 ?0次下載
    AM263x器件命名規(guī)則和<b class='flag-5'>子集</b>器件

    回溯英特爾在跨越半個(gè)世紀(jì)的發(fā)展歷程

    我們以英特爾三位風(fēng)云人物的三句名言為線(xiàn)索,回溯英特爾在跨越半個(gè)世紀(jì)的發(fā)展歷程中,如何利用芯片技術(shù)的力量,影響信息時(shí)代,開(kāi)啟未來(lái)之門(mén)。
    的頭像 發(fā)表于 08-16 14:58 ?1239次閱讀