拆分集合為相等的子集合(第1屆第1題)


題目要求

       問題描述:將1到N的連續整數組成的集合划分為兩個子集合,且保證每個集合的數字和相等。例如,對於N=4,對應的集合{1,2,3,4},能被划分為{1,4}、{2,3}兩個集合,使得1+4=2+3,且划分方案只有此一種。編程實現給定任一正整數N(1<=N<=39),輸出其符合題意的划分方案數。

       樣例輸入1:3

       樣例輸出1:1    (可划分為{1,2}、{3})

       樣例輸入2:4

       樣例輸出2:1    (可划分為{1,3}、{2,4})

       樣例輸入3:7

       樣例輸出3:4    (可划分為{1,6,7}、{2,3,4,5},或{1,2,4,7}、{3,5,6},或{1,3,4,6}、{2,5,7},或{1,2,5,6}、{3,4,7})

解決方案

       此題的解決方案有多種,但基本思想是動態規划

       首先,觀察子集合的和。

       對於任一正整數N,集合{1,2,3...N}的和為:

       那么將集合S划分為兩個和相等的子集合后,其子集合C中的整數和必為:

       例如對於正整數4,集合{1,2,3,4}的和為S=4*(4+1)/2=10,那么將其划分為和相等的兩個子集合后,其子集合C中的整數和為sum=S/2=5。

       於是,此題就轉化為在集合{1,2,3...N}中,任意選取k個數,使其和為N*(N+1)/4的問題,換句話說,就是限制子集合和為N(N+1)/4時,集合{1,2,3...,N}中可提供的整數選取方案數

       此描述隱含兩個條件:第一,N*(N+1)除以4必須為整數,否則無法選取;第二,在選出k個數的所有方案中,每個方案都有其互補的方案。還是拿整數4舉例,對於集合{1,2,3,4},任選k個數,使其和為4*(4+1)/4=5時,有兩種方案{1,4}和{2,3},這兩種方案互補構成所有整數集合,並生成一種集合划分方案。由此可得出,將選取k個數的所有方案數除以2,就是集合N划分為相等的子集合的方案數。

       接下來,從集合S中往出選k個數,使其和為N*(N+1)/4,看有多少種選取方案。

       給定集合S={1,2,3...,N},我們將其一字排開,挨個判斷每個數是否應該加入到滿足整數和為N*(N+1)/4的子集合C。對於每個數,要么可以被加入到集合C,要么不可以被加入,只有這兩種可能。那么如何知道當前的整數是否應該加入子集合呢?由於這個子集合的和與單個整數大小有懸殊,我們似乎一眼看不出來。既然這樣,我們不妨縮小問題規模來漸進考慮,而縮小問題規模的常見切入點是減小“自變量”的規模

       重讀“接下來...”那句話,發現其中有兩個條件,一個目的。目的是“有多少種選取方案”,這就是因變量。條件是“選出k個數”和“使其和為N(N+1)/4”,這就是兩個自變量,一個限制選取的整數,另一個限制選出的整數的和。既然有了自變量和因變量,不如定義個函數出來更好的描述問題:


       在函數F(i, sum)中,i代表當前需要判斷集合S中第i個數是否應該加入子集合sum代表此時限制的子集合整數和大小F(i, sum)代表限制子集合整數和為sum時,集合{0,1,2...,i-1,i}中可提供的選取方案。(如果有點蒙,繼續往下看,后面會附圖...事實證明多看幾遍就理解了...)

       如果我們要減小自變量規模,就要從上面兩個自變量下手。先看選取的整數,集合S中的最小整數是1,我們加入一個更小的整數0(顯然,0不會影響集合的划分)來輔助思考。對於選出的整數的和,也就是限制的子集合的整數和,我們也從最小的0開始判斷,那么問題的最小規模就是:限制子集合的整數和為0時,考慮整數0是否可以加入子集合?這個問題的答案是肯定的,當子集合的和為0時,完全可以將0加入,且僅有次一種方案,那么用上述函數來表達就是:

       明白了此點,也就順利地得出:F(0, 1)=F(0, 2)=F(0, 3)...=F(0, N)=0,因為限制子集合整數和大於0時,光有0無論如何也不能選出符合此限制的整數集合,即可行方案數為0。

       邁出第一步,后面的就好辦了...這是安慰人,事實是前方高能,更費心神!

       為避免詞語重復,下面說S中第i個元素時,就是指第i個整數

       假設此時,S中前i-1個元素都判斷完了,緊接着應該判斷第i個元素,與此同時,子集合的整數和被限定為sum,那么這第i個元素要不要被加入子集合呢?對此,我們做如下推斷:

           1:如果這第i個元素本身大於子集合的整數和sum,即i>sum,那么這第i個元素肯定不能加入子集合,否則就超出子集合整數和限制了。此時:F(i, sum)=F(i-1, sum),意思就是在相等的子集合整數和限制下,既然第i個元素沒被加入,那么判斷完第i個元素后的整數選取方案與判斷完第i-1個數時的方案應該是相同的。

           2:如果這第i個元素小於子集合整數和,那么就有兩種考慮:

                2.1:堅持不把第i個元素放入子集合,那么此時整數的選取方案仍然有F(i-1, sum)種。

                2.2:如果把第i個元素放入了子集合,那么此時整數的選取方案有F(i-1, sum-i)種,sum-i的含義在於既然要放入第i個元素,就要給它留下足夠的空間。F(i-1, sum-i)是在肯定要放入元素i的情形下,放入元素i前,整數的選取方案。

           也即是說,i<=sum時,F(i, sum)=F(i-1, sum)+F(i-1, sum-i)

           綜上可得

       如果覺得這個式子還是比較蒙圈,那還是從具體的解決方案入手深化理解,畢竟理論都是抽象的,不好琢磨。

       下面的解決方案中,我們均設定N=4,那么集合S={1,2,3,4}對應的最終子集合的整數和就是4*(4+1)/4=5,即求F(4, 5)的值。

解決方案一

        先對可能出現的圖例做說明:

       圖零:

       當對第0個元素判斷時:

            >若子集合整數和限定為0,那么只有把0放入子集合這一種可能。若子集合整數和大於0,那么放入0顯然不能滿足題意,故其選取方案均為0。

       圖一:

       當對第1個元素判斷時:

            >限定子集合整數和為1:若要將元素1放入子集合,則1之前子集合中的元素和必須為1-1=0;若不放入元素1,則1之前子集合中的元素和必須為1,故在此子集合整數和限制下,加入1和不加入1就組成了兩種方案,且這兩種方案數的和為:F[1,1]=F[0,0]+F[0,1]

            >限定子集合整數和為2:若要將元素1放入子集合,則1之前子集合中的元素和必須為2-1=1;若不放入元素1,則1之前子集合中的元素和必須為2,即:F[1,2]=F[0,1]+F[0,2]

            >依次類推F[1,3]=F[0,2]+F[0,3]F[1,4]=F[0,3]+F[0,4]F[1,5]=F[0,4]+F[0,5]

            >注意最后:當選取的元素i(縱向)大於子集合整數和(橫向)時,F[i, sum]=F[i-1, sum];也就是說,此時的元素i肯定放不進子集合,那么它滿足題意的選取方案與上一個元素的方案一致。 

       圖二:

 

       圖三:

 

       圖四:

 

        最后,右下角的值F[i, sum]反應了所有滿足題意的子集合數,將其除以2才是集合S的划分方案數。

源碼示例一

 

解決方案二

        由上面的解釋,不知道大家是否察覺到計算過程其實就是個遞歸過程,那么我們嘗試將其轉換為遞歸形式。

        結合綜述中的式子,遞歸應該是最好被理解的,但是遞歸的缺點就是計算太慢...

源碼示例二

 

解決方案三

       針對前面的敘述,換一個角度思考。

       給定集合{0,1,2,3,4},如果我們是按順序挑選的,那么要使選出的元素和為5,那么可以是選出元素和為5的組合,再把元素0加進來(如果之前的組合中沒有0),還可以先選出和為4的元素組合,再把元素1加進來(如果之前的組合中沒有1),或者,可以先選出和為3的元素組合,再把元素2加進來(如果之前的組合中沒有2),再或者,可以先選出和為2的元素組合,再把元素3加進來(如果之前的組合中沒有3)...最后,還可以是先選出和為0的組合,然后再把元素5加進來(如果之前的組合中沒有5)。

       如果用S(sum)表示元素和為sum的一個組合,那么上面的敘述可表示為:

S(5)=S(5)+0;    S(5)=S(4)+1;    S(5)=S(3)+2;    S(5)=S(2)+3;    S(5)=S(1)+4;    S(5)=S(0)+5

       現在,再換一個維度考慮。

       仍然是集合{0,1,2,3,4},如果我們按順序挑選到了i,那么i可能成為S(0)到S(5)任一組合中的元素之一。

       如果i成了S(5)中的一份子,那么S(5)的組成方案數必定是沒加入i前S(5)已有的組成方案數加上加入i后S(4)的組成方案數。(定一定神,結合解決方案一考慮,每遍歷到一個元素i,都要加上之前遍歷過程中求出的解決方案數)。

       下面上圖...

       圖零:

       當挑選到第0個元素時,顯然,構成S[0]只有一種方案,就是把0放入,其他S[1]S[5]均為0。

       圖一:

       當挑選到第1個元素時,滿足S[5]的方案數等於當前已有的方案數(沒加入元素1之前)加上滿足S[4]的方案數(加上元素1),依次類推。

       這里可能有兩個疑問,第一是當前已有的方案數(沒加入元素1)從何而來?事實上,這個方案數自初始化以來,就一直"傳遞"下去,並在遍歷到每個元素時,進行更新。另一個疑問是這里為什么倒着計算,即每遍歷到一個元素,先從S[5]計算,其實是S[4...1]。這個原因在於每次更新數據前,當前位置保持的是遍歷完上一個元素后的方案數,而計算當前遍歷元素下的方案數時,總是需要用到遍歷完上一個元素后的數據,所以,如果正着往后算,會造成數據錯亂。(不知道我說清楚了沒...)

       舉例,假如剛剛遍歷完元素0,現在輪到遍歷元素1了,此時上圖數組的初始狀態分別存儲了遍歷完元素0后滿足子集合整數和為0、1、2、3、4、5的元素選取方案數。這個初始狀態來自於上個元素,而且要被復用,所以必須等使用完了才能再根據當前元素1的情形進行更新。由於其復用的規律是后面用到前面的數據,所以從后往前推算就不會造成混亂了。

       圖二:

       圖三:

       圖四:

源碼示例三

 

結果展示

 

小結

       好了,再說下去我也快蒙圈了...

       動態規划題型很多,需大量練習才能領會。其核心思想就是計算后面的結果時,利用之前的結果。當不能一眼看出題目中的遞推關系時,不妨先找到題目的自變量去減小題目規模來逐步考慮,在考慮時,注意特殊情況的處理。

       另外,將文字敘述變為公式推導也是重要的技能,唯有多練才可以掌握。

       剛接觸動態規划的同學可以從0-1背包問題看起,這里有篇文章或許能給你帶來啟發:0-1背包問題和部分背包問題分析

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM