Показаны сообщения с ярлыком рекуррентные соотношения. Показать все сообщения
Показаны сообщения с ярлыком рекуррентные соотношения. Показать все сообщения

среда, 23 мая 2012 г.

Вычисление факториала на Си

Как написать программу вычисления факториала в Си? Точно также как на любом другом языке программирования. Если число n! небольшое, то можно использовать и длинное целое. А если n=50, 100, 500 и более? То надо использовать умножение длинных чисел. Немного ее модифицировав. Так как один из множителей входит в диапазон целых чисел, то для хранения большого второго множителя используем массив целых чисел. Какой должен быть при этом размер масива? Можно оценить, используя обычный калькулятор или написав программу с дробными числами. Показатель степени в экспоненциальной форме даст граничное значение. Так для 500! потребуется 1137 цифр, а для 1000! - уже 2567. Возьмем массив до 10000 элементов. Нулевой элемент хранит последнюю цифру факториала. Тогда a[0]=1. В нашем случае нужно ораничить массив до первой цифры. Пусть это будет k. И в начале оно равно 0. По мере того, как число увеличивается, k тоже будет увеличиваться. Так для n=7 получим следующие шаги цикла по i:
i=0 a[0]=1, k=0
i=1 a[0]=1, k=0 - 0!=1!=1, поэтому цикл можно начать с двух.
i=2 a[0]=1*2=2, k=0
i=3 a[0]=2*3=6, k=0
i=4 a[0]=6*4=24, k=0
Т.е., если получившееся число больше 9, то нужно разбить его на разряды: a[0]=4, a[1]=2, и теперь k=1.
i=5 a[0]=5*4=20, здесь запомним цифру 2 для переноса в следующий разряд. Тогда a[0]=0, d=2,
      a[1]=2*5+2=12. Здесь опять запомним цифру 1 для переноса в следующий разряд: a[1]=2, d=1. Так как мы дошли до конечного k, то, также как и в предыдущем случае для i=4, увеличиваем его и можно еще раз пройти по тому же циклу j:
       a[2]=0*5+1=1, d=0.
i=6  a[0]=a[0]*6+d=0*6+0=0, d=0
       a[1]=a[1]*6+d=2*6+0=12, d=1, a[1]=2
       a[2]=a[2]*6+d=1*6+1=7, d=0.
i=7  a[0]=a[0]*7+d=0*7+0=0, d=0
       a[1]=a[1]*7+d=2*7+0=14, d=1, a[1]=4
       a[2]=a[2]*7+d=7*7+1=50, d=5, a[2]=0, k=k+1=3
       a[3]=a[3]*7+d=0*7+5=5, d=0
Получим во внешнем цикле по i от 2 до n с шагом 1 еще один цикл для умножения каждой цифры на число i. Пусть это будет цикл по j от 0 до k с шагом 1. Запишем тело цикла, учитывая все выше изложенные шаги и замечания:
      a[j]=a[j]*i+d; если a[j]<10, то d=0, иначе d=целое при делении a[j] на 10, a[j]=остаток деления a[j] на 10 (здесь важен именно такой порядок, так как если мы сразу переприсвоим элемент массива, то d может стать равным 0);  если j конечное, то увеличим k на единицу и еще раз пройдем цикл. Если a[j] будет больше 100, то этот блок повторится до тех пор, пока все разряды не займут свое место.
      Все - число n! готово. Осталось вывести. Так как k хранит все разряды результирующего числа, то в цикле по i от k до 0 выведем все цифры без пробела, начиная с первой цифры.

Получим следующую программу:

#include <stdio.h>
int main()
{
 int n;
 int a[10000];
 int d,i,j,m,k;
 scanf("%d",&n);
 for(i=0;i<=10000;i++) a[i]=0; //обнуляем все элементы массива, кроме первого  a[0]=1;


 k=0; //количество цифр в результирующем массиве цифр
  d=0; //остаток от деления на 10 для переноса в следующий разряд for(i=2;i<=n;i++)
 {

    for (j=0;j<=k;j++)
    {
      a[j]=a[j]*i+d; /*умножаем каждую цифру в массиве, начиная с конца (в нашем случае нумерация последней цифры в числе начинается с нуля.*/      if (a[j]<10) d=0;

      else
/*если результат больше 9, то запоминаем цифру для переноса в следующий разряд*/        {
            d=a[j]/10;

            a[j]=a[j]%10;
            if (j==k) ++k; /*если это последняя цифра, то увеличиваем количество цифр в массиве. Вот эта конструкция на Паскале не сработает, так как начальное и конечное значение в цикле for не изменяется в теле цикла.*/
        }
     }

  }
//вывод производим с начальной цифры, т.е. с к-го номера до нулевогоfor(i=k;i>=0;i--)printf("%d",a[i]);
return 0;
}


А теперь вопрос: почему при выполнении этой программы при любом n получаем 1? Где ошибка в программе? Замечу, что в самом алгоритме ошибок нет.

среда, 11 мая 2011 г.

Динамическое программирование

Несколько слов об этом методе. Что значит динамическое и еще программирование? Можно подумать, что нужно как то по новому программировать. На самом деле необходимо просто расписать решение задачи на каждом шаге и получить некоторую рекуррентную зависимость от предыдущего шага, при этом можно рассматривать не все варианты, а только те, которые не совпадают на предыдущем шаге или имеют симметричные значения.
Рассмотрим следующую задачу: даны N монет с указанием достоинств. Определить, можно ли распределить их на две кучки с наименьшим разрывом между ними. Например, для N=4 даны 4, 3, 7, 2. Тогда их можно распределить на две кучки так:
1 шаг: 4    -    0 -> разность =4
2 шаг: 4+3-    0 -> разность =7
  или  4 - 3       -> разность =1
3 шаг: 4+3+7 - 0 -> разность=14
  или  4+3 - 7     -> разность=0
  или  4+7 - 3     -> разность =8
  или  4 - 3+7    -> разность =-6
3 шаг: 4+3+7+2 - 0 -> разность =16
  или  4+3+2 - 7     -> разность =2
  или  4+7+2 - 3     -> разность =10
  или  4+2 - 3+7    -> разность =-4
  или  4+3+7 - 2 -> разность =12
  или  4+3 - 7+2 -> разность =-2
  или  4+7 - 3+2 -> разность =6
  или  4 - 3+7+2 -> разность =-8
Итак, получим решение 4, 3, 2 в первой кучке и 7 во второй или 4,3/7, 2 с минимальным разрывом в 2.
Я привела все варианты, но если посмотреть внимательно, то можно заметить, что хранить все варианты нет необходимости, можно запомнить на каждом шаге разность и знак + или - для текущей монеты: + положили в первую кучку, - во вторую.
1 шаг: 4    -    0 -> разность =4
2 шаг: 4+3-    0 -> разность =4+3=7
  или  4 - 3       -> разность =4-3=1
3 шаг: 4+3+7 - 0 -> разность =7+7=14
  или  4+3 - 7     -> разность =7-7=0
  или  4+7 - 3     -> разность =1+7=8
  или  4 - 3+7    -> разность = 1-7=-6
3 шаг: 4+3+7+2 - 0 -> разность =14+2=16
  или  4+3+2 - 7     -> разность =0+2=2
  или  4+7+2 - 3     -> разность =8+2=10
  или  4+2 - 3+7    -> разность = -6+2=-4
  или  4+3+7 - 2 -> разность =14-2=12
  или  4+3 - 7+2 -> разность =0-2=-2
  или  4+7 - 3+2 -> разность =8-2=6
  или  4 - 3+7+2 -> разность =-6-2=-8
Но может быть решение найдено, если начинать с другой монеты. Будем рассматривать все такие варианты, но искать при этом каждый раз минимум среди разностей по модулю. Хранить будем в массиве +1 или -1 в зависимости от полученного минимума. На следующем шаге используем разность без модуля.
Представим получаемые разности в виде таблицы:

Начинаем с
4
3
7
2
4
4
MIN(4+3,4-3)=1
MIN(1+7,1-7)=-6
MIN(-6+2,-6-2)=-4
3
MIN(3+4,3-4) =-1
-1
MIN(-1+7,-1-7)=-6
MIN(-6+2,-6-2)=-4
7
MIN(7+4,7-4) =3
MIN(3+3,3-3)=0
0
MIN(0+2,0-2)=2
2
MIN(2+4,2-4) =-2
MIN(-2+3,-2-3)=1
MIN(1+7,1-7)=-6
-6
Полученное значение в последнем столбце сравниваем с предыдущим минимумом и если оно равно 0, то решение найдено, иначе ищем минимум по модулю. В примере это MIN(-4,-4,2,-6)=2.
Пусть исходные значения хранятся в массиве В:=(4, 3, 7, 2). Результаты вычислений храним в двумерном массиве A(4,4). Тогда алгоритм вычисления запишем в виде функции:
R=B(i)
             +1, если i=j; R не изменяется
A(i,j)= если i<>j    +1, если |R+B(j)|<=|R-B(j)|; R=R+B(j)
                               -1, если |R-B(j)|<|R+B(j)|; R=R-B(j)
Для каждого i-го решения после вычисления всех значений i-ой строки находим Rmin = MIN ( Rmin, |R|). При этом запоминаем номер строки Imin для дальнейшего вывода. Rmin в начале можно присвоить сумме всех значений массива B(j).
При выводе если A(Imin,J)=+1, то B(J) в первую кучку, если -1 - во вторую.
Итак, мы разобрали задачу с элементами динамического программирования. Попробуйте написать программу самостоятельно. Для таблицы можно использовать логические значения TRUE (+1) и FALSE (-1). Можно не использовать всю таблицу, а только два двумерных массива: один хранит значения на текущем шаге, другой - значения с минимумом.
Иногда при подборе такого решения нужно доказывать состоятельность выбранного пути решения. Не факт, что при таком выборе пути мы найдем верное решение или оно будет не лучшим из всех возможных. Поэтому иногда приходится выполнять полный перебор решений. Но это видно сразу из заданных диапазонов и времени выполнения: обычно это или время завышено или диапазон при малом времени выполнении слишком мал.
Вот выдержки с сайта acmp.ru по этому вопросу:
Динамическое программирование (ДП) - метод решения задач путем составления последовательности из подзадач таким образом, что:
  • первый элемент последовательности (возможно несколько элементов) имеет тривиальное решение
  • последний элемент этой последовательности - это исходная задача
  • каждая задача этой последовательности может быть решена с использованием решения подзадач с меньшими номерами
Другими словами, для решения задачи T методом ДП составляется некоторая последовательность подзадач T1, T2, ... Tn такая, что решение задачи T1 уже имеет решение, T = Tn, и самое главное, что зная решения задач T1, T2, ... Ti-1 можно вывести решение задачи Ti для любого i = 2..n .
Доказательство работоспособности метода ДП напрямую следует из принципа математической индукции. Действительно, если нам удастся для некоторой задачи построить вышеупомянутую последовательность, удовлетворяющую данным свойствам, то зная T1 мы легко вычислим T2 (при i=2), а из решений задач T1 и T2 будет следовать решение задачи T3 (при i=3) и т.д. увеличивая значение i мы будем находить решение задачи Ti через ранее решенные задачи до тех пор, пока i не достигнет значения n, а решение задачи Tn эквивалентно решению исходной задачи.
Динамическое программирование обычно придерживается двух подходов к решению задач:
  • Нисходящее ДП: задача разбивается на подзадачи меньшего размера, они решаются и затем комбинируются для решения исходной задачи. Используется запоминание для решений часто встречающихся подзадач.
  • Восходящее ДП: Все подзадачи, которые впоследствии понадобятся для решения исходной задачи просчитываются заранее и затем используются для построения решения исходной задачи. Этот способ лучше нисходящего ДП в смысле размера необходимого стэка и количества вызова функций, но иногда бывает нелегко заранее выяснить решение каких подзадач нам потребуется в дальнейшем.
Оттуда задача (в вольном переводе): есть последовательность N чисел. При переходе от текущего элемента к следующему суммируется разность этих чисел по модулю или, если перепрыгиваем через элемент, то утроенная разность этих чисел. Найти минимум этой суммы. Например, для элементов 1 5 10 получим сумму 9 ((5-1)+(10-5)), а если 1 5 2 - сумма равна 3 ((2-1)*3).
Пусть есть 7 чисел:
1 5 2 3 9 1 10
Рассчитаем разности по модулю по парам:
4 3 1 6 8 9
и через один утроенные разности по модулю:
3 6 21 6 3
Как здесь применить метод динамического программирования? Что нам дают эти разности? Если брать сразу min(4,3)=3, то дальше нужно перепрыгнуть и получим min(21,1)=1, а далее min(6,6)=6. Но какой брать минимум? Если через один, то сумма равна 3+1+6+3=13, а если подряд, то сумма равна 3+1+6+8+9=27.
Еще пример для 10 чисел:
3 4 8 10 1 7 1 10 12 15
1 4 2 9 6 6 9 2 3
15 18 21 9 0 9 33 15
Min(1,15)=1, min(4,18)=4, min(2,21)=2, min(9,9)=9 и далее либо 6+6+9+2+3 либо 6+9+2+3. Как определить дальнейшие действия? Пока не дойдешь до конца не узнаешь. А если таких развилок несколько? Вроде намечается путь рекурсивного вызова с возвратом. Но есть другой способ.
Пусть мы "стоим" на 2-м элементе. Как можно придти в это положение? С какой суммой или "багажом"? Пока только одним способом: 4-3=1. Запомним это число в переменную Р[2]. Перейдем к 3-му элементу. К нему можно придти двумя способами: (8-3)*3=15 или (8-4)+P[2]=5. Среди этих двух чисел наименьшее 5. Запомним в Р[3].
Далее, для i=4 (10-8)+P[3]=7 или (10-4)*3+P[2]=19. P[4]=7.
Для i=5 (10-1)+P[4]=16 или (8-1)*3+P[3]=26. P[5]=16.
Для i=6 (7-1)+P[5]=24 или (10-7)*3+P[4]=16. P[6]=16.
Для i=7 (7-1)+P[6]=22 или (1-1)*3+P[5]=16. P[7]=16.
Для i=8 (10-1)+P[7]=25 или (10-7)*3+P[6]=25. P[8]=25.
Для i=9 (12-10)+P[8]=27 или (12-1)*3+P[7]=49. P[9]=27.
Для i=10 (15-12)+P[9]=30 или (15-10)*3+P[8]=40. P[10]=30.
Итак, запишем функцию для получения решения на каждом i-ом шаге:
P[2]=abs(A[2]-A[1]), P[3]=min(abs(A[3]-A[2]),abs(A[3]-A[1])*3),
P[i] = min (abs(A[i]-A[i-1])+P[i-1],abs(A[i]-A[i-2])*3)+P[i-2]), если i>2.
Осталось написать функцию минимума двух чисел и алгоритм готов.
Подумайте, как в этой задаче можно обойтись без массивов?
Удачи!

пятница, 7 января 2011 г.

Рекурсия и не только

Рекурсия предполагает решение задачи с помощью подпрограмм (процедуры или функции) с обязательным вызовом самой себя  внутри кода. Например, вы открываете сумку, достаете кошелку, открываете ее, достаете кошелек, открываете и достаете деньги, чтобы оплатить проезд, получаете билет и сдачу, и проделываете обратную процедуру: деньги в кошелек, кошелек в кошелку, кошелка в сумку. Или другой пример: открытие матрешек или, например, дверей в проходе по коридору царского дворца. Можно придумать еще много ассоциаций, но суть будет одна: нужно каждый раз что-то "открывать" (вызывать подпрограмму) до тех пор, пока не встретится искомое решение или не выполнится какое-то условие (условие останова или возврата). Вроде все просто и понятно.
Но есть подводные камни: такой алгоритм не может быть достаточно долгим или бесконечным, так как память компьютера конечна, и может возникнуть сообщение "Stack overflow". Такой алгоритм похож на некое подобие сбора и разбора пирамидки, так как он организуется по принципу стека (первый вошел, последним вышел). Таким образом стековая память заполняется этими "кодами подрограмм" - еще не завершен первый код, как уже вызывается следующий и т.д. Кроме этого в подпрограммах передаются параметры, которые тоже требуют размещения в памяти компьютера каждый раз при вызове подпрограммы и висит там до тех пор, пока подпрограмма не завершится. Т.е. нужно иногда думать не только как организовать рекурсию, но и какие параметры туда стоит передавать, а какие достаточно объявить глобально. Непростая задача даже для опытного программиста!!! Что же делать начинающим?

Ну во-первых, нужно знать как рекурсию преобразовать в цикл или рекуррентное соотношение.
Во-вторых, есть готовые рекурсивные подпрограммы для решения большого класса задач: сортировка, обход дерева, поиск пути в лабиринте и т.д. Бери, выучи и пользуйся.

Но наша цель не использование готовых решений, а самостоятельное их нахождение.

Разберем простой пример: вычисление факториала (это произведение первых n натуральных чисел). Его можно записать так: f:=n*f(n-1), если n>0 и f:=1, если n=0.
Оформим рекурсию в виде функции:
function f(n:longint):longint;
begin
  if n=0 then f:=1 {условие останова}
         else f:=f(n-1)*n; {рекурсивный вызов функции}
end;
Итак, покажем как рекурсивно вызывается код подрограммы при вызове функции F(4):
  1. n=4, вызов F(3) в формуле F:=F(n-1)*n;
  2. n=3, вызов F(2) в формуле F:=F(n-1)*n;
  3. n=2, вызов F(1) в формуле F:=F(n-1)*n;
  4. n=1, вызов F(0) в формуле F:=F(n-1)*n;
  5. n=0, и только теперь F:=1.
  6. Теперь "собираем матрешку". Возвращаемся в подпрограмму п. 4 и вместо F(0) подставим 1 в формулу F:=F(n-1)*n; получим следующее выражение F:=1*1=1.
  7. Возвращаемся в подпрограмму п. 3 и вместо F(1) подставим 1 в формулу F:=F(n-1)*n; получим следующее выражение F:=1*2=2.
  8. Возвращаемся в подпрограмму п. 2 и вместо F(2) подставим 2 в формулу F:=F(n-1)*n; получим следующее выражение F:=2*3=6.
  9. Возвращаемся в подпрограмму п. 1 и вместо F(3) подставим 6 в формулу F:=F(n-1)*n; получим следующее выражение F:=6*4=24.
  10. Возвращаемся в точку вызова функции F(4) и получаем в результате число 24.
Я расписала подробно саму процедуру вызова и возврата, чтобы затем было проще представлять алгоритмы в виде рекурсивных подпрограмм.
Но этот пример не используется в программировании. Вместо этого используют обычный алгоритм вычисления произведения в цикле:
F:=1;
For i:=1 to n do 
  F:=F*i;
В цикле используется рекуррентное соотношение, когда следующее значение переменной относительно предыдущего изменяется на некоторую величину.
Такие соотношения используются и в вычислениях суммы ряда, где есть факториалы и степени. При этом не надо вычислять сами факториалы, достаточно определить изменяемую величину и вставить ее вместо переменной i в предыдущем примере. Например, вычислить сумму ряда для a[n]=x^n/n!. Получим нужный множитель из формулы a[n+1]=I*a[n], где a[n+1]=x^(n+1)/(n+1)! => I=a[n+1]/a[n]=x^n/n! /(x^(n+1)/(n+1)!)= (n+1)/x. Итак, наш код будет выглядеть так:

F:=1; {при n=0, a[0]=x^0/0!=1/1=1}
For i:=1 to n do 
  F:=F*(i+1)/x;

В литературе приводится пример нахождения наибольшего общего делителя двух чисел:
                      HOД(m-n, n), если m>n 
НОД(m, n) = HOД(n-m, m), если n>m
                      m, если m=n 
Приведем пример функции:
Function HOD(m,n:longint):longint;
begin
  if m=n then HOD:=m
         else if m>n then HOD:=HOD(m-n,n)
                     else HOD:=HOD(n-m,m); 
end; 
Сравнивая две функции НОД(m,n) и F(n) можно заметить, что в начале кода всегда проверяется условие останова и возвращается то значение, которое не имеет вызова функции,  а только потом используются формулы с рекурсивным вызовом.
Практически после получения формулы для расчета НОД(m,n) и F(n) можно сразу написать по ней рекурсивную функцию.
В функции HOD вместо вычисления разности можно использовать операцию mod.
Эту функцию можно использовать для вычисления НОД нескольких переменных, используя тот факт, что НОД (m,n,c)=НОД(НОД(m,n),c). Попробуйте не использовать рекурсивную функцию в этом случае. Замените ее циклом.
Также можно использовать эту функцию для вычисления НОК(m,n)=m*n/HOД(m,n).

Приведем формулу для возведение числа a в степень натурального положительного n:

                   SQR (Stepen(n div 2)), если n  четное

Stepen(n) = Stepen(n -1)*a, если n нечетное
                   1, если n=0
Приведем пример применения формулы для n=10: a^10 = (a^5)^2 = (a*a^4)^2 = (a*(a^2)^2)^2 = (a*((a^1)^2)^2)^2 = (a*((a*a^0)^2)^2)^2 = (a*((a*1)^2)^2)^2. Напишите сами рекурсивную функцию. Для проверки нечетности можно использовать функцию ODD(n).

В дальнейшем рассмотрим другие алгоритмы, которые приводят к рекурсии и способы, когда ее можно оформить в виде цикла и рекуррентных соотношений.