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)
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
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;
 

Задачи за домашно 1 и 2