
1. 項目概述從“數數”到“算數”的思維躍遷“求一個數N的正約數集合”這聽起來像是小學數學課上的練習題。但如果你曾嘗試過用最樸素的循環從1到N去逐個判斷當N稍微大一點比如超過10^6程序就會慢得讓你懷疑人生。這恰恰是這個問題最迷人的地方——它表面上是一個簡單的枚舉問題實則是一道絕佳的思維試金石考察的是你能否從“數數”的蠻力思維躍遷到“算數”的分解與組合思維。我在算法競賽和實際開發中比如設計哈希表容量、計算資源分配的最優粒度無數次遇到它的變體其核心原理“質因數分解與約數公式”是數論中最實用、最優雅的工具之一。今天我們就徹底拆解它不僅告訴你“怎么求”更要講透“為什么可以這樣求”以及在不同場景下如何選擇最高效的“武器”。2. 核心原理算術基本定理與約數公式理解正約數的求法絕對不能繞過算術基本定理。這是整個大廈的地基。2.1 算術基本定理每個整數的“基因身份證”任何大于1的整數N都可以唯一地分解成一系列質數的乘積。這個“唯一”非常關鍵就像每個人的DNA序列是唯一的一樣。公式表達為N p?^α? × p?^α? × ... × p?^α?其中p?, p?, ..., p? 是互不相同的質數α?, α?, ..., α? 是正整數。舉個例子360 23 × 32 × 51。這個分解式就是360的“基因身份證”包含了構成它的所有基本“元素”質數及其“含量”指數。2.2 正約數個數公式一個排列組合問題既然N由這些質因數冪的乘積構成那么N的任何一個正約數d必然也是由這些相同的質因數構成只不過每個質因數的指數不能超過N中對應的指數。對于質因數p?在約數d中它的指數β?可以是多少它可以是0, 1, 2, ..., 一直到α?。也就是說有(α? 1)種選擇。由于各個質因數的選擇是獨立的根據乘法原理所有可能的組合數即N的正約數總個數τ(N)為τ(N) (α? 1) × (α? 1) × ... × (α? 1)還是以360為例質因數2的指數是3有 (31)4 種選擇指數選0,1,2,3。質因數3的指數是2有 (21)3 種選擇指數選0,1,2。質因數5的指數是1有 (11)2 種選擇指數選0,1。 因此360的正約數個數 4 × 3 × 2 24個。注意這個公式計算的是正約數的個數。如果要包括負約數和1以及它自身概念上需要另行處理但在這個正整數的正約數語境下我們通常就指這個。2.3 正約數之和公式延伸理解了個數公式和公式就順理成章了。它不再是簡單的計數而是求和。對于每個質因數p?^α?在構成約數時它可以貢獻 p?^0, p?^1, ..., p?^α? 這些不同的“值”。所有可能貢獻的和是一個等比數列求和p?^0 p?^1 ... p?^α? (p?^(α?1) - 1) / (p? - 1)。同樣根據獨立性所有約數的總和σ(N)為σ(N) [(p?^(α?1)-1)/(p?-1)] × [(p?^(α?1)-1)/(p?-1)] × ... × [(p?^(α?1)-1)/(p?-1)]對于360σ(360) [(2?-1)/(2-1)] × [(33-1)/(3-1)] × [(52-1)/(5-1)] (15/1) × (26/2) × (24/4) 15 × 13 × 6 1170。3. 算法實戰如何高效求得所有正約數知道有多少個約數只是第一步我們往往需要把它們全部列出來。這里根據不同的需求有不同的策略。3.1 方法一試除法求質因數分解這是所有方法的起點。目標是得到N p?^α? × p?^α? × ... × p?^α?這個形式。def prime_factorization(n): 返回一個列表每個元素為 (質因數, 指數) 的元組 factors [] i 2 # 只需遍歷到 sqrt(n) while i * i n: if n % i 0: cnt 0 while n % i 0: n // i cnt 1 factors.append((i, cnt)) i 1 if i 2 else 2 # 2以后只檢查奇數小幅優化 # 如果最后剩下的n大于1它本身就是一個質數 if n 1: factors.append((n, 1)) return factors # 示例分解 360 print(prime_factorization(360)) # 輸出[(2, 3), (3, 2), (5, 1)]原理與細節為什么到 sqrt(n) 即可如果n有一個大于sqrt(n)的質因數那么它必然與一個小于sqrt(n)的因數配對。在循環中通過不斷整除n的值會越來越小。循環結束后如果n1那么當前的n就是那個大于sqrt(原始n)的質因數且指數為1。i的遞增優化除了2是偶數其他質數都是奇數。所以當i2處理完后可以直接從3開始每次加2跳過所有偶數這是一個簡單有效的常數級優化。3.2 方法二DFS回溯生成所有約數得到質因數分解列表factors [(p1, a1), (p2, a2), ...]后生成所有約數就變成了一個標準的回溯DFS問題每個質因數有(指數1)種選擇選0次到選α次我們需要遍歷所有組合。def generate_divisors(factors): 根據質因數分解列表生成所有正約數 divisors [1] # 初始約數為1 for p, exp in factors: # 遍歷每個質因數 current_len len(divisors) # 對于當前已生成的所有約數分別乘以 p^1, p^2, ..., p^exp for i in range(current_len): val divisors[i] for e in range(1, exp 1): divisors.append(val * (p ** e)) # 更高效的寫法避免重復計算p的冪 # multiplier 1 # for _ in range(exp): # multiplier * p # for i in range(current_len): # divisors.append(divisors[i] * multiplier) return divisors # 示例生成360的所有約數 factors prime_factorization(360) divisors generate_divisors(factors) divisors.sort() # 排序后輸出 print(f約數個數{len(divisors)}) print(f約數列表{divisors}) # 輸出約數個數24 # 約數列表[1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 18, 20, 24, 30, 36, 40, 45, 60, 72, 90, 120, 180, 360]實操心得這個DFS是隱式的我們通過循環疊加來實現比遞歸更節省棧空間。注意內層循環的邊界current_len len(divisors)一定要在遍歷質因數前獲取。如果在循環條件里直接寫for i in range(len(divisors))會導致列表在循環中不斷變長陷入死循環。排序不是必須的但排序后更便于觀察和后續使用如查找第k小的約數。3.3 方法三成對枚舉法僅求集合不依賴分解如果我們不需要質因數分解這個中間結果只想得到約數集合并且N不是特別大比如N ≤ 10^12且約數個數不會爆炸可以使用更直接的成對枚舉法。def get_divisors_pairwise(n): 通過成對枚舉找出所有約數 divisors_small [] divisors_large [] i 1 while i * i n: # 只遍歷到 sqrt(n) if n % i 0: divisors_small.append(i) # 避免重復添加平方根 if i ! n // i: divisors_large.append(n // i) i 1 # 將大的約數列表反轉與小的約數列表拼接得到有序序列 divisors_large.reverse() return divisors_small divisors_large # 示例 print(get_divisors_pairwise(360)) # 輸出[1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 18, 20, 24, 30, 36, 40, 45, 60, 72, 90, 120, 180, 360]為什么這個方法有效因為約數總是成對出現的如果d是n的約數那么n/d也一定是n的約數。我們只需要枚舉較小的那個約數d ≤ √n就可以同時得到較大的那個約數n/d。這個方法的時間復雜度是 O(√n)在n很大但約數不多時比先分解再DFS更直觀。重要對比方法二分解DFS和方法三成對枚舉適用場景不同。分解DFS當需要頻繁使用質因數分解結果時更優例如同時需要求約數個數、約數和、歐拉函數值等。并且當N的質因數分解形式已知或可以預處理時生成約數的速度極快。成對枚舉實現簡單邏輯直觀當只需要求一次約數集合且N不大時很方便。但當N的約數個數非常多時例如高度合成數divisors_large列表反復插入可能略慢于DFS的列表擴展。4. 性能優化與邊界處理在實際編碼中尤其是面對算法競賽或大數處理時細節決定成敗。4.1 質因數分解的極致優化上面的試除法對于單個查詢已經足夠。但對于多組查詢或極大的N10^15我們可以進一步優化。預處理質數表如果需要分解很多個數可以先用埃拉托斯特尼篩法或歐拉篩預處理出一定范圍如√N的最大值內的所有質數然后用這些質數去試除避免用合數去試除。Pollard-Rho算法這是一個概率性算法用于分解非常大的整數通常超過10^18。它的平均時間復雜度約為O(N^(1/4))但對于編程競賽和一般應用掌握試除法及其優化已經足夠。4.2 大數約數個數溢出的問題約數個數公式是連乘增長非常快。一個經典的例子是 73513440 2^5 × 3^3 × 5 × 7 × 11 × 13 × 17它的約數個數是 (51)(31)(11)^5 642^5 768個。而一些更大的高度合成數其約數個數可以上萬甚至更多。注意事項在計算約數個數(α?1)*(α?1)...時中間結果可能超過32位整型范圍即使在Python中無此擔憂但在C/Java中要使用long long。生成所有約數列表時如果約數個數巨大例如超過10^5內存消耗和排序時間O(D log D)D為約數個數會成為瓶頸。此時是否真的需要生成全部列表需要根據業務需求慎重考慮。4.3 特殊數字的處理N11只有一個正約數就是它本身。質因數分解時1沒有質因數我們的算法循環會直接跳過最后factors為空列表約數列表應為[1]。需要在生成約數的函數開頭做特判。N為質數此時約數只有1和N。質因數分解結果為[(N, 1)]約數個數為2。N為完全平方數例如3662。成對枚舉法中當i*i n時i和n//i是同一個數必須避免重復添加。這就是代碼中if i ! n // i:判斷的意義。5. 應用場景與問題變形理解了原理和算法我們來看看它能解決哪些實際問題。5.1 直接應用判斷約數個數相關問題例題1求區間內約數個數最多的數反質數問題給定范圍[L, R]求其中約數個數最多的那個數若有多個輸出最小的。這類問題需要結合質因數分解和DFS搜索枚舉質因數的指數組合找到約數個數最多且數值最小的數。核心就是利用約數個數公式進行剪枝搜索。例題2求第k小的約數給定N和k求N的所有正約數排序后第k小的那個。思路是先質因數分解然后利用生成約數的過程但不需要生成全部可以通過計算每個“子樹”下的約數數量來指引搜索方向類似于在字典序中查找第k個排列。5.2 間接應用其他數論問題的基石最大公約數(GCD)與最小公倍數(LCM)雖然通常用歐幾里得算法但理解其質因數分解形式GCD取指數最小值LCM取指數最大值對理解原理至關重要。歐拉函數φ(n)計算小于n且與n互質的正整數個數。公式為 φ(n) n × Π(1 - 1/p?)其中p?是n的質因數。這同樣建立在質因數分解之上。模運算與同余方程在求解 a^x ≡ b (mod n) 這類問題時往往需要分析n的約數特別是φ(n)的約數。5.3 實際工程中的影子哈希表容量選擇一個好的哈希表容量通常是一個質數或者至少是一個約數較少的數以減少哈希沖突。理解約數有助于理解為什么選擇這樣的容量。資源分配與分塊當需要將總量為N的資源盡可能均勻地分給m個單元時如果N能被m整除即m是N的約數那么分配是最均勻的。這在大數據分片、并行計算任務劃分中很常見。計算幾何網格劃分在劃分區域時若區域總單元格數為N希望劃分成大小相等的矩形塊那么塊的個數必須是N的約數。6. 常見錯誤與調試技巧即使理解了原理實現時也難免踩坑。下面是一些常見的“坑點”。6.1 無限循環或結果錯誤在生成約數的循環中錯誤使用動態變化的列表長度如前所述for i in range(len(divisors)):在循環體內向divisors添加元素會導致無限循環。必須用current_len len(divisors)固定循環次數。質因數分解中忘記處理最后剩余的n循環結束后如果n 1它一定是最后一個質因數。漏掉這步會導致分解不完全進而使約數個數和列表都出錯。成對枚舉法的邊界條件循環條件必須是i * i n而不是i sqrt(n)因為浮點數運算可能有精度問題。更安全的寫法是i n // i。6.2 性能問題對每個數都從2開始試除在需要處理多個數時沒有預處理質數表導致大量重復的合數試除判斷。生成約數后進行了不必要的全局排序如果業務邏輯不要求有序或者可以接受DFS生成的大致有序但不嚴格的列表則可以省去排序的 O(D log D) 時間。用“成對枚舉法”處理約數極多的數當N的約數個數D很大接近√N量級時成對枚舉法本身的時間復雜度O(√N)可能遠大于O(D)。例如一個約數有10萬個的數其N可能非常大√N的循環次數可能遠超10萬。此時如果已知質因數分解用DFS生成O(D)個約數反而更快。6.3 調試建議從小數據開始用N1, 2, 質數(如17)完全平方數(如36)以及標準數(如360)來測試。交叉驗證同時實現“分解DFS”和“成對枚舉”兩種方法對同一個N運行比較結果是否一致。驗證公式用質因數分解的結果計算約數個數與生成的約數列表長度對比。打印中間結果在質因數分解和DFS生成過程中打印出關鍵的變量如每次找到的質因數、當前的約數列表有助于快速定位邏輯錯誤。最后我個人的體會是求約數這個問題是連接數論基礎與算法實踐的完美橋梁。它教會我們的不僅僅是幾個公式和算法更重要的是一種“分解與組合”的思維模式。在面對一個復雜問題時先思考能否拆解成獨立的、更簡單的子問題質因數再思考這些子問題的解如何以各種方式組合成最終的解約數這種化整為零、分而治之的思想在編程和解決工程問題的方方面面都極具價值。下次當你再遇到需要枚舉因子或分解結構的問題時不妨先想想“它的‘質因數’是什么”