11. Разделяй и владей

План:
Задачи 11 и 12
Метод на разклоненията и границите
Подрязване на търсенето
Пълно изчерпване
Разделяй и владей
Мажорант
Бързо умножение на дълги числа
Циклично преместване на елементите на масив
Задачи 13 и 14

Метод на разклоненията и границите [6.4]

* При метода търсене с връщане процесът на решаване на задачата може да се представи като дърво на търсенето.

* Движението по дървото е от корена към листата (търсене) и когато се достигне листо (няма възможен ход и не е достигнато решението), движението се обръща от листото към корена (връщане).

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

Подрязване на търсенето [AL p. 51]

Даден е квадрат, състоящ се от 7x7 квадрати. Да се пресметне по колко начина може да се стигне от горния ляв квадрат до долния десен квадрат с хоризонтално и вертикално движение.

Пълно изчерпване (Brute force search)

Намиране на всички подмножества на едно множество.

{A, B, C}, 2^3 = 8
{}, {A}, {B}, {C}, {A,B}, {A,C}, {B,C}, {A,B,C}

Дадено е множество от цели числа. Да се намерят сумите от елементите на всички подмножества.

subsets.cpp.


Разделяй и владей [7]

* Принципи на "разделяй и владей": разбиване на изходната задача на няколко подзадачи (разделяй), решаване на подзадачите и конструиране на решение на изходната задача (владей).

* Двоично търсене.

* Бързо сортиране с разделяне на дялове (Хоор, "разделяй и владей") - O(n2) в най-лошия случай, O(n) в най-добрия случай, O(n log n) средно. Сложността зависи от избора на елемент за разделяне на дялове.

* Сортиране чрез сливане


Мажорант [7.2]

Нека е дадено n-елементно мултимножество (множество, в което се допуска повторение на елементи). Ще казваме, че даден елемент на множеството е негов мажорант, ако се среща строго повече от n/2 пъти.

Решение с двоен цикъл: броене колко пъти се среща всеки елемент - O(n2).

Решение със сортиране при линейна наредба на елементите му: сортираме и проверяваме средния елемент за мажорант - време O(n log n) или по-добро, ако сортираме с по-бърз алгоритъм.

Решение с броене: O(n + k), ако в масива има k различни стойности.

Твърдение. Ако масивът има мажорант и премахнем два различни елемента, мажорантът на новия масив ще съвпада с този на изходния.

Решение с кандидат и брояч:
- Ако броячът е 0, кандидат става текущият елемент, а броячът става 1.
- Ако броячът е различен от 0:
  -- Ако кандидатът съвпада с текущия елемент, броячът се увеличава с 1.
  -- Ако са различни, броячът се намалява с 1.
Ако брoячът е 0 - няма мажорант, иначе проверяваме дали кандидатът е мажорант.
Пример
:
C е мажорант
A A A C C B B C C C B C C

А(А,1), А(А,2), А(А,3), C(А,2), C(А,1), B(?,0), B(B,1), C(?,0), C(C,1), C(C,2), B(C,1), C(C,2), C(C,3)

C не е мажорант
A A A A C B B C C C B C C

А(А,1), А(А,2), А(А,3), A(A,4), C(A,3), C(A,2), B(A,1), B(?,0), C(C,1), C(C,2) C(C,3), B(C,2), C(C,3),  C(C,4) 


Бързо умножение на дълги числа [7.7] 

* Класическият алгоритъм за умножение на цели числа има сложност O(n2).

* Алгоритъм за умножение на две n-цифрени числа X и Y, като n е степен на 2.

X = 2n/2 A + B,    Y = 2n/2 C + D
XY = 2n AC + 2n/2 [(A - B)(D - C) + AC + BD] + BD
Числата A, B, C и D са n/2 цифрени числа (разделяй) - може да се използва рекурсия, като тривиалният случай е (директно) умножение на едноцифрени числа.

* Тази формула изисква 3 умножения, 6 събирания и 2 премествания вляво (умножения на степени на 2). От рекурентната формула

T(1) = 1,  T(n) = 3T(n/2) + cn, n > 0
получаваме, че сложността на алгоритъма е O(nr), r = log23. Тъй като log23 < 2, то този алгоритъм е по-добър от класическия.

* Пример 1: 15 x 8 = 120,  X = 15 = 11112 Y = 8 = 10002, n = 4 = 22
 A = 11, B = 11, C = 10, D = 00
 AC = 110,  (A - B)(D - C) = 0.10 = 0, BD = 0
 XY = 110 << 4 + 110 << 2 = 1100000 + 11000 = 11110002 = 8+16+32+64 = 12010

* Когато числата са с различен брой цифри и/или този брой не е степен на 2, допълваме отляво с незначещи нули и използваме горния алгоритъм.

* Пример 2. 50 x 25 = 1250, X = 50 = 1100102 Y = 25 = 110012, n = 8 = 24 с допълване до най-близката степен на 2.
 A = 0011 (3), B = 0010 (2), C = 0001 (1), D = 1001 (9)
 AC = 0011 (3),  (A - B)(D - C) = 0001.1000 = 1000 (8), BD = 10010 (18)
 XY = 3.256 + (8 + 3 + 18).16 + 18 =
          11 << 8 + 11101 << 4 = 11 0000 0000 + 1 1101 0000 + 10010 = 100111000102 =
          2+32+64+128+1024 = 125010
* Подзадачи:
  - представяне на дълги числа (низ или вътрешно);
  - брой на битовете на цяло число;
  - разделяне на десните от левите битове на числото;
  - сума и разлика на две числа.


Циклично преместване на елементите на масив [7.10]

* Пример за циклично преместване на k позиции елементите на масив m от n елемента.
n = 6, k = 2   0 1 2 3 4 5   ->  2 3 4 5 0 1

* Алгоритъм 1:
- копираме първите k елемента на m в друг масив x;
- преместваме n-k елемента на m на k позиции вляво;
- копираме елементите на x обратно в m на последните k позиции.
Сложност O(n), допълнителна памет k елемента.

* Алгоритъм 2:
Описания алгоритъм за k = 1. За да осъществим циклично преместване с k елемента, е необходимо да приложим този алгоритъм  k пъти.
Сложност O(n2), допълнителна памет 1 елемент.

* Алгоритъм 3:
- копираме m[0] в x;
- преместваме m[k] в m[0], преместваме m[2k%n] в m[k], преместваме m[3k%n] в m[2k%n] и т.н. докато стигнем последния непреместен елемент, който го заменяме с x (Пример А). Ако достигнем до преместен елемент, спираме и започваме отначало по същия начин, като заменяме k с k+1 (Пример Б).
 

Пример А    n = 11, k = 3: 
0 1 2 3 4 5 6 7 8 9 10  ->  3 4 5 6 7 8 9 10 0 1 2
m01: 0 1 2 3 4 5 6 7 8 9 10 | x:
m02: 0 1 2 3 4 5 6 7 8 9 10 | x: 0
m03: 3 1 2 3 4 5 6 7 8 9 10 | x: 0; 
m04: 3 1 2 6 4 5 6 7 8 9 10 | x: 0; 
m05: 3 1 2 6 4 5 9 7 8 9 10 | x: 0; 
m06: 3 1 2 6 4 5 9 7 8 1 10 | x: 0; 
m07: 3 4 2 6 4 5 9 7 8 1 10 | x: 0; 
m08: 3 4 2 6 7 5 9 7 8 1 10 | x: 0; 
m09: 3 4 2 6 7 5 9 10 8 1 10 | x: 0; 
m10: 3 4 2 6 7 5 9 10 8 1 2 | x: 0; 
m11: 3 4 5 6 7 5 9 10 8 1 2 | x: 0; 
m12: 3 4 5 6 7 8 9 10 8 1 2 | x: 0
m13: 3 4 5 6 7 8 9 10 0 1 2 | x: 0; 
Пример Б   n = 9, k = 3: 
0 1 2 3 4 5 6 7 8  ->  3 4 5 6 7 8 0 1 2
m01: 0 1 2 3 4 5 6 7 8 | x:
m02: 3 1 2 3 4 5 6 7 8 | x: 0
m03: 3 1 2 6 4 5 6 7 8 | x: 0
m04: 3 1 2 6 4 5 0 7 8 | x: 0
m05: 3 1 2 6 4 5 0 7 8 | x: 1; 
m06: 3 4 2 6 4 5 0 7 8 | x: 1; 
m07: 3 4 2 6 7 5 0 7 8 | x: 1
m08: 3 4 2 6 7 5 0 1 8 | x: 1
m09: 3 4 2 6 7 5 0 1 8 | x: 2; 
m10: 3 4 5 6 7 5 0 1 8 | x: 2; 
m11: 3 4 5 6 7 8 0 1 8 | x: 2
m12: 3 4 5 6 7 8 0 1 2 | x: 2

Сложност O(n), допълнителна памет 1 елемент.

* Алгоритъм 4 (разделяй и владей):
- разделяме масива на 2 части: A - първите k елемента и B - последните n - k елемента;
- разделяме масива B на 2 части, Br - последните k елемента, Bl - останалите елементи;
- разменяме местата на елементите на A и Br;
- прилагаме същия алгоритъм за масива B.

Пример.
n = 11, k = 3:
0 1 2 3 4 5 6 7 8 9 10  ->  3 4 5 6 7 8 9 10 0 1 2
[0 1 2 3 4 5 6 7 8 9 10]
A = [0 1 2], Bl = [3 4 5 6 7], Br = [8 9 10]
[8 9 10 3 4 5 6 7 0 1 2 ]

[8 9 10 3 4 5 6 7 ]  [0 1 2]
A = [8 9 10], Bl =[3 4], Br = [5 6 7]
[5 6 7 3 4 8 9 10]

[5 6 7 3 4]  [8 9 10 0 1 2]
A = [5 6 7], Bl = [], Br = [3 4 5]
[ 3 4 5 6 7]

[3 4 5 6 7 8 9 10 0 1 2]

Сложност O(n), допълнителна памет - константа.

* Алгоритъм 5 (Кърниган и Плоджер):   BA = (AR BR)R

AR е масив, получен от елементите на A в обратен ред. С 3 извиквания на функция reverse решаваме задачата за циклично преместване на елементите на масив.

Пример.
n = 11, k = 3:
0 1 2 3 4 5 6 7 8 9 10  ->  3 4 5 6 7 8 9 10 0 1 2

A = [0 1 2], B = [3 4 5 6 7 8 9 10], AB = [0 1 2 3 4 5 6 7 8 9 10]
AR = [2 1 0],  BR = [10 9 8 7 6 5 4 3],  AR BR = [2 1 0 10 9 8 7 6 5 4 3]
(AR BR)R = [3 4 5 6 7 8 9 10 0 1 2]

// shift3.c
#include <stdio.h>
#define N 11

int m[N];

void reverse(unsigned a, unsigned b)
{ unsigned i, j, k, c;
  int tmp;
  for (c=(b-a)/2, k=a, j=b, i=0; i<c; i++, j--, k++)
  { tmp = m[k];
    m[k] = m[j];
    m[j] = tmp;
  }
}

void shift3(unsigned k)
{ reverse(0, k-1);
  reverse(k, N-1);
  reverse(0, N-1);
}

int main()
{
 int i;
 for (i=0; i<N; i++) m[i]=i;
 shift3(3);
 for (i=0; i<N; i++) printf("%d ", m[i]);
 printf("\n");
 return 0;
}
Сложност O(n), допълнителна памет - константа.