5 януари 2017 г.

AlgorithmO #3 - Алгоритъм на Крускал (минимално покриващо дърво на граф)

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

---



ОПИСАНИЕ:

Алгоритъмът на Крускал се използва за намиране на минимално покриващо дърво на граф (още наричано "минимално обхващащо дърво" или МОД). МОД представлява дърво, в което са включени всички върхове на графа и от всеки връх има ребро към друг, а теглото на ребрата, които ги свързват, е минимално.

АЛГОРИТЪМ:

Версия 1

1) Сортираме ребрата по реда на нарастване на теглата
2) Разглеждаме всеки връх като дърво от 1 възел
3) Преглеждаме ребрата по сортирания ред
- Ако поредното ребро съединява 2 отделни дървета, то включваме реброто в МОД и обединяваме 2-те дървета в едно.
4) Повтаряме стъпка 3 до получаването на единствено дърво.

Версия 2

(взето от книгата на Преслав Наков, "Програмиране = ++Алгоритми")

1) Създаваме n множества, като в i-тото множество поставяме i-тия връх от графа.
2) Сортираме ребрата на графа във възходящ ред (по теглата им).
3) Създаваме празно дърво T(V, Ø). След приключване на алгоритъма T ще бъде търсеното покриващо дърво.
4) Последователно (n–1 пъти) добавяме ребро (i,j) ∈ Е към T, така че да бъде изпълнено:
- теглото f(i, j) да бъде възможно най-малко (разполагаме със сортиран по теглата списък на ребрата от графа)
- върховете i и j да се намират в различни множества
След всяко добавено ребро (i,j) обединяваме множествата, в които се намират i и j.

ПРИМЕР:

Нека имаме следния граф:


Нека първо ви покажа как ща намерим МОД с първата версия на алгоритъма. 

Така, първото нещо, което трябва да направим, е да си направим списък с ребрата в графа и техните тегла. Нека използваме таблица, за да е по-прегледно:

Ребро
Тегло
1-2
1
1-4
2
4-7
14
4-6
16
7-6
1
4-3
4
2-3
3
2-5
13
3-6
3
6-5
12
6-9
1
5-9
13
5-8
1

Чудесно, сега нека ги сортираме във възходящ ред спрямо теглото (стъпка 1): 

Ребро
Тегло
5-8
1
1-2
1
6-9
1
7-6
1
1-4
2
2-3
3
3-6
3
4-3
4
6-5
12
5-9
13
2-5
13
4-7
14
4-6
16

Следващата стъпка е да създадем 9 отделни дървета от по 1 възел (за всеки възел от графа):


Сега следва забавната част! Разглеждаме всяко едно ребро по сортирания ред.

Първо 5-8. 5 и 8 са в отделни дървета, следователно това ребро ще бъде част от МОД!

5-8 ✅


Продължаваме по същия начин със следващите ребра.

1-2 ✅
6-9 ✅
7-6 ✅
1-4 ✅
2-3 ✅
3-6 ✅
4-3 ❌

Опа, 4-3 няма да е част от МОД. Защо? Нека да видим как изглежда МОД преди да стигнем до реброто 4-3:


4-3 няма да е част от МОД, тъй като не свързва 2 отделни дървета (както се вижда, 4 има връзка с 1, а пък 1 с 2 и накрая 2 с 3. Следователно 3 и 4 вече са в едно и също дърво.

Добре, нека да продължим напред!

6-5 дали става? Ами, 5 има връзка само с 8, следователно 6 се намира в друго дърво. Значи 6-5 е валидно ребро.

6-5 ✅


Следва 5-9. 5 има връзка с 6, а пък 6 с 9, така че двата възела са част от едно дърво и няма да добавяме реброто към МОД.

5-9 ❌

Аналогично за 2-5 (2 има връзка с 3, а 3 с 6 и 6 с 5... да, става малко объркващо, но двата възела са част от едно и също дърво).

2-5 ❌

Продължаваме с 4-7. Тук връзката е дълга и широка (4-1 => 1-2 => 2-3 => 3-6 => 6-7), но резултатът е пак същият. Това ребро отново няма да е част от МОД.

4-7 ❌

Остана и последното ребро, 4-6. Двата възела се намират в едно и също дърво (4-1 => 1-2 => 2-3 => 3-6). Не го добавяме към МОД.

И така, в крайна сметка, горното дърво се оказва МОД на графа:


Ребрата му са следните: (5-8), (1-2), (6-9), (7-6), (1-4), (2-3), (3-6), (6-5)
Теглата са същите като в таблицата горе.

---

Добре, нека решим същата задача по другия начин.

Алгоритъмът ни казва, че трябва да направим "n" множества, т.е колкото са възлите в графа. В случая са 9, затова правим 9 множества (с по 1 елемент - стойността във възела):

{ 1 }
{ 2 }
{ 3 }
{ 4 }
{ 5 }
{ 6 }
{ 7 }
{ 8 }
{ 9 }

Тъй като ще ни трябва, ето я таблицата със сортираните ребра... once again: 

Ребро
Тегло
5-8
1
1-2
1
6-9
1
7-6
1
1-4
2
2-3
3
3-6
3
4-3
4
6-5
12
5-9
13
2-5
13
4-7
14
4-6
16
Започваме с първото ребро, 5-8. Двата върха не са в едно множество, следователно това ребро ще е част от МОД (5-8 ✅). Съединяваме множествата { 5 } и { 8 }: 

{ 1 }
{ 2 }
{ 3 }
{ 4 }
{ 5, 8 }
{ 6 }
{ 7 }
{ 9 }

Продължаваме с 1-2. Абсолютно същото като предишния случай (1-2 ✅). Съединяваме множествата { 1 } и { 2 }:
{ 1, 2 }
{ 3 }
{ 4 }
{ 5, 8 }
{ 6 }
{ 7 }
{ 9 }

Схванахте идеята, но все пак... 6-9? Да, става (6-9 ✅): 

{ 1, 2 }
{ 3 }
{ 4 }
{ 5, 8 }
{ 6, 9 }
{ 7 }

Ами 7-6? Да, отново са в различни множества, така че добавяме това ребро към МОД (7-6 ✅):

{ 1, 2 }
{ 3 }
{ 4 }
{ 5, 8 }
{ 6, 9, 7 }

1-4? Валидно (1-4 ✅)! Добавяме към МОД:

{ 1, 2, 4 }
{ 3 }
{ 5, 8 }
{ 6, 9, 7 }

Аналогично за 2-3 (2-3 ✅):

{ 1, 2, 4, 3 }
{ 5, 8 }
{ 6, 9, 7 }

3-6? Отново към МОД (3-6 ✅). Спомнете си, че съединяваме множествата (а не просто добавяме единия елемент към множеството), така че сега ще имаме едно дълго множество: 

{ 1, 2, 4, 3, 6, 9, 7 }
{ 5, 8 }

И така, стигнахме до по-интересната част. 4-3 не става (4-3 ❌), защото 4 и 3 са в едно множество. Но това не важи за 6-5 (6-5 ✅), защото 6 и 5 са в различни множества. Съединяваме двете множества:

{ 1, 2, 4, 3, 6, 9, 7, 5, 8 }

Така, получихме едно дълго множество, което съдържа всички възли. Това означава, че сме приключили с намирането на ребрата, които ще са част от МОД. 5-9, 2-5, 4-7 и 4-6 няма да са част от него (5-9 ❌, 2-5 ❌, 4-7 ❌, 4-6 ❌).

В крайна сметка, МОД ще включва следните ребра:

(5-8), (1-2), (6-9), (7-6), (1-4), (2-3), (3-6), (6-5)

Резултатът е абсолютно същият. Въпрос на избор е коя "версия" на алгоритъма ще ползвате!

---

Това е за днес. Ако видите някоя неточност, моля кажете ми (аз също все още се уча)! Peeeeace.

3 януари 2017 г.

AlgorithmO #2 - Код на Хъфман за компресиране на данни

Този алгоритъм ми харесва много, защото наистина виждам приложението му. Спомням си, че едно време се чудех как WinRAR магически смалява файлове (при това даже не нахалства и ти позволява да ползваш софтуера безкрайно, макар и да е изтекъл trial периодът 😅). Е, този алгоритъм е един от широко използваните при компресирането на данни.

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

---



ОПИСАНИЕ:

Алгоритъмът на Хъфман служи за компресиране на информация и е добър пример за алчен алгоритъм (тъй като винаги взема "най-доброто"). Идеята зад него е, че ако имаме някакво съобщение, можем да го преобразуваме в код от 0-ли и 1-ци.

За целта се използва т.нар. "дърво на Хъфман", от което се извежда и "код на Хъфман".

Ако се чудите "какво по дяволите е дърво" - ето тук има отлично обяснение на нещата, които ще трябва да знаете предварително. :)

АЛГОРИТЪМ:

1. Създават се дървета от по един възел, съответстващи на буквите от азбуката в съобщението.
2. Записват се честотите на срещане на всяка буква в корените на тези дървета
3. Избират се две дървета с минимално тегло и се обединяват в едно, като неговото тегло се получава от сумата на теглата на двете поддървета.
4. Горното действие се повтаря до получаване на единствено дърво.


ПРИМЕР: 

Съобщение: baabbcdddebbaccbdbda

Първото, което трябва да направим, е да видим кои са буквите, които участват в съобщението и колко пъти се среща всяка от тях. В случая имаме участието на само 5 букви - a, b, c, d, e.

Ето и колко пъти се среща всяка от тях:

а - 4 срещания
b - 7 срещания
c - 3 срещания
d - 5 срещания
e - 1 срещане

Следващата стъпка е да създадем 5 дървета с по 1 възел (или "връх"). Тъй като имаме само 1 възел, той представлява корен на съответното дърво.


Чудесно! Сега трябва да открием 2-те дървета с най-малко тегло (най-малка стойност) в корена. Лесно виждаме, че тези стойности са 3 и 1. Обединяваме ги в ново двоично подредено дърво. Това ново дърво ще има корен с тегло 4 (сумата от теглата на предишните 2 тегла), и наследници с тегла 3 и 1. 

Тъй като дървото е подредено, това означава че вляво добавяме по-малката стойност, а вдясно по-голямата (не правете същата грешка като мен - бях ги обърнал и резултатът не беше красив 👲).

След всичко това стигаме до следната ситуация:


Повтаряме същото! Този път най-малките стойности са 4 и 4 (не, няма значение, че стойностите са еднакви). Следваме същия принцип и получаваме следното:


Схванахте вече. Но нека отново ви кажа какво следва... 7 и 5! Ето и резултатът:


Хайде още 1 път... Разбира се, стойностите са 8 и 12, тъй като други няма. Резултатът:


Стигнахме до забавната част - вече имаме само 1 дърво. Сега следва да номерираме всяко ляво ребро с 0, и всяко дясно ребро с 1 (ребрата са стрелките), подобно на алгоритъма на Шенън-Фано.

Най-накрая получаваме дървото на Хъфман:


И сега... как да получим кода? Много просто - следваме пътя до всяка буква. Как да стигнем до буквата 'b' например? Започваме от корена 20 и преминаваме към 12 (реброто между двата възела е с тегло 1), от 12 отиваме към 7 (реброто между двата възела е с тегло 1).

В случая ни интересува последователността от теглата на ребрата до възела на дадената буква. Иначе казано, понеже първото ребро има тегло 1 и второто също има тегло 1, кодът на буквата 'b' е 11.

Аналогично намираме кода на всяка от останалите букви и получаваме следната таблица:

a
00
b
11
c
011
d
10
e
010

Така... какво беше съобщението? А, да:

baabbcdddebbaccbdbda

Сега просто заменяме всяка буква в съобщението с нейния код.

Получаваме кода на Хъфман:

11 | 00 | 00 | 11 | 11 | 011 | 10 | 10 | 10 | 010 | 11 | 11 | 00 | 011 | 011 | 11 | 10 | 11 | 10 | 00
 b     a     a     b     b     c      d     d     d     e      b     b     a      c      c      b     d     b     d    a

=> 11000011110111010100101111000110111110111000

Вече сте богове на компресирането. 😅

22 декември 2016 г.

"Слушайте тялото си" - най-голямата пренебрегната ИСТИНА

Йо! Болен съм. При това за 3-ти път през последните 2 месеца.

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

Може и да греша, но аз по-скоро го наричам... глупост. Глупост от моя страна.



Това, в което се убеждавам все повече и повече, с всеки изминал ден, е, че тялото ни винаги ни дава някаква индикация, че "болестта наближава".

Интересното е, че в миналото никога не съм се замислял дълбоко за последствията от малките ми действия. Например ще изляза навън без шапка, а навън ще е много студено. Или пък ще стоя навън малко по-дълго от необходимото при някакъв нечовешки студ. На момента няма да почувствам никакъв негативен ефект, но след 1-2 дена ще съм болен.

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

Липсва ни една важна част от пъзела.

Разбира се, че трябва да се лекуваме ако сме болни, но липсва един ценен компонент - не се замисляме какво точно ни е довело до текущото ни състояние.

Повечето хора се опитват да премахнат симптомите, а не това, което предизвиква болестта.

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

Сега стоя тук, отново болен, но някакси изплуват различни неприятни образи в главата ми. "Аа, пич, спомняш ли си, че вчера си махна шапката, защото ти се стори топло, ама всъщност не беше", "аа, пич, спомняш ли си, че онзи ден като тренира се беше изпотил много и беше студено, но ти игнорира този факт, защото искаше да направиш нов клип".

Не е приятно. Много по-лесно е да кажа "еми, такъв е сезонът, има много болни, вината не е моя". Но според мен далеч не е така.

Можем да предотвратим голяма част от лошите неща, които ни се случват, но просто избираме по-лесния път. Такава е природата ни.

Слушайте тялото си. Може да ви спести АДСКИ много главоболия.


Смея да твърдя, че имам доста активно ежедневие. В миналото постоянно се чудех как да запълня времето си и мразех почивките, защото това предизвикваше хаос в главата ми и се чувствах неприятно от цялото това бездействие. Сега нямам този проблем - не съм чувствал "скука" от доста дълго време.

Но за сметка на това - сега не се спирам. Опитвам се да запълня почти всяка една минута с нещо, което ме доближава до реализирането на целите ми. Гледам другите и си мисля "ха ха, виж го как бездейства, не като МЕН", но виждам само едната страна на монетата.

Макар да се гордея с това, че действам и подобрявам живота си, пак допускам една фатална грешка - пренебрегвам нуждата за почивка на тялото ми. И затова страдам.

Не правете тази грешка. Ключът е в баланса - действие + почивка. Това е просто естественият ред на нещата. Има слънчеви дни, но има и дъждовни дни. Здрави сме, но един ден се разболяваме. Раждаме се и умираме (да вкарам малко оптимизъм в цялата работа, а? :)).

А сега сериозно... нека повторя посланието.

Слушайте тялото си. Може да ви спести АДСКИ много главоболия.

Peace.

2 декември 2016 г.

AlgorithmO #1 - Алгоритъм на Евклид (с изваждане)

Доста се дразня когато видя хаотични и неясни обяснения на нова важна информация. Това, което научих, е, че ако даден материал не е обяснен добре още в началото, има голям шанс изцяло да загубим интерес към дадената област.

В момента изучавам алгоритми и отново се сблъсквам с гореописания проблем. Затова ще се опитам с тази поредица от blog постове да предоставя максимално кратки и ясни обяснения на популярни алгоритми, придружени с примери, за да може всеки да разбере как точно работят и какво е приложението им.

Един трик ако искате да затвърдите знанията си в каквото и да било (или да осъзнаете къде са пропуските ви) - опитайте се да НАУЧИТЕ ДРУГ на това, което знаете.

"Ако не можеш да обясниш нещо достатъчно просто, значи не го разбираш достатъчно добре." - Алберт Айнщайн

---

Евклид след тежка вечер. :)


ОПИСАНИЕ:

Алгоритъмът на Евклид се използва за намиране на най-голям общ делител (НОД) на 2 числа. НОД представлява най-голямото число, на което и 2-те числа се делят без остатък.

Този алгоритъм се счита за един от най-старите и често се използва за опростяване на дроби или за намиране на част от решението при по-комплексни задачи.

АЛГОРИТЪМ:

НОД(A, B) = ?

1. Въведи А и B
2. Ако А != B, към стъпка 3.
    Иначе към стъпка 5.
3. Ако А > B, пресметни A = A - B.
    Иначе пресметни B = B - A
4. Kъм стъпка 2
5. Изведи А
6. Край

ПРИМЕР:

НОД(2505, 9775) = ?

A
B
2505
9775
2505
9775 - 2505 = 7270
2505
7270 - 2505 = 4765
2505
4765 - 2505 = 2260
2505 - 2260 = 245
2260
245
2260 - 245 = 2015
245
2015 - 245 = 1770
245
1770 - 245 = 1525
245
1525 - 245 = 1280
245
1280 - 245 = 1035
245
1035 - 245 = 790
245
790 - 245 = 545
245
545 - 245 = 300
245
300 - 245 = 55
245 - 55 = 190
55
190 - 55 = 135
55
135 - 55 = 80
55
80 - 55 = 25
55
25
55 - 25 = 30
25
30 - 25 = 5
25 - 5 = 20
5
20 - 5 = 15
5
15 - 5 = 10
5
10 - 5 = 5
5
5
5

=> НОД(2505, 9775) = 5

---

НОД(10127, 8323) = ?

A
B
10127
8323
10127 - 8323 = 1804
8323
1804
8323 - 1804 = 6519
1804
6519 - 1804 = 4715
1804
4715 - 1804 = 2911
1804
2911 - 1804 = 1107
1804 - 1107 = 697
1107
697
1107 - 697 = 410
697 - 410 = 287
410
287
410 - 287 = 123
287 - 123 = 164
123
164 - 123 = 41
123
41
123 - 41 = 82
41
82 - 41 = 41
41
41

=> НОД(10127, 8323) = 41

ИМПЛЕМЕНТАЦИЯ (Java):

26 ноември 2016 г.

CODE TIPS #5 - Намалете обхвата и живота на променливите си

Доста интересна идея, за която не се бях замислял дълбоко, докато не прочетох за нея във фантастичната книга Code Complete на Steve McConnell.




Обхватът на една променлива, най-просто казано, е къде в програмата тя може да бъде използвана.

Ето един прост пример на C++:

Тук имаме 2 променливи с еднакви имена, но първата myVar съществува само във функцията firstFunction(), а втората само във функцията anotherFunction().

Следователно ако извикаме anotherFunction(), ще видим стойността 5 на екрана.

---

Друго интересно понятие е "живот на променливата" или по-точно броят редове между декларирането на променливата и последното й използване.

Например:

Тук имаме 3 променливи. Животът на променливата a е 3, на b е 3, а на c е 0.

---

Има няколко причини да се целим към минимизиране на обхвата и живота на променливите, които декларираме:

1. По-лесно откриване на грешки.
Става значително по-лесно да следим стойността на дадена променлива, когато тя е декларирана максимално близо до мястото в програмата, в което се използва. Не се налага да се съсредоточаваме върху голяма порция код, а само върху няколко реда и това значително улеснява откриването и коригирането на грешки (а може би дори и предотвратяването им).

2. По-четим код
Отново е свързано с факта, че е нужно да се фокусираме върху по-малко количество информация и следователно кодът става значително по-четим.

3. По-лесно разделяне на сложни функции на по-прости
Много по-лесно е да отделим дадена част от кода в отделна функция ако декларациите се намират до редовете, в които се използват променливите. Рефакторирането (преработването) на кода се улеснява значително.

---

Направи ми впечатление, че е често срещана практиката при правене на прости програми да се следва структурата: деклариране на променливи, а след това следва главната функционалност на програмата и изход от нея.

Това не е чак такъв проблем при малки програми, но при по-сложни проекти е добра практика да се минимизра обхвата и живота на променливите, които използваме.

Това също е основна причина да се избягва използването на глобални променливи - трудно се следи стойността им и това води до трудни за откриване грешки.

Просто една интересна идея, която ми направи впечатление... Препоръчвам да разгледате книгата Code Complete - изключително много полезна информация за всеки сериозен програмист.

Over and out.