Динамично оптимиране - числа на Фибоначи и биномни коефициенти
[8.3.1,
8.3.2]
Числа на Фибоначи
* Числа на Фибоначи: F(0) = F(1) = 1 и F(i)
= F(i-1) + F(i-2) за i
> 1.
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
* Класически рекурсивен неефективен алгоритъм:
unsigned long fib(unsigned n)
{ if (n < 2) return 1;
return fib(n - 1) + fib(n - 2);
}
Експоненциална сложност - многократно извикване на
функцията с един и същи аргумент!
* Динамично програмиране - със запомняне на вече пресметнатите
стойности:
#include <stdio.h>
#define MAX 256
unsigned n = 10;
unsigned long m[MAX + 1];
unsigned long fibMemo(unsigned n)
{ if (n < 2) m[n] = 1;
else if (0 == m[n]) m[n] =
fibMemo(n - 1)
+ fibMemo(n - 2);
return m[n];
}
int main()
{ memset(m, 0, MAX*sizeof(*m));
scanf("%u", &n);
printf("\n%u-тото число на Фибоначи
е: %lu", n, fibMemo(n));
return 0;
}
Линейна сложност - всяка стойност се пресмята само
веднаж.
* Друга формула за пресмянате на n-тото
число на Фибоначи - със запомняне
на вече пресметнате стойност.
F(n) = F2(n/2) + F2(n/2
- 1), за n четно;
F(n) = F(n - 1) + F(n -
2),
за n нечетно.
#include <stdio.h>
#define MAX 250
#define SQR(X) ((X)*(X))
unsigned n = 10;
unsigned long m[MAX + 1];
unsigned long fMemo(unsigned n)
{ if (n < 2) m[n] = 1;
else if (0 == m[n])
if (1 == n%2)
m[n] = fMemo(n - 1) + fMemo(n - 2);
else m[n] =
SQR(fMemo(n/2)
+ fMemo(n/2 - 1));
return m[n];
}
int main()
{ memset(m, 0, MAX*sizeof(*m));
scanf("%u", &n);
printf("\n%u-тото число на Фибоначи
е: %lu", n, fibMemo(n));
return 0;
}
Логаритмична сложност.
Биномни коефициенти
Нека C(n, k) е биномният
коефициент
"n над k" или комбинации без повторения от n
елемента
k-ти клас. Общата формула е: C(n, k)
= n!/((n - k)! k!), а от триъгълника на Паскал
имаме
и рекурентна формула:
C(n, k) = 1, за k = 0 или k
= n;
C(n, k) = C(n - 1, k -
1) + C(n - 1,
k), за 0 < k < n;
C(n, k) = 0, за k > n.
* Рекурсивен неефективен алгоритъм:
unsigned long binom(unsigned n, unsigned
k)
{ if (k > n) return 0;
if (k == 0 || k == n) return 1;
return binom(n - 1, k - 1) +
binom(n - 1,
k);
}
* Динамично програмиране - със запомняне на вече пресметнатите
стойности.
Не е необходимо да се пази цялата таблица
C(n, k), а само един ред от таблицата -
предишния.
#define MAX 200
unsigned long m[MAX];
unsigned long binomDynam(unsigned n,
unsigned
k)
{ unsigned i, j;
for (i = 0; i <= n; i++)
{ m[i] = 1;
if (i > 1)
{ if (k < i - 1) j =
k; else j = i - 1;
for (;
j >= 1; j--) m[j] += m[j - 1];
}
}
return m[k];
}
Пример: n = 4, k
= 3; C(4,3) = 4.
i |
j |
m
0 1 2 3 4
|
0 |
- |
1 1 1 1 1 |
1 |
- |
1 1 1 1 1 |
2 |
1 |
1 2 1 1 1 |
3 |
2 |
1 3 3 1 1 |
4 |
3 |
1 4 6 4 1 |
Този алгоритъм има сложност O(nk).
Да се реализира рекурсивен
вариант, основан на динамичното програмиране.
Задача: (ЗИМНИ МАТЕМАТИЧЕСКИ ПРАЗНИЦИ,
БУРГАС, 29 януари
2005, ТЕМА
ЗА ГРУПА А (12 КЛАС))
Дадени са N дъски с правоъгълна форма, номерирани с
числата от 1 до N (N < 2000). Дъските са с еднаква ширина, но с
различна височина, като K-тата дъска е висока K дециметра. Подреждаме
вертикално N-те дъски една до друга, така че да образуваме ограда. В
зависимост от начина на подреждането се образуват участъци с
последователно намаляващи дължини на дъските. Тези участъците
обикновенно са повече от един, но може и цялата преграда да образува
само един участък. Възможно е някои участъци да се състоят само от една
дъска. Например, ако подредим 9 дъски, така че височините им да
образуват последователността 6, 4, 2, 5, 3, 9, 7, 2, 1, 8, имаме 4
такива участъка 6, 4, 2; 5, 3; 9, 7, 2, 1 и 8. Колко от всичките
възможни наредби на дъските ще образуват преграда, съдържаща точно P
участъка (0 < P < N + 1).
Напишете програма, която решава тази задача.
Програмата трябва да прочете от стандартния вход стойностите на N и P,
разделени с един интервал и да изведе на стандартния изход търсения
брой прегради по модул 1020847 (т.е. остатъка на търсения брой при
деление на 1020847).
ПРИМЕР 1
Вход
4 3
Изход
11
|
ПРИМЕР 2
Вход
20 2
Изход
27708
|
Решение:
Решаваме задачата по метода на динамичното
оптимиране. Нека е
дефинирана функция T(P,Q), която връща колко са пермутациите на P
елемента, в които има точно Q намаляващи подредици. Тогава T(N,K) е
отговорът на задачата. Допускаме, че когато искаме да изчислим T(P,Q),
сме изчислили всички стойности T(R,S), за R < P и S <=
Q.
Да разсъждаваме по следния начин. Нека имаме една
пермутация на P-1 елемента и започнем да добавяме числото P на всички
възможни места. Тогава ако го добавим някъде във вече обособена
намаляваща подредица, тя ще се разцепи на две, а ако го добавим точно
преди началото на намаляваща подредица, броят на намаляващите редици в
пермутацията ще се запази. Освен това поради начина на добавяме няма да
повторим пермутации на P елемента. Понеже имаме изискването редиците да
бъдат точно Q на брой, а местата, на които няма промяна в броя редици,
са точно Q, то за T(P,Q) получаваме следната рекурентна формула:
T(P,Q) = Q*T(P-1,Q) +
(P+1-Q)*T(P-1,Q-1).
Освен това
Т(1,1) = 1,
понеже ако имаме един единствен елемент, броят начини да образуваме
една редица е точно 1.
Сложността на описаното решение и от гледна точка на
време, и от гледна точка на памет, е O(N2).
Примерна реализация:
#include <stdio.h>
#define MOD 1020847
#define MAXN 2048
int t[MAXN][MAXN];
int main()
{ int N, P, i, j;
scanf("%d%d", &N, &P);
t[1][1] = 1;
for (i = 2; i <= N;i++)
for (j = 1; j <= i; j++)
t[i][j] = ((j * t[i - 1][j])%MOD +
((i + 1 - j)*t[i - 1][j - 1])%MOD)%MOD;
printf("%d\n", t[N][P]);
return 0;
}
Автор: Слави Маринов (http://infoman.musala.com/)