我需要一种算法来生成所有可能的正数分区,我想出了一个(作为答案发布),但它是指数时间。
该算法应该返回一个数字可以表示为小于或等于其自身的正数之和的所有可能方式。因此,例如对于数字5,结果将是:
- 5
- 4+1
- 3+2
- 3+1+1
- 2+2+1
- 2+1+1+1
- 1+1+1+1+1
所以我的问题是:有没有更有效的算法呢?
编辑:问题的标题是“数字的总和分解”,因为我真的不知道这叫什么。ShreevatsaR 指出它们被称为“分区”,因此我相应地编辑了问题标题。
我需要一种算法来生成所有可能的正数分区,我想出了一个(作为答案发布),但它是指数时间。
该算法应该返回一个数字可以表示为小于或等于其自身的正数之和的所有可能方式。因此,例如对于数字5,结果将是:
所以我的问题是:有没有更有效的算法呢?
编辑:问题的标题是“数字的总和分解”,因为我真的不知道这叫什么。ShreevatsaR 指出它们被称为“分区”,因此我相应地编辑了问题标题。
分区数量 p(n) 呈指数增长,因此您为生成所有分区所做的任何事情都必须花费指数时间。
也就是说,您可以比您的代码做得更好。请参阅David Eppstein的Python Algorithms and Data Structures或其更新版本。
这是我在 Python 中的解决方案(指数时间):
q = { 1: [[1]] }
def decompose(n):
try:
return q[n]
except:
pass
result = [[n]]
for i in range(1, n):
a = n-i
R = decompose(i)
for r in R:
if r[0] <= a:
result.append([a] + r)
q[n] = result
return result
>>> decompose(5)
[[5], [4, 1], [3, 2], [3, 1, 1], [2, 2, 1], [2, 1, 1, 1], [1, 1, 1, 1, 1]]
当您要求更有效的算法时,我不知道该比较哪个。但这是一种以直接方式(Erlang)编写的算法:
-module(partitions).
-export([partitions/1]).
partitions(N) -> partitions(N, N).
partitions(N, Max) when N > 0 ->
[[X | P]
|| X <- lists:seq(min(N, Max), 1, -1),
P <- partitions(N - X, X)];
partitions(0, _) -> [[]];
partitions(_, _) -> [].
它在时间上是指数的(与Can Berk Güder 在 Python 中的解决方案相同)并且在堆栈空间中是线性的。但是使用相同的技巧,memoization,你可以通过节省一些内存和更少的指数来实现很大的改进。(N=50 快十倍)
mp(N) ->
lists:foreach(fun (X) -> put(X, undefined) end,
lists:seq(1, N)), % clean up process dictionary for sure
mp(N, N).
mp(N, Max) when N > 0 ->
case get(N) of
undefined -> R = mp(N, 1, Max, []), put(N, R), R;
[[Max | _] | _] = L -> L;
[[X | _] | _] = L ->
R = mp(N, X + 1, Max, L), put(N, R), R
end;
mp(0, _) -> [[]];
mp(_, _) -> [].
mp(_, X, Max, R) when X > Max -> R;
mp(N, X, Max, R) ->
mp(N, X + 1, Max, prepend(X, mp(N - X, X), R)).
prepend(_, [], R) -> R;
prepend(X, [H | T], R) -> prepend(X, T, [[X | H] | R]).
无论如何,您应该针对您的语言和目的进行基准测试。
这是一种更冗长的方法(这是我在知道“分区”一词之前所做的,这使我能够进行谷歌搜索):
def magic_chunker (remainder, chunkSet, prevChunkSet, chunkSets):
if remainder > 0:
if prevChunkSet and (len(prevChunkSet) > len(chunkSet)): # counting down from previous
# make a chunk that is one less than relevant one in the prevChunkSet
position = len(chunkSet)
chunk = prevChunkSet[position] - 1
prevChunkSet = [] # clear prevChunkSet, no longer need to reference it
else: # begins a new countdown;
if chunkSet and (remainder > chunkSet[-1]): # no need to do iterations any greater than last chunk in this set
chunk = chunkSet[-1]
else: # i.e. remainder is less than or equal to last chunk in this set
chunk = remainder #else use the whole remainder for this chunk
chunkSet.append(chunk)
remainder -= chunk
magic_chunker(remainder, chunkSet, prevChunkSet, chunkSets)
else: #i.e. remainder==0
chunkSets.append(list(chunkSet)) #save completed partition
prevChunkSet = list(chunkSet)
if chunkSet[-1] > 1: # if the finalchunk was > 1, do further recursion
remainder = chunkSet.pop() #remove last member, and use it as remainder
magic_chunker(remainder, chunkSet, prevChunkSet, chunkSets)
else: # last chunk is 1
if chunkSet[0]==1: #the partition started with 1, we know we're finished
return chunkSets
else: #i.e. still more chunking to go
# clear back to last chunk greater than 1
while chunkSet[-1]==1:
remainder += chunkSet.pop()
remainder += chunkSet.pop()
magic_chunker(remainder, chunkSet, prevChunkSet, chunkSets)
partitions = []
magic_chunker(10, [], [], partitions)
print partitions
>> [[10], [9, 1], [8, 2], [8, 1, 1], [7, 3], [7, 2, 1], [7, 1, 1, 1], [6, 4], [6, 3, 1], [6, 2, 2], [6, 2, 1, 1], [6, 1, 1, 1, 1], [5, 5], [5, 4, 1], [5, 3, 2], [5, 3, 1, 1], [5, 2, 2, 1], [5, 2, 1, 1, 1], [5, 1, 1, 1, 1, 1], [4, 4, 2], [4, 4, 1, 1], [4, 3, 3], [4, 3, 2, 1], [4, 3, 1, 1, 1], [4, 2, 2, 2], [4, 2, 2, 1, 1], [4, 2, 1, 1, 1, 1], [4, 1, 1, 1, 1, 1, 1], [3, 3, 3, 1], [3, 3, 2, 2], [3, 3, 2, 1, 1], [3, 3, 1, 1, 1, 1], [3, 2, 2, 2, 1], [3, 2, 2, 1, 1, 1], [3, 2, 1, 1, 1, 1, 1], [3, 1, 1, 1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [2, 2, 2, 2, 1, 1], [2, 2, 2, 1, 1, 1, 1], [2, 2, 1, 1, 1, 1, 1, 1], [2, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
这是我在 Haskell 中编写的使用超态的解决方案。
import Numeric.Natural (Natural)
import Control.Monad (join)
import Data.List (nub)
import Data.Functor.Foldable (ListF (..), para)
partitions :: Natural -> [[Natural]]
partitions = para algebra
where algebra Nothing = []
algebra (Just (0,_)) = [[1]]
algebra (Just (_, past)) = (nub . (getAll =<<)) (fmap (1:) past)
getAll :: [Natural] -> [[Natural]]
getAll = fmap (dropWhile (==0) . sort) . subsets
where subsets xs = flip sumIndicesAt xs <$> indices xs
indices :: [Natural] -> [[Natural]]
indices = join . para algebra
where algebra Nil = []
algebra (Cons x (xs, [])) = [[x:xs]]
algebra (Cons x (xs, past)) = (:) <$> [x:xs,[]] <*> past
它绝对不是最有效的,但我认为它非常优雅,而且很有启发性。
static void printArray(int p[], int n){
for (int i = 0; i < n; i++)
System.out.print(p[i]+" ");
System.out.println();
}
// Function to generate all unique partitions of an integer
static void printAllUniqueParts(int n) {
int[] p = new int[n]; // An array to store a partition
int k = 0; // Index of last element in a partition
p[k] = n; // Initialize first partition as number itself
// This loop first prints current partition, then generates next
// partition. The loop stops when the current partition has all 1s
while (true) {
// print current partition
printArray(p, k + 1);
// Generate next partition
// Find the rightmost non-one value in p[]. Also, update the
// rem_val so that we know how much value can be accommodated
int rem_val = 0;
while (k >= 0 && p[k] == 1) {
rem_val += p[k];
k--;
}
// if k < 0, all the values are 1 so there are no more partitions
if (k < 0){
break;
}
// Decrease the p[k] found above and adjust the rem_val
p[k]--;
rem_val++;
while (rem_val > p[k]) {
p[k + 1] = p[k];
rem_val = rem_val - p[k];
k++;
}
p[k + 1] = rem_val;
k++;
}
}
public static void main(String[] args) {
System.out.println("All Unique Partitions of 5");
printAllUniqueParts(5);
System.out.println("All Unique Partitions of 7");
printAllUniqueParts(7);
System.out.println("All Unique Partitions of 9");
printAllUniqueParts(8);
}
另一个 Java 解决方案。它首先创建只有给定数字的第一个分区。然后它进入 while 循环,在最后创建的分区中找到大于 1 的最后一个数字。从该数字它将 1 移动到数组中的下一个数字。如果下一个数字最终与找到的数字相同,它将移至下一个。当最后创建的分区的第一个数字为 1 时,循环停止。这是因为所有分区中的数字始终按降序排序。
编号为 5 的示例。首先它创建第一个分区,即编号为 5。然后它在最后一个分区中找到大于 1 的最后一个数字。由于我们的最后一个分区是数组 [5,0,0,0,0],因此它找到了编号5 在索引 0 处。然后从 5 中取一个并将其移动到下一个位置。这就是我们获得分区 [4, 1, 0, 0, 0] 的方式。它再次进入循环。现在它从 4 中取一个并将其向上移动,因此我们得到 [3, 2, 0, 0, 0]。然后同样的事情,我们得到 [3, 1, 1, 0, 0]。在下一次迭代中,我们得到 [2, 2, 1, 0, 0]。现在它从第二秒 2 中取出一个并尝试将其移动到索引 2 中我们有 1。它会跳到下一个索引,因为我们也会得到 2 并且我们会有分区 [2, 1, 2, 0, 0]只是最后一个的重复。相反,我们得到 [2, 1, 1, 1, 0]。在最后一步,我们得到 [1, 1, 1, 1,
private static List<int[]> getNumberPartitions(int n) {
ArrayList<int[]> result = new ArrayList<>();
int[] initial = new int[n];
initial[0] = n;
result.add(initial);
while (result.get(result.size() - 1)[0] > 1) {
int[] lastPartition = result.get(result.size() - 1);
int posOfLastNotOne = 0;
for(int k = lastPartition.length - 1; k >= 0; k--) {
if (lastPartition[k] > 1) {
posOfLastNotOne = k;
break;
}
}
int[] newPartition = new int[n];
for (int j = posOfLastNotOne + 1; j < lastPartition.length; j++) {
if (lastPartition[posOfLastNotOne] - 1 > lastPartition[j]) {
System.arraycopy(lastPartition, 0, newPartition, 0, lastPartition.length);
newPartition[posOfLastNotOne]--;
newPartition[j]++;
result.add(newPartition);
break;
}
}
}
return result;
}
这是我的 Rust 实现(灵感来自Python 算法和数据结构):
#[derive(Clone)]
struct PartitionIter {
pub n: u32,
partition: Vec<u32>,
last_not_one_index: usize,
started: bool,
finished: bool
}
impl PartitionIter {
pub fn new(n: u32) -> PartitionIter {
PartitionIter {
n,
partition: Vec::with_capacity(n as usize),
last_not_one_index: 0,
started: false,
finished: false,
}
}
}
impl Iterator for PartitionIter {
type Item = Vec<u32>;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None
}
if !self.started {
self.partition.push(self.n);
self.started = true;
return Some(self.partition.clone());
} else if self.n == 1 {
return None;
}
if self.partition[self.last_not_one_index] == 2 {
self.partition[self.last_not_one_index] = 1;
self.partition.push(1);
if self.last_not_one_index == 0 {
self.finished = true;
} else {
self.last_not_one_index -= 1;
}
return Some(self.partition.clone())
}
let replacement = self.partition[self.last_not_one_index] - 1;
let total_replaced = replacement + (self.partition.len() - self.last_not_one_index) as u32;
let reps = total_replaced / replacement;
let rest = total_replaced % replacement;
self.partition.drain(self.last_not_one_index..);
self.partition.extend_from_slice(&vec![replacement; reps as usize]);
if rest > 0 {
self.partition.push(rest);
}
self.last_not_one_index = self.partition.len() - (self.partition.last().cloned().unwrap() == 1) as usize - 1;
Some(self.partition.clone())
}
}
Java 实现。可以从记忆中受益。
public class Partition {
/**
* partition returns a list of int[] that represent all distinct partitions of n.
*/
public static List<int[]> partition(int n) {
List<Integer> partial = new ArrayList<Integer>();
List<int[]> partitions = new ArrayList<int[]>();
partition(n, partial, partitions);
return partitions;
}
/**
* If n=0, it copies the partial solution into the list of complete solutions.
* Else, for all values i less than or equal to n, put i in the partial solution and partition the remainder n-i.
*/
private static void partition(int n, List<Integer> partial, List<int[]> partitions) {
//System.out.println("partition " + n + ", partial solution: " + partial);
if (n == 0) {
// Complete solution is held in 'partial' --> add it to list of solutions
partitions.add(toArray(partial));
} else {
// Iterate through all numbers i less than n.
// Avoid duplicate solutions by ensuring that the partial array is always non-increasing
for (int i=n; i>0; i--) {
if (partial.isEmpty() || partial.get(partial.size()-1) >= i) {
partial.add(i);
partition(n-i, partial, partitions);
partial.remove(partial.size()-1);
}
}
}
}
/**
* Helper method: creates a new integer array and copies the contents of the list into the array.
*/
private static int[] toArray(List<Integer> list) {
int i = 0;
int[] arr = new int[list.size()];
for (int val : list) {
arr[i++] = val;
}
return arr;
}
}