2.  Динамично оптимиране

План:
Метод на динамичното оптимиране
Задача за монети - минимален брой
Задача за монети - брой разбивания
Разбиване на естествено число
Домино подредица 
Разстояние на Левенщайн
Задачи за домашно 1 и 2


** Метод на динамичното оптимиране [8.1]

Общ принцип за решаване на трудни задачи:
Задачата се разбива на подзадачи, те от своя страна отново се разбиват на подзадачи и т.н. до достигане на достатъчно прости задачи, които могат да се решат директно. След това решенията на подзадачите се комбинират по подходящ начин, така че да се получи решение на изходната задача.
-- Разделяй и владей:  множествата на подзадачите са непресичащи се (Двоично търсене);
-- Динамично оптимиране: задължително пресичащи се подзадачи - припокриване на подзадачите (Фибоначи).

unsigned long fib(unsigned n)
{
    if (n < 2) return n;
    else return fib(n-1) + fib(n-2);
}

Едно решение за избягване на повторно пресмятане на вече пресметната стойност е да се въведе таблица на всички вече пресметнати стойности. Всеки път, когато трябва да пресметнем fib(n)за някоя конкретна стойност на n, първо ще проверяваме дали вече не сме я записали в таблицата и едва тогава, в случай че задачата все още не е била решавана, ще извършваме съответните пресмятания.

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

Оптимизационни задачи  (Задача за раницата):
-- множество допустими кандидати за решения;
-- целева функция.
На всеки кандидат за решение се съпоставя някакво число - стойност на целевата функция.
Целта е да се намери решение (оптимално решение), за което функцията приема своята екстремална (максимална или минимална) стойност.
Възможно е задачата да има повече от едно оптимални решения (в общия случай прилагането на динамично оптимиране ще ни даде само едно от тях).


** Задача за монети - минимален брой [Coin problem, AL p. 65]

Дадени са монети със стойности c1, c2, ..., ck и целева сума n. Задачата е да се направи тази сума с минимален брой монети.

Пример: Дадени са три вида монети със стойности 1, 3 и 4.
solve(0) = 0
solve(1) = 1
solve(2) = 2
solve(3) = 1
solve(4) = 1
solve(5) = 2
solve(6) = 2
solve(7) = 2
solve(8) = 2
solve(9) = 3
solve(10) = 3

solve: целева сума -> минимален брой монети

Рекурсивна формула за примера:
solve(x) = min{solve(x−1) + 1, solve(x−3) + 1, solve(x−4) + 1} = min{solve(x−1), solve(x−3) , solve(x−4) } + 1

solve(10) =  min{solve(10 - 1), solve(10 - 3), solve(10 - 4)} + 1 = min{solve(9), solve(7), solve(6)} = min{3, 2, 2} + 1 = 2 + 1 = 3
solve(10) = solve(7) + 1 = solve(4) + 2 = solve(0) + 3 = 3.

Обща рекурсивна формула за монети със стойности c1, c2, ..., ck :
solve(0) = 0, solve(x) = min {solve(x - ci) + 1, i = 1, 2,..., k}

int solve(int x)
{
    if (x == 0) return 0;
    int best = INF;
    for (int i = 0; i < k; i++)
        if (x - c[i] >= 0) best = min(best, solve(x - c[i]) + 1);
    return best;
}

Рекурсия със запомняне на пресметнатите стойности (memorization) в масива value:
int solve(int x)
{
    if (x == 0) return 0;
    if (value[x]  >= 0) return value[x];
    int best = INF;
    for (int i = 0; i < k; i++)
        if (x - c[i] >= 0) best = min(best, solve(x - c[i]) + 1);
    value[x] = best;
    return best;
}

Итеративен вариант със запомняне на пресметнатите стойности (memorization) в масива value::
value[0] = 0;
for (int x = 1; x <= n; x++)
{
    value[x] = INF;
    for (int i = 0; i < k; i++)
        if (x - c[i] >= 0) value[x] = min(value[x], value[x - c[i]] + 1);
}

Така получаваме с колко монети може да се направи сумата. С кои монети е решението може да се намери със следната модификация:

int first[N];
value[0] = 0;
for (int x = 1; x <= n; x++)
{
    value[x] = INF;
    for (int i = 0; i < k; i++)
        if (x-c >= 0 && value[x-c]+1 < value[x])
    {
        value[x] = value[x-c[i]]+1;
        first[x] = c[i];
    }
}

while (n > 0)
{

    cout << first[n] << "\n";
    n -= first[n];
}


** Задача за монети - брой разбивания [8.3.4]

Дадени са n типа монети със стойности съответно: c0, c1, ..., cn–1, и естествено число s. Да се намери броят на различните представяния на s с монети измежду наличните типове. Стойностите c0, c1, ..., cn–1 са цели положителни числа. Всеки тип монети може да участва в сумата неограничен брой пъти.

F(s, m) са броя на начините, по които можем да представим сумата s с тези монети, чиято стойност не надвишава m.
F(s, m) = 0, за s = 0;
F(s, m) = F(s, s), за s < m;
F(s, m) = 1 + sum{ F(s - i, i): i = 1, 2, ..., m,  има k: ck = i} за s = m и има k: ck = s;
F(s, m) = sum{ F(s - i, i): i = 1, 2, ..., m,  има k: ck = i} иначе

Пример: Монети 1,2,3,4,6, сума 6
F(s, m)
Сума s = 3 с монети със стойност <= m = 2:
F(3, 2) = F(2, 1) + F(1, 2) = 1 + 1 = 2;
F(3, 3) = 1 + F(2, 1) + F(1, 2)  = 1 + 1 + 1 = 3
Сума 4 с монети със стойност <= 2:
F(4,2) = F(3, 1) + F(2, 2)  = 1 + 2  = 3;
F(4, 3) = F(3, 1) + F(2, 2) + F(1, 3) = 4;
F(4, 4) = 1 + ...

s/m
1
2
3
4
5
6
1
1
1
1
1
1
1
2
1
2
2
2
2
2
3
1
2
3
3
3
3
4
1
3
4



5
1





6
1







#include<iostream>
#define MAXCOINS 100 /* Максимален брой монети */
#define MAXSUM 100   /* Максимална сума */

using namespace std;

unsigned long F[MAXSUM][MAXSUM]; /* Целева функция */
unsigned char exist[MAXSUM];     /* Съществува ли монета с такава стойност */
unsigned coins[MAXCOINS] = {1,2,3,4,6}; /* Налични типове монети */
unsigned sum = 6;               /* Сума, която искаме да получим */
const unsigned n = 5;           /* Общ брой налични монети */

/* Инициализираща функция */
void init(void)
{
    unsigned i, j;
    /* Нулиране на целевата функция */
    for (i = 0; i <= sum; i++)
        for (j = 0; j <= sum; j++) F[i][j] = 0;
  
    /* Друго представяне на стойностите на монетите за по-бърз достъп */
    for (i = 0; i <= sum; i++) exist[i] = 0;
    for (i = 0; i < n; i++) exist[coins[i]] = 1;
}

/* Намира броя на представянията на sum */
unsigned long count(unsigned sum, unsigned max)
{
    unsigned long i;
    if (sum <= 0) return 0;
    if (F[sum][max] > 0) return F[sum][max];
    else
    {
        if (sum < max) max = sum;
        if (sum == max && exist[sum]) /* Има монета с такава стойност */
            F[sum][max] = 1;
   
        for (i = max; i > 0; i--) /* Пресмятаме всички */
            if (exist[i]) F[sum][max] += count(sum - i, i);
    }
    return F[sum][max];
}

int main()
{
    init();
    cout << "Sum " << sum << "   Num " << count(sum, sum) << endl;
    return 0;
}

** Разбиване на естествено число [8.3.6]

Разбиване на естествено число n е всяко мултимножество от естествени числа, не непременно различни, чиято сума е n. Да се намери броя на различните разбивания.

Пример:  n = 4
4
3 + 1
2 + 2
2 + 1 + 1
1 + 1 + 1 + 1

Виж Разбиване на числа [1.3.4]

S(n, m)  e броят на представянията на n като сума от естествени числа, ненадминаващи m.

S(1, n) = S(m, 1) = 1
,
S(n, n) = 1 + S(n, n - 1),
S(n, m) = S(n, n), за n < m,

S(n, m) = S(n, m - 1) + S(n - m, m) за n > m.

S(4, 4) = 1 + S(4, 3);
S(4, 3) = S(4, 2) + S(1, 3) = S(4, 2) + S(1, 1) = S(4, 2) + 1;
S(4, 2) = S(4, 1) + S(2, 2) = 1 + 1 + S(2, 1)  = 3;
 

**Домино подредица [8.4.12, стр. 579]

Домино-редица ще наричаме редица от естествени числа, за която най-старшата цифра на i-тия член съвпада с най-младшата на (i–1)-ия.

Пример: Домино-редица.
34 402 2 29 91 11 1

Задача: По зададена редица X(n):  x1, x2, ..., xn от естествени числа да се намери нейна максимална по дължина домино-подредица.

Пример:
 25 62 36 51 12 6 33 22
[1   2   3   4   5   6  7   8]

Да разгледаме редицата X(k):  xk, xk+1, ..., xn.
С F(i, k) означаваме максималната дължина на домино-редица, подредица на X(k),  и с първа цифра i на първия член на домино-редицата.
i = 0, 1, 2, ...., 9; k = 1, 2, ..., n

F(1, 8) = F(1, 7) = F(1, 6) = 0;  F(1, 5) = F(1, 4) = F(1, 3) = F(1, 2) = F(1, 1) = 2; 12 22
F(2, 8) = F(2, 7) = F(2, 6) =  F(2, 5) = F(2, 4) = F(2, 3) = F(2, 2) = 1, F(2, 1) = 4; 25 51 12 22
...
Решението на задачата ще бъде най-голямото от числата F(1, 1), F(2, 1), ..., F(9, 1).

Означаваме с l(k) първата (left) цифра на числото xk и с r(k) - последната (right) цифра на xk.

Рекурентните равенства са:

1) F(i, k) = F(i, k + 1), ако l(k) != i;
Ако първата цифра l(k) на xk е различна от i, xk не може да влияе на стойността на F(i, k).

2) F(i, k) = max{F(i, k + 1), 1 + F(r(k), k + 1)}, ако  l(k) = i;
Ако i = l(k), са налице две възможности — да игнорираме xk или да го вмъкнем като първи елемент на максималната домино-подредица с първи елемент, започващ с последната цифра на xk - r(k).

F(1, 5) = max{F(1, 6), 1 + F(2, 6)} = 2
F(2, 1) = max{F(2, 2), 1 + F(5, 2)} = 4

** Разстояние на Левенщайн [Edit distance, AL p. 74]

Разстояние на Левенщайн между два низа е минималният брой операции за редактиране, необходими за преобразуване на низ в друг низ. Операциите за редактиране са:

• insert - добавя символ (например ABC -> ABCA)
• remove - изтрива символ (например ABC -> AC)
• modify - замества един символ с друг (например ABC -> ADC)

Например разстоянието между LOVE и MOVIE е 2, защото можем първо да извършим операцията LOVE -> MOVE (modify) и след това операцията MOVE -> MOVIE (insert).
Това е най-малкият възможен брой операции, защото е ясно, че само една операция не е достатъчна.

Нека са дадени низовете s1 и s2.
distance(a, b) е разстоянието между низовете s1.substr(0, a)  и  s2.substr(0, b)

distance(a, b) = min{distance(a, b −1) + 1,
                                 distance(a − 1, b) + 1,
                                 distance(a − 1, b − 1) + cost(a, b)}.
distance(0, b) = b; distance(a, 0)  = a;

cost(a, b) = 1, ако s1[a] != s2[b] и
cost(a, b) = 0, ако s1[a] == s2[b].



L - 1
O - 2
V- 3
E - 4

0
1
2
3
4
M - 1
1
min{2, 2, 1} = 1
2
3
4
O -2
2
2
1
2
3
V - 3
3




I - 4
4




E - 5
5