Нерекурсивный алгоритм генерации всех разбиений и композиций целого числа +10


Привет, Хабр! Вдруг снова захотелось написать сюда! Так как было время на выходных, я снова решил поиграться с алгоритмизацией и написал вот эту статейку. Хотел даже отправить в какой-нибудь рецензируемый журнал, но оценить тривиальность/нетривиальность данного материала я не в состоянии, так как программирование всего лишь мое хобби и то эпизодическое, поэтому, как обычно, заранее прошу прощения, если все окажется слишком простым и до боли знакомым.

Спасибо администрации Хабра за отзывчивость и молниеносную оперативность при восстановлении аккаунта!

Итак, плоды усилий долгих...

Нерекурсивный алгоритм генерации всех разбиений целого числа в лексикографическом
порядке, когда все элементы выстроены в порядке убывания, является альтернативным; в
Интернете представлено несколько способов порождения данных комбинаторных
объектов, однако, как это справедливо и относительно других комбинаторных алгоритмов,
реализации сводятся к двум типам — нерекурсивному и рекурсивному. Чаще можно встретить
реализации, которые не учитывают порядок вывода объектов или осуществляют
вывод по принципу дробления числа.

Приведенная ниже реализация работает по обратному принципу: исходное число изначально разбито на единицы, алгоритм работает до тех пор, пока число в нулевом индексе массива не станет равным сумме исходного числа. Особенностью данного алгоритма является то, что он крайне прост для понимания, однако это не лишает его некоторый специфики:

1) Первый объект просто выводится на экран в самом начале, таким образом, он вынесен за пределы циклов, фактически является инициализирующим;
2) Существует несколько способов реализации переноса единицы, которые могут, как упростить код, так и сделать его более запутанным;
3) Данная нерекурсивная реализация может служить наглядным примером для объяснения генерации комбинаторных объектов на нескольких процессорах, после незначительной модификации. Для разделения генерации на процессоры достаточно: а) определить элемент по его номеру и сделать инициализирующим; б) определить момент для остановки работы. Например, если известно число объектов генерируемых на одном процессоре, то достаточно ввести еще одну инкрементируемую переменную в верхний цикл и изменить условие выхода из самого верхнего цикла по достижении требуемого количества.

Код на языке PHP приведен только для демонстрации корректности алгоритма и может содержать лишние языковые средства, которые добавляют реализации избыточности.

Описание алгоритма

Дано: исходный массив в виде единиц — А (1,1,1,1,1).

Шаги
0) Если получили сумму (в случае реализации ниже, если нулевой индекс равен сумме числа), тогда остановка алгоритма.

1) Двигаясь по массиву слева направо, искать в массиве А первый минимальный элемент — x,
последний элемент не учитывается (не участвует в поиске минимального).
2) Перенести единицу из конца (последнего элемента) в найденный минимальный элемент x
(равносильно увеличению x на единицу и уменьшению на единицу последнего элемента).
3) Если в массиве А есть ноль — 0, то удалить последний элемент.
4) Разложить сумму всех элементов после измененного элемента — x – на единицы.

Пример

А=(1,1,1,1,1)
2,1,1,1
2,2,1
3,1,1

Код алгоритма на PHP
<?php
$a = array(
	1,
	1,
	1,
	1,
	1,
	1,
	1,
	1,
	1
);
print_r($a);
print '<br />';
$w = count($a);
$h = 0;

while ($a[0] != $w)
	{
	$min = $a[0];
	$c = count($a) - 1;
	$i = 0;
	while ($i != count($a) - 1)
		{
		if ($a[$i] < $min)
			{
			$min = $a[$i];
			$min2 = $i;
			}

		$i++;
		}

	if ($min2 == 0) $min2 = 0;
	$a[$min2]+= 1;
	$a[$c]-= 1;
	if (in_array(0, $a)) array_pop($a);
	array_splice($a, $min2 + 1);
	foreach($a as $v)
		{
		$sum+= $v;
		}

	$j = 0;
	$all = $w - $sum;
	while ($j != $all)
		{
		$a[] = 1;
		$j++;
		}

	print_r($a);
	print '<br />';
	unset ($all,$sum,$min,$min2);
	$h++;
	}

echo 'Amount: ' . ++$h;
?>


Update:
Операция с поиском и удалением 0 в массиве лишняя (так как используется array_splice, а затем новое заполнение массива), как правильно было замечено в комментарии участником dev26

Выводы

Хотел бы в конце поделиться одним наблюдением, я очень долго пытался понять, почему одни алгоритмы понятны сразу и легки для кодирования, а другие заставляют мучиться… и мучиться порой долго. Должен отметить, что этот алгоритм у меня получилось закодировать почти сразу, но только после того, как я получил однозначно понятное описание каждого шага. И тут есть важный момент, понять алгоритм и описать — задачи одна другой не легче. Однако, в алгоритмизации и составлении описания, особенно важным оказывается то, какими глаголами описываются действия в алгоритме — это (субъективно) в конечном счете может влиять и на конечную реализацию.

Литература
[1] Donald E. Knuth. The Art of Programming. Vol. 4. 2008.
[2] Dennis Ritchie and Brian Kernighan. The C Programming Language. 1978.
[3] Aleksandr Shen. Algorithms and Programming: Problems and Solutions.
[4] ru.wikipedia.org/wiki/Разбиение_числа
[5] en.wikipedia.org/wiki/De_Arte_Combinatoria

P.S. Несмотря на приведенный список литературы, алгоритм пришлось выводить заново.
Еще одно дополнение:
Вынос первого элемента из циклов при данном подходе имеет силу как для генерации разбиений, так и для генерации сочетаний, перестановок. В принципе данный подход (хоть и несколько избыточный при реализациях) вполне обобщается для генерации других комбинаторных объектов.

UPDATE
Алгоритм разбиений в соединении с алгоритмом перестановок позволяет генерировать и все композиции числа. Идея простая: для каждого разбиения вызывается функция генерации всех перестановок для этого разбиения.

Генерация композиций. PHP
<?php
set_time_limit(0);
//Функция генерации всех перестановок для разбиения
function perm($b) {
$b=array_reverse($b);
$a=array_reverse($b);
       while (1) {
	   	   	print_r($a);
			print '<br/>';
		   if ($a ==$b) break;
   $i=1;
	while($a[$i] >= $a[$i-1]) {
    $i++;
}
   $j=0;
	  while($a[$j] <= $a[$i]) {	
    $j++;	
}
	  $c=$a[$j];
	  $a[$j]=$a[$i];
	  $a[$i]=$c;
$c=$a;
	 $z=0;
	for ($i-=1; $i > -1; $i--) $a[$z++]=$c[$i];	
}
}

//Генерация всех композиций. Эта часть генерирует разбиения
//Заполнение массива
$g=0;
while ($g != 4){
$a[]=1;
$g++;
}

print_r($a);
print '<br>';
$w = count($a);
while ($a[0] != $w)
	{
	$min = $a[0];
	$c = count($a) - 1;
	$i = 0;
	while ($i != count($a) - 1)
		{
		if ($a[$i] < $min)
			{
			$min = $a[$i];
			$min2 = $i;
			}
		$i++;
		}
	if ($min2 == 0) $min2 = 0;
	$a[$min2]+= 1;
	$a[$c]-= 1;
	array_splice($a, $min2 + 1);
	$sum=array_sum($a);
	for ($j=0; $j != $w-$sum; $j++) $a[] = 1;
	perm ($a);
	unset ($sum,$min,$min2);
}

?>

При чтении описания данного алгоритма получается ли у Вас вывести его на листке бумаги?

Проголосовало 30 человек. Воздержалось 23 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

-->


К сожалению, не доступен сервер mySQL