Пермутация с повторение: избягване на преливане

Заден план:

Дадени са n топки, така че:

'a' balls are of colour GREEN
'b' balls are of colour BLUE
'c' balls are of colour RED
...

(разбира се a + b + c + ... = n)

Броят на пермутациите, в които тези топки могат да бъдат подредени, се дава от:

perm = n! / (a! b! c! ..)

Въпрос 1: Как мога „елегантно“ да изчисля perm, така че да избегна целочислено препълване колкото е възможно и да съм сигурен, че когато приключа с изчисляването, имам правилната стойност на perm, или знам, че крайният резултат ще прелее?

По принцип искам да избягвам използването на нещо като GNU GMP.

По избор, Въпрос 2: Това наистина лоша идея ли е и трябва ли просто да продължа и да използвам GMP?


person ArjunShankar    schedule 21.12.2011    source източник
comment
Защо искате да избегнете GMP? Като цяло искате да свършите възможно най-малко работа.   -  person Dave    schedule 21.12.2011
comment
Откриването на препълване всъщност е една от слабостите на C. Да предположим, че успеете да избегнете препълването възможно най-дълго и следователно можете да сте сигурни, че ще имате правилната стойност само ако е било възможно да я изчислите без препълване. Дори тогава все още няма да знаете дали действително е настъпило препълване.   -  person ruakh    schedule 21.12.2011
comment
@Dave: Прав си. Но проблемът все пак е интересен. Така че въпросът остава за тези, които се интересуват от "как" повече от "защо" :). Може би някой ще го използва в 8051 в интерактивен тостер :P   -  person ArjunShankar    schedule 21.12.2011


Отговори (5)


Ако имате много процесорно време, можете да направите списъци от всички факториели, след това да намерите разлагането на прости множители на всички числа в списъците, след това да съкратите всички числа отгоре с тези отдолу, докато числата са напълно намалена.

person Dave    schedule 21.12.2011
comment
Колко голямо очаквате да получите N? - person Dave; 21.12.2011
comment
N представлява броя на инструкциите в „основен блок“, оптимизиран от задната част на компилатора. Така че N може да бъде доста голямо, ако някой напише куп код без контролни изрази [но все пак е цяло число]. Алгоритъмът може да се окаже полезен за мой колега, който прилага неясен оптимизационен пропуск за неясна DSP архитектура. Необходимостта от избягване на GMP е по-скоро каприз, отколкото изискване. - person ArjunShankar; 21.12.2011
comment
За целите на този проблем този алгоритъм е достатъчно бърз. Въпреки това времето, прекарано в кодиране на алгоритъм за факторизиране и анулиране, е доста прекомерно IMO. - person tskuzzy; 21.12.2011
comment
Съгласен. Сега маркирам това правилно, защото е чисто и IMO елегантно решение. - person ArjunShankar; 22.12.2011

Те са известни като мултиномиални коефициенти, които ще обознача с m(a,b,...).

И вие можете ефективно да ги изчислите, като избягвате препълване, като използвате тази идентичност (която трябва да е доста проста за доказване):

m(a,b,c,...) = m(a-1,b,c,...) + m(a,b-1,c,...) + m(a,b,c-1,...) + ...
m(0,0,0,...) = 1 // base case
m(anything negative) = 0 // base case 2

След това е прост въпрос на използване на рекурсия за изчисляване на коефициентите. Имайте предвид, че за да избегнете експоненциално време на изпълнение, трябва или да кеширате резултатите си (за да избегнете повторно изчисляване), или да използвате динамично програмиране.

За да проверите за препълване, просто се уверете, че сумите няма да препълнят.

И да, много лоша идея е да се използват библиотеки с произволна точност, за да се извърши тази проста задача.

person tskuzzy    schedule 21.12.2011
comment
Това има смисъл. Определено е необходимо известно динамично програмиране. :-) - person ruakh; 21.12.2011
comment
Да, запомнянето е задължително :) - person tskuzzy; 21.12.2011

Най-безопасният от преливане начин е начинът, по който Дейв предложи. Намирате степента, с която простото p дели n! на сумата

m = n;
e = 0;
do{
    m /= p;
    e += m;
}while(m > 0);

Извадете показателите на p в факторизациите на a! и т.н. Направете това за всички прости числа <= n и ще получите факторизацията на мултиномиалния коефициент. Това изчисление препълва, ако и само ако крайният резултат препълва. Но мултиномиалните коефициенти растат доста бързо, така че вече ще имате препълване за сравнително малко n. За съществени изчисления ще ви трябва библиотека bignum (ако не се нуждаете от точни резултати, можете да изкарате малко по-дълго, като използвате doubles).

Дори ако използвате библиотека bignum, струва си междинните резултати да не станат твърде големи, така че вместо да изчислявате факториелите и да разделяте огромни числа, е по-добре да изчислявате частите последователно,

n!/(a! * b! * c! * ...) = n! / (a! * (n-a)!) * (n-a)! / (b! * (n-a-b)!) * ...

и за да изчислим всеки от тези фактори, нека вземем втория за илюстрация,

(n-a)! / (b! * (n-a-b)!) = \prod_{i = 1}^b (n-a+1-i)/i

се изчислява с

prod = 1
for i = 1 to b
    prod = prod * (n-a+1-i)
    prod = prod / i

Накрая умножете частите. Това изисква n деления и n + number_of_parts - 1 умножения, запазвайки междинните резултати умерено малки.

person Daniel Fischer    schedule 21.12.2011

Според тази връзка можете да изчислите многочленните коефициенти като произведение на няколко биномни коефициента, контролирайки целочислено препълване по пътя.

Това намалява първоначалния проблем до контролирано от препълване изчисляване на биномен коефициент.

person Evgeny Kluev    schedule 21.12.2011

Нотации: n! = prod(1,n) където продукт можете да познаете какво прави.

Много е лесно, но първо трябва да знаете, че за всеки 2 положителни числа (i, n > 0) този израз е положително цяло число:

prod(i,i+n-1)/prod(1,n)

По този начин идеята е изчислението на n! да се раздели на малки парчета и да се раздели възможно най-скоро.

С a, отколкото с b и т.н.

perm = (a!/a!) * (prod(a+1, a+b)/b!) * ... * (prod(a+b+c+...y+1,n)/z!)

Всеки от тези фактори е цяло число, така че ако perm няма да препълни, нито един от неговите фактори няма да свърши работа.

Въпреки това, при изчисляването на такъв коефициент може да има преливане в числителя или знаменателя, но това може да се избегне, като се направи умножение в числителя и след това деление в редуване:

prod(a+1, a+b)/b! = (a+1)(a+2)/2*(a+3)/3*..*(a+b)/b

По този начин всяко деление ще даде цяло число.

person Mihai Manole    schedule 24.08.2015