精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

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

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

3天內不再提示

一道非常經典的回溯算法問題:子集劃分問題

算法與數據結構 ? 來源:labuladong ? 作者:labuladong ? 2022-05-07 09:32 ? 次閱讀

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

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

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

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

題目非常簡單:

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

函數簽名如下:

booleancanPartitionKSubsets(int[]nums,intk);

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

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

一、思路分析

首先,我們回顧一下以前學過的排列組合知識:

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

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

3、排列、組合總數的計算公式:

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

好,現在我問一個問題,這個排列公式P(n, k)是如何推導出來的?為了搞清楚這個問題,我需要講一點組合數學的知識。

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

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

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

現在你來,往盒子里放球,你怎么放?其實有兩種視角。

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

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

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

另外,你也可以站在球的視角,因為并不是每個球都會被裝進盒子,所以球的視角分兩種情況:

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

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

結合上述兩種情況,可以得到:

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

你看,兩種視角得到兩個不同的遞歸式,但這兩個遞歸式解開的結果都是我們熟知的階乘形式:

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

至于如何解遞歸式,涉及數學的內容比較多,這里就不做深入探討了,有興趣的讀者可以自行學習組合數學相關知識。

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

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

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

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

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

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

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

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

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

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

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

二、以數字的視角

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

for(intindex=0;index

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

booleancanPartitionKSubsets(int[]nums,intk){
//其他代碼不變
//...
/*降序排序nums數組*/
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 的語言特性,這段代碼通過先升序排序再反轉,達到降序排列的目的。

三、以桶的視角

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

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

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

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

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

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

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

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

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

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號桶初始什么都沒裝,從nums[0]開始做選擇
returnbacktrack(k,0,nums,0,used,target);
}

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

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

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

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

booleanbacktrack(intk,intbucket,
int[]nums,intstart,boolean[]used,inttarget){
//basecase
if(k==0){
//所有桶都被裝滿了,而且nums一定全部用完了
//因為target==sum/k
returntrue;
}
if(bucket==target){
//裝滿了當前桶,遞歸窮舉下一個桶的選擇
//讓下一個桶從nums[0]開始選數字
returnbacktrack(k-1,0,nums,0,used,target);
}

//從start開始向后探查有效的nums[i]裝入當前桶
for(inti=start;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)){
returntrue;
}
//撤銷選擇
used[i]=false;
bucket-=nums[i];
}
//窮舉了所有數字,都無法裝滿當前桶
returnfalse;
}

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

首先,在這個解法中每個桶都可以認為是沒有差異的,但是我們的回溯算法卻會對它們區別對待,這里就會出現重復計算的情況。

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

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

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

現在第一個桶裝滿了,就開始裝第二個桶,算法會裝入2, 3

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

然后以此類推,對后面的元素進行窮舉,湊出若干個和為 5 的桶(子集)。

但問題是,如果最后發現無法湊出和為targetk個子集,算法會怎么做?

回溯算法會回溯到第一個桶,重新開始窮舉,現在它知道第一個桶里裝1, 4是不可行的,它會嘗試把2, 3裝到第一個桶里:

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

現在第一個桶裝滿了,就開始裝第二個桶,算法會裝入1, 4

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

好,到這里你應該看出來問題了,這種情況其實和之前的那種情況是一樣的。也就是說,到這里你其實已經知道不需要再窮舉了,必然湊不出來和為targetk個子集。

但我們的算法還是會傻乎乎地繼續窮舉,因為在她看來,第一個桶和第二個桶里面裝的元素不一樣,那這就是兩種不一樣的情況呀。

那么我們怎么讓算法的智商提高,識別出這種情況,避免冗余計算呢?

你注意這兩種情況的used數組肯定長得一樣,所以used數組可以認為是回溯過程中的「狀態」。

所以,我們可以用一個memo備忘錄,在裝滿一個桶時記錄當前used的狀態,如果當前used的狀態是曾經出現過的,那就不用再繼續窮舉,從而起到剪枝避免冗余計算的作用

有讀者肯定會問,used是一個布爾數組,怎么作為鍵進行存儲呢?這其實是小問題,比如我們可以把數組轉化成字符串,這樣就可以作為哈希表的鍵進行存儲了。

看下代碼實現,只要稍微改一下backtrack函數即可:

//備忘錄,存儲used數組的狀態
HashMapmemo=newHashMap<>();

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

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

if(memo.containsKey(state)){
//如果當前狀態曾今計算過,就直接返回,不要再遞歸窮舉了
returnmemo.get(state);
}

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

這樣提交解法,發現執行效率依然比較低,這次不是因為算法邏輯上的冗余計算,而是代碼實現上的問題。

因為每次遞歸都要把used數組轉化成字符串,這對于編程語言來說也是一個不小的消耗,所以我們還可以進一步優化

注意題目給的數據規模nums.length <= 16,也就是說used數組最多也不會超過 16,那么我們完全可以用「位圖」的技巧,用一個 int 類型的used變量來替代used數組。

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

這樣一來,不僅節約了空間,而且整數used也可以直接作為鍵存入 HashMap,省去數組轉字符串的消耗。

看下最終的解法代碼:

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號桶初始什么都沒裝,從nums[0]開始做選擇
returnbacktrack(k,0,nums,0,used,target);
}

HashMapmemo=newHashMap<>();

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

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

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

returnfalse;
}

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

四、最后總結

本文寫的這兩種思路都可以算出正確答案,不過第一種解法即便經過了排序優化,也明顯比第二種解法慢很多,這是為什么呢?

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

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

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

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

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

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

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

審核編輯 :李倩

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

    關注

    0

    文章

    10

    瀏覽量

    6591
  • 數組
    +關注

    關注

    1

    文章

    416

    瀏覽量

    25910

原文標題:集合劃分問題:排列組合中的回溯思想

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

收藏 人收藏

    評論

    相關推薦

    ADS1256 8通依次采樣,數據不正確怎么解決?

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

    RVBacktrace RISC-V極簡棧回溯組件

    RVBacktrace組件簡介個極簡的RISC-V棧回溯組件。功能在需要的地方調用組件提供的唯API,開始當前環境的棧回溯支持輸出addr2line需要的命令,使用addr2lin
    的頭像 發表于 09-15 08:12 ?301次閱讀
    RVBacktrace RISC-V極簡棧<b class='flag-5'>回溯</b>組件

    機器學習的經典算法與應用

    關于數據機器學習就是喂入算法和數據,讓算法從數據中尋找種相應的關系。Iris鳶尾花數據集是經典數據集,在統計學習和機器學習領域都經常被
    的頭像 發表于 06-27 08:27 ?1578次閱讀
    機器學習的<b class='flag-5'>經典</b><b class='flag-5'>算法</b>與應用

    求助,關于STM32上開發函數調用堆棧回溯的問題求解

    1、stm32f1系列 2、上了FreeRTOS 3、想開發函數調用回溯功能 在編譯選項中增加了--use_frame_pointer,編程個正常的程序(之前直run的),測試發現,程序啟動即crash,請問有沒有高手之前遇
    發表于 05-10 07:32

    vlan的劃分方法有哪些?有哪幾種?

    VLAN(Virtual Local Area Network)是種虛擬局域網技術,可以將多個物理上分散的局域網劃分為邏輯上的若干虛擬局域網。VLAN的劃分方法主要有以下幾種: 1. 端口VLAN
    的頭像 發表于 04-20 14:20 ?3194次閱讀

    子集成芯片和光子集成技術的區別

    子集成芯片和光子集成技術雖然緊密相關,但它們在定義和應用上存在些區別。
    的頭像 發表于 03-25 14:45 ?738次閱讀

    子集成芯片和光子集成技術是什么

    子集成芯片和光子集成技術是光子學領域的重要概念,它們代表了光子在集成電路領域的應用和發展。
    的頭像 發表于 03-25 14:17 ?962次閱讀

    子集成芯片是什么

    子集成芯片,也稱為光子芯片或光子集成電路,是種將光子器件小型化并集成在特殊襯底材料上的技術。這些特殊的光子器件,如光柵、耦合器、光開關、激光器、光電探測器、陣列波導等,被組合在
    的頭像 發表于 03-22 16:51 ?1097次閱讀

    子集成芯片的應用范圍

    子集成芯片的應用范圍非常廣泛,得益于其在高速數據傳輸、低功耗通信以及高度集成等方面的顯著優勢。
    的頭像 發表于 03-20 17:05 ?932次閱讀

    微波光子集成芯片和硅基光子集成芯片的區別

    微波光子集成芯片和硅基光子集成芯片都是光電子領域的重要技術,但它們在設計原理、應用領域以及制造工藝上存在著顯著的區別。
    的頭像 發表于 03-20 16:14 ?927次閱讀

    微波光子集成芯片的基本原理

    微波光子集成芯片的應用非常廣泛。首先,它可以用于無線通信系統中,可以將微波信號轉換為光信號進行傳輸,從而實現高速、遠距離的數據傳輸。
    發表于 03-01 10:09 ?849次閱讀

    STM32控制中常見的PID算法總結

    在很多控制算法當中,PID控制算法又是最簡單,最能體現反饋思想的控制算法,可謂經典中的經典經典
    發表于 12-27 14:07 ?1564次閱讀
    STM32控制中常見的PID<b class='flag-5'>算法</b>總結

    DshanMCU-R128s2啟動與資源劃分

    下面簡單介紹下 R128 方案的資源劃分與啟動流程。 資源劃分 CPU 資源劃分 這只是默認配置方案,CPU 資源劃分可以按照需求任意修改
    的頭像 發表于 12-22 17:46 ?628次閱讀
    DshanMCU-R128s2啟動與資源<b class='flag-5'>劃分</b>

    jvm運行時內存區域劃分

    的內存區域劃分對于了解Java程序的內存使用非常重要,本文將詳細介紹JVM運行時的內存區域劃分。 JVM運行時內存區域主要劃分為以下幾個部分: 程序計數器(Program Counte
    的頭像 發表于 12-05 14:08 ?508次閱讀

    半導體芯片切割,一道精細工藝的科技之門

    在半導體制造的過程中,芯片切割是一道重要的環節,它不僅決定了芯片的尺寸和形狀,還直接影響到芯片的性能和使用效果。隨著科技的不斷進步,芯片切割技術也在不斷發展,成為半導體制造領域中一道精細
    的頭像 發表于 11-30 18:04 ?1278次閱讀
    半導體芯片切割,<b class='flag-5'>一道</b>精細工藝的科技之門