10. Динамично оптимиране - II

Братска подялба [8.2.2]
    Двама братя трябва да си поделят комплект от n подаръка. Всеки подарък има стойност цяло положително число. Да се разделят подаръците на две части със стойности  a и b, така, че |a - b| да има най-малка стойност.

    Нека сумата от стойностите на всички подаръци е p. Правим масив c с p елемента, като c[i] = 1 ако i може да се получи като сума на някои подаръци, в противен случай
c[i] = 0. Решението на задачата ще бъде индекса на най-близкия до p/2 ненулев елемент на c.

Нека стойностите на подаръците да са:
3, 2, 3, 2, 2, 77, 89, 23, 90, 11

Решение.

a = 136, b = 166

Кои подаръци дават стойност a?
Вместо 1 или 0 в елемента c[i] ще пазим номера на подаръка, последен добавен, за да се получи стойност i.

Решение.

11 + 90 + 23 + 2 + 2 + 3 + 2 + 3 = 136
89 + 77  = 166


    Ако братята са трима? А повече от трима?

Числа на Фибоначи [8.3.1]

* Числа на Фибоначи: 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-тото число на Фибоначи e", %lu", n, fibMemo(n));
return 0;
}
Линейна сложност - всяка стойност се пресмята само веднаж.

Биномни коефициенти [8.3.2]

Нека 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 = 5, k = 3; C(5,3) = 10.
i k<i-1
начално j m 0  1  2  3  4
0 -
-   1  1  1  1  1
1 -
-   1  1  1  1  1
2 3<2-1
1
  1  2  1  1  1
3 3<3-1
2   1  3  3  1  1
4 3<4-1
3   1  4  6  4  1 
5
3<5-1
3
  1  5 10 10  5
Този алгоритъм има сложност O(nk).

Най-дълга обща подредица [8.2.6]

Дадени са две редици (от числа или символи):

X = (x1, x2, ..., xm) и  Y = ( y1, y2, ..., yn)

Търси се най-дълга редица Z = (z1, z2, ..., zk), която е подредица на и Y едновременно. Z е подредица на  X, ако Z може да бъде получена чрез премахване на (0 или няколко) членове на X.

Най-напред ще търсим само дължината на най-дългата обща подредица. Ще приложим метода на динамичното оптимиране, като F(i, j) е търсената дължина за първите i члена на редицата X и първите j члена на редицата Y. Очевидно
    F
(i,0) = 0 за всяко i, F(0, j) = 0 за всяко j.
    F
(i, j) = F(i -1, j -1) + 1 за xi = yj,
    F(i, j) = max {F(i -1, j), F(i, j -1)} в противен случай.
Намираме последователно стойностите на F(i, j) и последната намерена стойност F(m,n) е решението на задачата.

Намирането на една най-дълга подредица (може да не е една) става по същия начин, като тръгваме от последния елемент и следим откъде идва максималната стойност.

// lsc.c
#include <stdio.h>
#include <string.h>
#define MAXN 100
#define MAX(a,b) (((a)>(b)) ? (a) : (b))

char F[MAXN][MAXN];
const char x[MAXN] = "acbcacbcaba";
const char y[MAXN] = "abacacacababa";
unsigned m,n;
/*
"acb cacbcaba " "a cbcac bcaba"
"a bacacacababa" "abacacacab aba"
solution "a b cac caba " "a c cac b aba"
*/
unsigned lcs_len(void)
{ unsigned i,j;
for (i=0; i<=m; i++) F[i][0]= 0;
for (j=0; j<=n; j++) F[0][j]= 0;
for (i=1; i<=m; i++)
for (j=1; j<=n; j++)
if (x[i-1] == y[j-1]) F[i][j]=F[i-1][j-1]+1;
else F[i][j] = MAX(F[i-1][j], F[i][j-1]);
return F[m][n];
}
void print(unsigned i, unsigned j)
{ if (i == 0 || j == 0) return;
if (x[i-1] == y[j-1])
{ print(i-1,j-1);
printf("%c", x[i-1]);
}
else if (F[i][j] == F[i-1][j]) print(i-1,j);
else print(i,j-1);
}
int main()
{ m = strlen(x);
n = strlen(y);
printf("%u\n", lcs_len());
print(m,n);
return 0;
}
9
accacbab


acbcacbcaba
abacacacababa




0
1
2
3
4
5
6
7
8
9
10
11


""
a c
b
c
a
c
b
c
a
b
a
0
""
0
0
0
0
0
0
0
0
0
0
0
0
1
a
0
1
1
1
1
1
1
1
1
1 1
2
b
0
1
1
2
2
2
2
2
2
2
2
2
3
a
0
1
1
2
2
3
3
3
3
3
3
3
4
c
0
1
2
2
3
3
4
4
4
4
4
4
5
a
0
1
2
2
3
4
4
4
4
5
5
5
6
c
0
1
2
2
3
4
5
5
5
5
5
5
7
a
0
1
2
2
3
4
5
5
5
6
6
6
8
c
0
1
2
2
3
4
5
5
6
6
6
6
9
a
0
1
2
2
3
4
5
5
6
7
7
7
10
b
0
1
2
3
3
4
5
6
6
7
8
8
11
a
0
1
2
3
3
4
5
6
6
7
8
9
12
b
0
1
2
3
3
4
5
6
6
7
8
9
13
a
0
1
2
3
3
4
5
6
6
7
8
9