將一個正整數N分解成幾個正整數相加,可以有多種分解方法,例如7=6+1,7=5+2,7=5+1+1,…。編程求出正整數N的所有整數分解式子。
輸入格式:
每個輸入包含一個測試用例,即正整數N (0 < N ≤ 30)。
輸出格式:
按遞增順序輸出N的所有整數分解式子。遞增順序是指:對於兩個分解序列 \(N_1={n_1, n_2, \cdots}\) 和 \(N_2={m_1, m_2, \cdots}\),若存在 i 使得 \(n_1=m_1\), \(\cdots\) , \(n_i=m_i\),但是 \(n_{i+1} < m_{i+1}\),則 \(N_1\) 序列必定在 \(N_2\) 序列之前輸出。每個式子由小到大相加,式子間用分號隔開,且每輸出 4 個式子后換行。
輸入樣例:
7
輸出樣例:
7=1+1+1+1+1+1+1;7=1+1+1+1+1+2;7=1+1+1+1+3;7=1+1+1+2+2
7=1+1+1+4;7=1+1+2+3;7=1+1+5;7=1+2+2+2
7=1+2+4;7=1+3+3;7=1+6;7=2+2+3
7=2+5;7=3+4;7=7
解題思路:
本文第一版 (C 語言) 中的解釋難以令人滿意,因此這里又完全重新寫了一篇。本次使用了 Python 語言來表述。若沒學過 Python,可簡單地將其看作為偽代碼。
這一題乍看起來有些復雜,但實際上僅需幾行代碼就可以完成任務。
我們首先考慮如何分解一個正整數,暫且不要求各部分保持從小到大的順序,比如整數 3
可以被分解為:
1 1 1
1 2
2 1
3
不難發現,正整數 3
最多可由 3 個正整數 1
組成。那么使用 3 個 for
循環來遍歷所有情況,即可得到上述分解式:
# 代碼 1
def partition(n):
for i in range(1, n + 1):
for j in range(1, n + 1):
for k in range(1, n + 1):
if i + j + k == n:
print(i, j, k)
if i + j == n:
print(i, j)
if i == n:
print(i)
簡單、直觀,但也粗暴。
此時我們再來考慮如何按照題目的要求使得各部分保持從小到大的順序。這也非常簡單,只需要調整第二個部分和第三個部分的搜索范圍即可。即讓第二個數字 j
從大於等於第一個部分 i
的地方開始搜索,讓第三個數字 k
從大於等於第二個部分 j
的地方開始搜索:
# 代碼 2
def partition(n):
for i in range(1, n + 1):
for j in range(i, n + 1):
for k in range(j, n + 1):
if i + j + k == n:
print(i, j, k)
if i + j == n:
print(i, j)
if i == n:
print(i)
如果題目只要求如何分解正整數 3
,那么我們的工作就結束了。
但是如何去分解任意一個正整數?按照上述解法,如果要分解正整數 7
,由於 7
最多可由 7 個正整數組成,那么則需要我們編寫 7
個 for
循環。這或許還能承受,但如果要分解正整數 1000
,恐怕...
當我們在編寫程序時,如果產生了想復制之前寫的一部分代碼的沖動,這往往是在提醒我們去做點什么來增強代碼的復用性,比如編寫一個函數重復調用該過程。
對於這種重復性的嵌套結構,我們可以考慮使用遞歸來減少這種代碼的重復性。遞歸本質上也是函數重復調用的過程,但是它在調用的過程中會保存每個函數的環境,而這種特性恰好與嵌套結構相合。比如在上段代碼中我們執行完最里層的 for
循環后,會返回到第二層 for
循環的狀態繼續執行。而不是重新開始執行第二層 for
循環。
現在我們使用遞歸結構來復現代碼 3:
# 代碼 3
n = 3
def partition(part_sum=0, search_start=1, res=[]):
for i in range(search_start, n + 1):
part_sum += i
res.append(i)
if part_sum == n:
print(res)
elif part_sum < n:
partition(part_sum, search_start=i, res=res)
part_sum -= i
res.pop()
其中 res
用於記錄當前的搜索結果,part_sum +=i; res.append(i)
和 part_sum -= i; res.pop()
則是一組對稱的操作。即先將 i
加入結果序列 res
中,然后判斷序列中所有元素之和是否等於我們要分解的正整數 n
,如果不是則還原 part_sum
的值以及彈出 i
。
遞歸是編程語言中最迷人的概念之一。它簡潔優雅而又巧妙,但同時也錯綜復雜。如果對上述代碼有疑惑,不妨試着手動模仿計算機執行 partition()
。將 n
設置為 3
這樣比較小的正整數,一步一步地執行並體會代碼 3 中的遞歸結構如何復現了代碼 2 中的嵌套 for
循環。
本文已經盡量簡化了代碼 3 的遞歸寫法,省略了一些會干擾主線的優化細節。在手動模擬執行時應該會發現代碼 3 還會執行很多不必要的操作。因此在理解了代碼 3 的遞歸寫法后,不妨試着對代碼 3 進行進一步的優化。
補充:
代碼 3 中的 partition
函數實際上是一個很“差勁”的示例,除了有很多不必要的操作之外,函數接口的設計也非常粗糙,而且還將 n
作為一個全局變量...對於一個用戶來說,最好的函數接口顯然是 partition(n)
,而不是其它,用戶輸入 n
就能得到答案,而不用關心其它細節。
此外,part_sum
這個參數其實也可以省去,因為除了求和之外,還可以遞減,即逐漸將 n
減至 0
。這里給出個人比較偏愛的 Python 寫法:
def partition(n, search_start=1, res=[]):
for i in range(search_start, n + 1):
if n == i:
print(*res, n)
return
if n < i:
return
partition(n - i, search_start=i, res=res + [i])
以下為舊文存檔 (C 語言)
解題思路:
采用了深度優先處理的思想,涉及到了一點點數據結構的知識。如果還沒學到數據結構,也不必擔心。在之前的題目中也可能用到了其它容易實現的數據結構,只是不知道它是數據結構中的內容。數據結構就是把各種各樣的操作、邏輯關系進行分類、總結,從而讓我們更加方便地設計算法來解決問題。
深度優先算法用遞歸寫起來比較方便。遞歸有兩個重要元素:
- 遞歸出口
- 遞歸的表達式
遞歸對技巧性要求很高,大多數時候其關系式並不是很容易找到。而且對遞歸的設計與理解,很容易鑽到具體細節的實現上。遞歸的優點就是可以讓一些復雜問題簡單化,把具體的細節交給計算機執行。而過分鑽研細節,就非常容易陷進去理不清頭緒。對於遞歸的學習應該是多看看經典的遞歸寫法,遇到類似問題會模仿寫就行了,不一定要自己創造出一個遞歸關系式。
本題也是如此。注意算法的主體部分,關鍵信息無非是:
void division () {
division (下一個);
對結點進行處理;
}
遞歸出口是累加的總和等於了輸入的 N。
到這里,就可以去看下面的代碼了。然后試着自己寫,不會寫,就模仿,下面的框圖對寫這個算法基本上沒有幫助——除了讓人覺得「好像挺復雜的」以外。遞歸的特點就是形式簡單,實際上細節繁多。不要扣於細節,先會寫了,再去思考和模擬它的執行細節以掌握它,這樣才不至於困難重重,無從下手。如果細節上有疑問,可以來看看下面的處理流程。
算法的處理流程是:
- 假設輸入的 N 為 3:
第一層遞歸 | 第二層遞歸 | 第三層遞歸 | 主要執行細節 |
---|---|---|---|
division (1) sum = 1,不跳出 → |
division (1) sum = 2,不跳出 → |
division (1) sum = 3 等於 N,輸出當前序列 1 1 1, 跳出,執行 for 循環,sum 均大於 3,跳出,返回上一層 ↓ |
第三層 s[0] s[1] s[2] 動作 1 1 1 輸出 1 1 2 跳出 1 1 3 跳出 1 1 4 跳出 |
`↓` | 開始處理<br>division (2)<br>sum = 3,**輸出**當前序列 1 2,然后跳出,執行 for 循環,均跳出<br>`←` 返回至上一層 | `←` 返回至上一層 | 第二層<br>s[0] s[1] 動作<br> 1 2 **輸出**<br>1 3 跳出<br> 1 4 跳出
開始處理 division (2)
sum = 2,不跳出 →
| division (2)
sum = 4,跳出,返回上一層 ↓
| | 第二層
s[0] s[1] 動作
2 2 跳出
開始處理 division (3)
sum = 3, 輸出當前序列 3,結束程序 | ←
返回至上一層 | | 第一層
s[0] 動作
3 跳出
- 箭頭指明了各層之間的流動方向。
如果 N 更大一點,這個表格會變得更加復雜。遞歸的手動模擬范圍應盡量小一點,否則容易混亂。
你可以發現,所謂的深度優先就是說,優先處理下一個節點,直到它們的 sum 等於 N,才返回上一個節點。先爬到最深處,再往回走。
解題代碼:
#include<stdio.h>
int N;
int s[31]; // 存放划分結果
int top = -1; // 數組指針
int count = 0; // 統計輸出的次數
int sum = 0; // 拆分項累加和
void division (int i);
int main ()
{
scanf ("%d", &N);
division (1);
return 0;
}
void division (int i) {
if (sum == N) {
count ++;
printf("%d=", N);
int k;
for (k=0; k<top; k++) {
printf("%d+", s[k]);
}
if (count%4 == 0 || s[top] == N) {
printf("%d\n", s[top]);
} else {
printf("%d;", s[top]);
}
return;
} // 輸出部分
if (sum > N) {
return;
}
for (int j=i; j<=N; j++) {
s[++top] = j;
sum += j;
division (j);
sum -= j;
top --;
} // 算法主體
}