├── Sem_01 └── Notes_01.pdf ├── Sem_02 ├── Notes_02.pdf └── Temporal_complexity_tasks │ ├── Task02.cpp │ ├── Task03.cpp │ └── Task01.cpp ├── Sem_03 ├── Notes_03.pdf └── Correctness │ ├── Task04.cpp │ └── Task01.cpp ├── Sem_04 └── Notes_04.pdf ├── Sem_05 ├── Notes_05.pdf ├── Kadane.cpp ├── Task02.cpp ├── Task03.cpp └── Pick.hpp ├── Advanced-Intel_intrinsics _ BS_optimization ├── Tests.png ├── README.md └── Intel_intrinsics-Binary_search.cpp ├── Sem_08 ├── Task05.js ├── Task02.cpp ├── Task04.cpp ├── Task03.cpp ├── Task02_recoverSolution.cpp ├── Fibonacci.cpp └── README.md ├── Sem_10 ├── GraphAlgorithms │ ├── UnionFind │ │ ├── UnionFind.h │ │ └── UnionFind.cpp │ └── GraphAlgorithms_Pt2.cpp ├── Task03.cpp └── README.md ├── Sem_07 ├── Task01_not-in-place.cpp ├── Task01_in-place.cpp ├── Task03.cpp ├── Task02.cpp ├── Task_Midterm.cpp └── README.md ├── Sem_06 ├── Task03.cpp ├── Task04.cpp ├── Task05.cpp ├── Task01.cpp ├── Task02.cpp └── README.md ├── Sem_11 ├── Task02.cpp └── README.md ├── Advanced-Timer_Scheduler ├── InvokableTask.hpp ├── TaskPool.hpp ├── Source.cpp ├── SchedulableTask.hpp └── TimerScheduler.hpp ├── README.md ├── Sem_09 ├── GraphAlgorithms_Pt1.cpp └── README.md ├── Sem_12 └── README.md ├── Sem_13 └── README.md └── Advanced-Memory_Allocator └── МemoryАllocator.cpp /Sem_01/Notes_01.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MariaGrozdeva/Design_and_analysis_of_algorithms_FMI_2021-2022/HEAD/Sem_01/Notes_01.pdf -------------------------------------------------------------------------------- /Sem_02/Notes_02.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MariaGrozdeva/Design_and_analysis_of_algorithms_FMI_2021-2022/HEAD/Sem_02/Notes_02.pdf -------------------------------------------------------------------------------- /Sem_03/Notes_03.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MariaGrozdeva/Design_and_analysis_of_algorithms_FMI_2021-2022/HEAD/Sem_03/Notes_03.pdf -------------------------------------------------------------------------------- /Sem_04/Notes_04.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MariaGrozdeva/Design_and_analysis_of_algorithms_FMI_2021-2022/HEAD/Sem_04/Notes_04.pdf -------------------------------------------------------------------------------- /Sem_05/Notes_05.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MariaGrozdeva/Design_and_analysis_of_algorithms_FMI_2021-2022/HEAD/Sem_05/Notes_05.pdf -------------------------------------------------------------------------------- /Advanced-Intel_intrinsics _ BS_optimization/Tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MariaGrozdeva/Design_and_analysis_of_algorithms_FMI_2021-2022/HEAD/Advanced-Intel_intrinsics _ BS_optimization/Tests.png -------------------------------------------------------------------------------- /Sem_08/Task05.js: -------------------------------------------------------------------------------- 1 | function coinChange(denominations, value) { 2 | const ways = new Array(value + 1).fill(0); 3 | ways[0] = 1; 4 | 5 | for (const coin of denominations) { 6 | for (let i = coin; i <= value; i++) { 7 | ways[i] += ways[i - coin]; 8 | } 9 | } 10 | 11 | return ways[value]; 12 | } 13 | -------------------------------------------------------------------------------- /Sem_02/Temporal_complexity_tasks/Task02.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int f2(int n) 4 | { 5 | int res = 0; 6 | for (size_t i = 1; i <= n; i *= 2) 7 | { 8 | for (size_t j = 1; j <= i; j++) 9 | res++; 10 | } 11 | 12 | return res; 13 | } 14 | 15 | int main() 16 | { 17 | std::cout << f2(29); 18 | } -------------------------------------------------------------------------------- /Sem_03/Correctness/Task04.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | bool isPrime(unsigned n) 4 | { 5 | if (n <= 1) 6 | return false; 7 | 8 | unsigned temp = sqrt(n); 9 | for (size_t i = 2; i <= temp; i++) 10 | { 11 | if (n % i == 0) 12 | return false; 13 | } 14 | return true; 15 | } 16 | 17 | int main() 18 | { 19 | std::cout << isPrime(37); 20 | } -------------------------------------------------------------------------------- /Sem_10/GraphAlgorithms/UnionFind/UnionFind.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | class UnionFind 5 | { 6 | private: 7 | std::vector parents; 8 | std::vector sizes; 9 | 10 | public: 11 | UnionFind(size_t n); // set of n elements (0..n-1) 12 | 13 | bool Union(size_t n, size_t k); // O(log(n)) 14 | size_t Find(size_t n); // O(log(n)) 15 | }; -------------------------------------------------------------------------------- /Sem_02/Temporal_complexity_tasks/Task03.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int f(int n) 4 | { 5 | int curr = 0; 6 | for (size_t i = 0; i < n; i++) 7 | curr += 1; 8 | 9 | return curr % 2; 10 | } 11 | 12 | int f3(int n) 13 | { 14 | int res = 1; 15 | for (size_t i = n; i >= 1; i /= 2) 16 | res = f(res); 17 | 18 | return res; 19 | } 20 | 21 | int main() 22 | { 23 | std::cout << f3(30); 24 | } -------------------------------------------------------------------------------- /Sem_03/Correctness/Task01.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | size_t mystery(size_t x, size_t y) 4 | { 5 | size_t z = x; 6 | size_t t = y; 7 | size_t p = 1; 8 | 9 | while (t > 0) 10 | { 11 | if (t % 2 == 0) 12 | { 13 | z = z * z; 14 | t = t / 2; 15 | } 16 | else 17 | { 18 | p = p * z; 19 | t = t - 1; 20 | } 21 | } 22 | return p; 23 | } 24 | 25 | int main() 26 | { 27 | std::cout << mystery(6, 9); 28 | } -------------------------------------------------------------------------------- /Sem_02/Temporal_complexity_tasks/Task01.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int f1(int n) 4 | { 5 | int res = 1; 6 | for (size_t i = 1; i <= n; i++) 7 | { 8 | for (size_t j = 1; j <= i * i; j++) 9 | res++; 10 | } 11 | 12 | for (size_t i = 0; i < 1000000; i++) 13 | { 14 | if (res % 3 == 0) 15 | res /= 3; 16 | else 17 | res *= 3; 18 | } 19 | 20 | return res; 21 | } 22 | 23 | int main() 24 | { 25 | std::cout << f1(22); 26 | } -------------------------------------------------------------------------------- /Sem_05/Kadane.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | size_t kadane(const std::vector& arr) // Maximum subarray problem 5 | { 6 | int m = 0; // Максималната сума на подмасив, завършващ на arr[0], arr[1],.., arr[i-1]. 7 | int l = 0; // Максималната сума на подмасив, завършващ на arr[i-1]. 8 | 9 | for (size_t i = 0; i < arr.size(); i++) 10 | { 11 | l += arr[i]; 12 | 13 | if (l < 0) 14 | l = 0; 15 | 16 | if (l > m) 17 | m = l; 18 | } 19 | return m; 20 | } 21 | int main() 22 | { 23 | std::vector v = { -5,-24,22,-3,23 }; 24 | 25 | std::cout << kadane(v); 26 | } -------------------------------------------------------------------------------- /Sem_07/Task01_not-in-place.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | size_t getFirstMissing(const std::vector& v) 5 | { 6 | std::vector occurrences(v.size(), false); 7 | 8 | for (size_t i = 0; i < v.size(); i++) 9 | if (v[i] <= v.size()) 10 | occurrences[v[i] - 1] = true; 11 | 12 | for (size_t i = 0; i < v.size(); i++) 13 | if (!occurrences[i]) 14 | return i + 1; 15 | 16 | return v.size() + 1; 17 | } 18 | 19 | int main() 20 | { 21 | std::vector nums = { 7, 1, 3, 5, 4, 6, 2, 8, 100000 }; 22 | std::cout << "The first missing number is: " << getFirstMissing(nums) << std::endl; 23 | } -------------------------------------------------------------------------------- /Sem_07/Task01_in-place.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | size_t getFirstMissing(std::vector& v) // this function CHANGES the input array 6 | { 7 | for (size_t i = 0; i < v.size(); i++) 8 | if (abs(v[i]) <= v.size()) 9 | v[abs(v[i]) - 1] = -abs(v[abs(v[i]) - 1]); 10 | 11 | for (size_t i = 0; i < v.size(); i++) 12 | if (v[i] > 0) 13 | return i + 1; 14 | 15 | return v.size() + 1; 16 | } 17 | 18 | int main() 19 | { 20 | std::vector nums = { 7, 7, 5, 4, 6, 2, 8, 100000 }; 21 | std::cout << "The first missing number is: " << getFirstMissing(nums) << std::endl; 22 | 23 | for (size_t i = 0; i < nums.size(); i++) // the index of the only positive number (counting from 1) is exactly the first missing one 24 | std::cout << nums[i] << ' '; 25 | } -------------------------------------------------------------------------------- /Sem_06/Task03.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void print(double x, double y, double z) 6 | { 7 | std::cout << "The sides of the triangle are of length: " << x << ", " << y << ", " << z << std::endl; 8 | } 9 | 10 | bool formATriangle(std::vector arr) 11 | { 12 | std::make_heap(arr.begin(), arr.end()); 13 | std::sort_heap(arr.begin(), arr.end()); 14 | 15 | for (size_t i = 0; i < arr.size() - 2; i++) 16 | { 17 | if (arr[i] + arr[i + 1] > arr[i + 2]) 18 | { 19 | print(arr[i], arr[i + 1], arr[i + 2]); 20 | return true; 21 | } 22 | } 23 | 24 | return false; 25 | } 26 | 27 | int main() 28 | { 29 | std::vector arr = { 2.5, 1, 11, 3.6, 8 }; 30 | if (!formATriangle(arr)) 31 | std::cout << "No such triangle!" << std::endl; 32 | } -------------------------------------------------------------------------------- /Sem_07/Task03.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | unsigned getMinimumOperations(unsigned n) 7 | { 8 | std::vector minOperations(n + 1, UINT_MAX); 9 | 10 | minOperations[1] = 0; 11 | minOperations[2] = 1; 12 | minOperations[3] = 1; 13 | 14 | for (size_t i = 4; i <= n; i++) 15 | { 16 | unsigned int v1 = i % 3 == 0 ? minOperations[i / 3] : UINT_MAX; 17 | unsigned int v2 = i % 2 == 0 ? minOperations[i / 2] : UINT_MAX; 18 | unsigned int v3 = minOperations[i - 1]; 19 | 20 | minOperations[i] = 1 + std::min({ v1, v2, v3 }); 21 | } 22 | 23 | return minOperations[n]; 24 | } 25 | 26 | int main() 27 | { 28 | std::cout << "The minimum number of operations is " << getMinimumOperations(962340) << std::endl; 29 | } 30 | -------------------------------------------------------------------------------- /Sem_06/Task04.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void printPair(int x, int y) 6 | { 7 | std::cout << "Pair found (" << x << ", " << y << ')' << std::endl; 8 | } 9 | 10 | bool findPair(std::vector& arr, int K) 11 | { 12 | std::make_heap(arr.begin(), arr.end()); 13 | std::sort_heap(arr.begin(), arr.end()); 14 | 15 | size_t l = 0, h = arr.size() - 1; 16 | 17 | while (l < h) 18 | { 19 | if (arr[l] + arr[h] == K) 20 | { 21 | printPair(arr[l], arr[h]); 22 | return true; 23 | } 24 | 25 | (arr[l] + arr[h] < K) ? l++ : h--; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | int main() 32 | { 33 | std::vector arr = { 8, 7, 2, 5, 3, 1 }; 34 | if (!findPair(arr, 10)) 35 | std::cout << "No such pair!" << std::endl; 36 | } -------------------------------------------------------------------------------- /Sem_06/Task05.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | template 5 | size_t partition(T* arr, size_t len, T p) 6 | { 7 | T pivot = p; 8 | size_t i = 0, j = len - 1; 9 | 10 | while (true) 11 | { 12 | while (arr[j] > pivot) 13 | j--; 14 | 15 | while (arr[i] < pivot) 16 | i++; 17 | 18 | if (i < j) 19 | std::swap(arr[i], arr[j]); 20 | else 21 | return j; 22 | } 23 | } 24 | 25 | void rearrange(std::vector& v) 26 | { 27 | size_t p = partition(v.data(), v.size(), 0) + 1; // p holds the index of the first positive element 28 | 29 | for (size_t i = 0; p < v.size() && i < p; p++, i += 2) 30 | std::swap(v[i], v[p]); 31 | } 32 | 33 | int main() 34 | { 35 | std::vector v = { 9, -3, 5, -2, -8, -6, 1, 3 }; 36 | rearrange(v); 37 | 38 | for (size_t i = 0; i < v.size(); i++) 39 | std::cout << v[i] << ' '; 40 | } -------------------------------------------------------------------------------- /Sem_08/Task02.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | size_t theft(const std::vector& paintingsPrices) 6 | { 7 | std::vector maxProfits(3); // We don't need to keep track of all n max profits. 8 | // We only need to keep the last three elements (maxProfits[k-2], maxProfits[k-1], maxProfits[k]). 9 | // Thus we optimise the spatial complexity from O(n) to O(1). 10 | 11 | maxProfits[0] = paintingsPrices[0]; 12 | maxProfits[1] = std::max(maxProfits[0], maxProfits[1]); 13 | 14 | for (size_t i = 2; i < paintingsPrices.size(); i++) 15 | { 16 | maxProfits[2] = std::max(paintingsPrices[i] + maxProfits[0], maxProfits[1]); 17 | maxProfits[0] = maxProfits[1]; 18 | maxProfits[1] = maxProfits[2]; 19 | } 20 | 21 | return maxProfits[2]; 22 | } 23 | 24 | int main() 25 | { 26 | std::vector paintingsPrices = { 7, 6, 13, 20, 1, 18, 19 }; 27 | std::cout << "The most expensive collection the thief can steal costs " << theft(paintingsPrices) << '.' << std::endl; 28 | } -------------------------------------------------------------------------------- /Sem_10/GraphAlgorithms/UnionFind/UnionFind.cpp: -------------------------------------------------------------------------------- 1 | #include "UnionFind.h" 2 | 3 | UnionFind::UnionFind(size_t n) : parents(n), sizes(n) 4 | { 5 | for (size_t i = 0; i < n; i++) 6 | { 7 | parents[i] = i; // At the beginning each set consists of only one element which is also a parent to itself (and the leader of the set) 8 | sizes[i] = 1; // All sets are with one element 9 | } 10 | } 11 | 12 | bool UnionFind::Union(size_t n, size_t k) // Union by rank 13 | { 14 | size_t firstLeader = Find(n); 15 | size_t secondLeader = Find(k); 16 | 17 | if (firstLeader == secondLeader) // n and k are already in one set 18 | return false; 19 | 20 | if (sizes[firstLeader] < sizes[secondLeader]) 21 | std::swap(firstLeader, secondLeader); 22 | 23 | // The set with leader "firstLeader" certainly has more elements than the other one 24 | parents[secondLeader] = firstLeader; 25 | sizes[firstLeader] += sizes[secondLeader]; 26 | 27 | return true; 28 | } 29 | size_t UnionFind::Find(size_t n) // Returns the leader of the set where n belongs to 30 | { 31 | if (parents[n] == n) 32 | return n; 33 | 34 | return Find(parents[n]); 35 | } -------------------------------------------------------------------------------- /Sem_11/Task02.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int getNextOpeningBracketIndex(const char* str) 7 | { 8 | for (size_t i = 0; *str != '\0'; i++, str++) 9 | if (*str == '[') 10 | return i; 11 | 12 | return -1; 13 | } 14 | 15 | size_t minimumSwapsNeededForBalancing(std::string& str) 16 | { 17 | size_t nextPosOfOpeningBracket = 0; 18 | size_t result = 0; 19 | int count = 0; 20 | 21 | for (size_t i = 0; i < str.size(); i++) 22 | { 23 | if (str[i] == '[') 24 | { 25 | ++nextPosOfOpeningBracket; 26 | ++count; 27 | } 28 | else 29 | { 30 | --count; 31 | } 32 | 33 | if (count < 0) 34 | { 35 | int nextOpeningBracketIndFromI = getNextOpeningBracketIndex(str.c_str() + i); 36 | assert(nextOpeningBracketIndFromI > 0); 37 | 38 | result += nextOpeningBracketIndFromI; 39 | std::swap(str[i], str[i + nextOpeningBracketIndFromI]); 40 | 41 | i++; 42 | count = 0; 43 | } 44 | } 45 | return result; 46 | } 47 | 48 | int main() 49 | { 50 | std::string str("[]][]["); 51 | std::cout << "Swaps: " << minimumSwapsNeededForBalancing(str) << std::endl; 52 | std::cout << str << std::endl; 53 | } 54 | -------------------------------------------------------------------------------- /Sem_05/Task02.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Pick.hpp" 3 | 4 | template 5 | size_t partition(T* arr, size_t len, T p) 6 | { 7 | T pivot = p; 8 | size_t i = 0, j = len - 1; 9 | 10 | while (true) 11 | { 12 | while (arr[j] > pivot) 13 | j--; 14 | 15 | while (arr[i] < pivot) 16 | i++; 17 | 18 | if (i < j) 19 | std::swap(arr[i], arr[j]); 20 | else 21 | return j; 22 | } 23 | } 24 | 25 | size_t countSouvenirsICanBuy(size_t* souvenirs, size_t len, size_t money) 26 | { 27 | if (len == 0) 28 | return 0; 29 | 30 | if (len == 1) 31 | return souvenirs[0] <= money; 32 | 33 | size_t mid = len / 2; 34 | 35 | size_t median = PICK(souvenirs, len, mid); // O(n) 36 | 37 | partition(souvenirs, len, median); // O(n) 38 | 39 | size_t sum = 0; 40 | for (size_t i = 0; i < mid; i++) // O(n) 41 | sum += souvenirs[i]; 42 | 43 | if (sum == money) 44 | return mid; 45 | 46 | if (sum > money) 47 | return countSouvenirsICanBuy(souvenirs, mid, money); 48 | 49 | if (sum < money) 50 | return mid + countSouvenirsICanBuy(souvenirs + mid, len - mid, money - sum); 51 | } 52 | int main() 53 | { 54 | size_t souvenirs[] = { 39, 4, 6, 1, 2, 66, 33 }; 55 | 56 | std::cout << countSouvenirsICanBuy(souvenirs, sizeof(souvenirs) / sizeof(size_t), 40); 57 | } 58 | -------------------------------------------------------------------------------- /Sem_07/Task02.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void print(size_t k, size_t m) 5 | { 6 | std::cout << "The indices of the numbers whose difference is divisible by 3n are " << k << " and " << m << '.' << std::endl; 7 | } 8 | 9 | bool sameRemainders(const std::vector& v) 10 | { 11 | size_t sizeTimesThree = 3 * v.size(); // 3n 12 | std::vector remainders(sizeTimesThree, -1); // at every index (corresponding to a certain remainder) we will keep the index of the last element which has 13 | // that remainder (mod 3n) or -1, if such doesn't exist 14 | 15 | int currentRemainder; 16 | for (size_t i = 0; i < v.size(); i++) 17 | { 18 | currentRemainder = v[i] % (int)sizeTimesThree; 19 | if (currentRemainder < 0) // we want the remainders in their positive forms (e.g. -3 % 27 = -3 or 24) 20 | currentRemainder += sizeTimesThree; 21 | 22 | if (remainders[currentRemainder] == -1) 23 | remainders[currentRemainder] = i; 24 | else 25 | { 26 | print(remainders[currentRemainder], i); 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | 34 | int main() 35 | { 36 | std::vector nums = { 4, 1, -3, 8, 16, 19, 22, 11, 24 }; 37 | if (!sameRemainders(nums)) 38 | std::cout << "No such elements!" << std::endl; 39 | } -------------------------------------------------------------------------------- /Advanced-Timer_Scheduler/InvokableTask.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // A wrapper of a callable object (supporting operator()) 6 | class InvokableTask 7 | { 8 | public: 9 | template 10 | explicit InvokableTask(FunctionType&& f) 11 | : m_invokableTaskPtr{ std::make_unique>(std::move(f)) } 12 | {} 13 | 14 | InvokableTask(const InvokableTask&) = delete; 15 | InvokableTask& operator=(const InvokableTask&) = delete; 16 | 17 | InvokableTask(InvokableTask&& other) noexcept 18 | : m_invokableTaskPtr{ std::move(other.m_invokableTaskPtr) } 19 | {} 20 | 21 | void operator()() 22 | { 23 | m_invokableTaskPtr->invoke(); 24 | } 25 | void invoke() 26 | { 27 | m_invokableTaskPtr->invoke(); 28 | } 29 | 30 | private: 31 | struct Base 32 | { 33 | virtual void invoke() = 0; 34 | virtual ~Base() = default; 35 | }; 36 | 37 | template 38 | struct Task : Base 39 | { 40 | explicit Task(FunctionType&& func) : m_func{ std::move(func) } 41 | { 42 | static_assert(std::is_invocable_v); 43 | } 44 | void invoke() override 45 | { 46 | m_func(); 47 | } 48 | 49 | FunctionType m_func; 50 | }; 51 | 52 | std::unique_ptr m_invokableTaskPtr; 53 | }; -------------------------------------------------------------------------------- /Sem_07/Task_Midterm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void preprocess(const std::vector& data, std::vector& enumerativeArray, size_t lowerBound, size_t upperBound) 5 | { 6 | size_t range = upperBound - lowerBound + 1; 7 | enumerativeArray.resize(range, 0); 8 | 9 | for (size_t i = 0; i < data.size(); i++) 10 | enumerativeArray[data[i] - lowerBound]++; 11 | 12 | for (size_t i = 0; i < enumerativeArray.size() - 1; i++) 13 | enumerativeArray[i + 1] += enumerativeArray[i]; 14 | 15 | } 16 | size_t getLengthOfAnInterval(const std::vector& queriesArray, size_t a, size_t b, size_t lowerBound) 17 | { 18 | return queriesArray[b - lowerBound] - queriesArray[a - lowerBound] + 1; 19 | } 20 | 21 | int main() 22 | { 23 | size_t l = 5; // lower bound of Ynumbers 24 | size_t m = 100; // upper bound of Ynumbers 25 | std::vector Ynumbers = { 5, 8, 10, 22, 24, 23, 55, 66, 18 }; // n numbers 26 | 27 | std::vector queriesArray; 28 | preprocess(Ynumbers, queriesArray, l, m); // Complexity: O(n+(m-l)) 29 | 30 | size_t a, b; 31 | while (true) 32 | { 33 | std::cout << "Enter a and b:" << std::endl; 34 | std::cin >> a >> b; 35 | 36 | // Complexity: O(1) for each query 37 | std::cout << "There are " << getLengthOfAnInterval(queriesArray, a, b, l) << " numbers in the interval [" << a << ", " << b << "]." << std::endl << std::endl; 38 | } 39 | } -------------------------------------------------------------------------------- /Sem_08/Task04.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void printTable(const std::vector>& dyn) 5 | { 6 | std::cout << "Dynamic programming table:" << std::endl; 7 | for (size_t i = 0; i < dyn.size(); i++) 8 | { 9 | for (size_t j = 0; j < dyn[0].size(); j++) 10 | std::cout << dyn[i][j] << ' '; 11 | std::cout << std::endl; 12 | } 13 | std::cout << std::endl; 14 | } 15 | 16 | size_t solutionsEquation(const std::vector& A, size_t B) 17 | { 18 | std::vector> dyn(A.size() + 1); // Memory optimisation is to keep only the last two rows. 19 | // Here is not done in order to print the dynamic table. 20 | for (size_t i = 0; i < dyn.size(); i++) 21 | dyn[i].resize(B + 1, 0); 22 | 23 | for (size_t i = 0; i < dyn.size(); i++) 24 | dyn[i][0] = 1; 25 | 26 | for (size_t i = 1; i < dyn.size(); i++) 27 | { 28 | for (size_t B = 1; B < dyn[0].size(); B++) 29 | { 30 | if (A[i - 1] > B) 31 | dyn[i][B] = dyn[i - 1][B]; 32 | else 33 | dyn[i][B] = dyn[i - 1][B] + dyn[i][B - A[i - 1]]; 34 | } 35 | } 36 | 37 | printTable(dyn); 38 | return dyn[dyn.size() - 1][dyn[0].size() - 1]; 39 | } 40 | 41 | int main() 42 | { 43 | std::vector A = { 2, 3, 5 }; 44 | size_t B = 9; 45 | 46 | std::cout << "The solution is " << solutionsEquation(A, B) << '.' << std::endl; 47 | } -------------------------------------------------------------------------------- /Sem_08/Task03.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int maxSum(const std::vector& A, const std::vector& B, const std::vector& C) 6 | { 7 | std::vector> maxSums; 8 | for (size_t i = 0; i < 3; i++) 9 | maxSums.push_back(std::vector(2)); // We don't need to keep track of all n columns. 10 | // We only need to keep the last two columns. 11 | // Thus we optimise the spatial complexity from O(n) to O(1). 12 | 13 | maxSums[0][0] = A[0]; 14 | maxSums[1][0] = B[0]; 15 | maxSums[2][0] = C[0]; 16 | 17 | for (size_t i = 1; i < A.size(); i++) 18 | { 19 | maxSums[0][1] = std::max(maxSums[1][0], maxSums[2][0]) + A[i]; 20 | maxSums[1][1] = std::max(maxSums[0][0], maxSums[2][0]) + B[i]; 21 | maxSums[2][1] = std::max(maxSums[0][0], maxSums[1][0]) + C[i]; 22 | 23 | maxSums[0][0] = maxSums[0][1]; 24 | maxSums[1][0] = maxSums[1][1]; 25 | maxSums[2][0] = maxSums[2][1]; 26 | } 27 | 28 | return std::max({ maxSums[0][1], maxSums[1][1], maxSums[2][1] }); 29 | } 30 | 31 | int main() 32 | { 33 | std::vector A = { 2, 3, 5, 1, 8, 7, 6 }; 34 | std::vector B = { 6, 9, 8, 0, 5, 9, 7 }; 35 | std::vector C = { 4, 1, 2, 8, 4, 2, 5 }; 36 | 37 | std::cout << "The maximal sum of n summands is " << maxSum(A, B, C) << '.' << std::endl; 38 | } 39 | -------------------------------------------------------------------------------- /Sem_06/Task01.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | template 6 | void merge(T* arr1, size_t len1, T* arr2, size_t len2, T* res) 7 | { 8 | T* result = new T[len1 + len2]; 9 | 10 | size_t cursor1 = 0; 11 | size_t cursor2 = 0; 12 | size_t resultCursor = 0; 13 | 14 | while (cursor1 < len1 && cursor2 < len2) 15 | { 16 | if (arr1[cursor1] < arr2[cursor2]) 17 | result[resultCursor++] = arr1[cursor1++]; 18 | else 19 | result[resultCursor++] = arr2[cursor2++]; 20 | } 21 | 22 | while (cursor1 < len1) 23 | result[resultCursor++] = arr1[cursor1++]; 24 | while (cursor2 < len2) 25 | result[resultCursor++] = arr2[cursor2++]; 26 | 27 | for (size_t i = 0; i < len1 + len2; i++) 28 | res[i] = result[i]; 29 | 30 | delete[] result; 31 | } 32 | 33 | void sortSpecialArray(std::vector& arr) 34 | { 35 | std::vector even; 36 | std::vector odd; 37 | 38 | for (size_t i = 0; i < arr.size(); i++) 39 | { 40 | if (arr[i] % 2 == 0) 41 | even.push_back(arr[i]); 42 | else 43 | odd.push_back(arr[i]); 44 | } 45 | 46 | std::reverse(odd.begin(), odd.end()); 47 | merge(even.data(), even.size(), odd.data(), odd.size(), arr.data()); 48 | } 49 | 50 | int main() 51 | { 52 | std::vector arr = { 2, 5, 8, 3, 12, 14, 20, -1 }; 53 | sortSpecialArray(arr); 54 | 55 | for (size_t i = 0; i < arr.size(); i++) 56 | std::cout << arr[i] << ' '; 57 | } -------------------------------------------------------------------------------- /Sem_06/Task02.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | template 5 | size_t mergeModified(T* arr1, size_t len1, T* arr2, size_t len2) 6 | { 7 | T* result = new T[len1 + len2]; 8 | 9 | size_t cursor1 = 0; 10 | size_t cursor2 = 0; 11 | size_t resultCursor = 0; 12 | 13 | size_t inversionCount = 0; 14 | 15 | while (cursor1 < len1 && cursor2 < len2) 16 | { 17 | if (arr1[cursor1] < arr2[cursor2]) 18 | result[resultCursor++] = arr1[cursor1++]; 19 | else 20 | { 21 | inversionCount += (len1 - cursor1); 22 | result[resultCursor++] = arr2[cursor2++]; 23 | } 24 | } 25 | 26 | while (cursor1 < len1) 27 | result[resultCursor++] = arr1[cursor1++]; 28 | while (cursor2 < len2) 29 | result[resultCursor++] = arr2[cursor2++]; 30 | 31 | for (size_t i = 0; i < len1 + len2; i++) 32 | arr1[i] = result[i]; 33 | 34 | delete[] result; 35 | return inversionCount; 36 | } 37 | 38 | template 39 | size_t inversionCount(T* arr, size_t len) // O(nlgn) 40 | { 41 | if (len == 1) 42 | return 0; 43 | 44 | size_t mid = len / 2; 45 | 46 | size_t x = inversionCount(arr, mid); 47 | size_t y = inversionCount(arr + mid, len - mid); 48 | 49 | size_t z = mergeModified(arr, mid, arr + mid, len - mid); 50 | 51 | return x + y + z; 52 | } 53 | 54 | int main() 55 | { 56 | int arr[] = { 10, 9, 8, 7, 6, 5 }; // number of inversions: (n-1)*n / 2 = 5*6 / 2 = 15 57 | std::cout << inversionCount(arr, sizeof(arr) / sizeof(int)); 58 | } -------------------------------------------------------------------------------- /Advanced-Intel_intrinsics _ BS_optimization/README.md: -------------------------------------------------------------------------------- 1 | # Intrinsics 2 | 3 | Intrinsics are a set of assembly-coded functions that allow developers to use C++ function calls and variables in place of assembly instructions. They are designed to enhance performance by providing inline expansions of the code, which eliminates the overhead of a function call. Additionally, intrinsics improve the readability of the code, assist with instruction scheduling, and reduce debugging time. 4 | 5 | [Here you can see a full list of **Intel intrinsics**](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html). 6 | 7 | One example of intrinsics (the one I've used for my solution) is the [**Advanced Vector Extensions (AVX)**](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions), which is an instruction set extension for x86 processors. AVX provides 256-bit floating-point operations and enables parallel processing of multiple data elements, which significantly enhances the performance of vectorized algorithms. 8 | 9 | To evaluate the performance of AVX, a series of tests were conducted to compare the speed of an ordinary Binary Search with that of an AVX Binary Search. The tests used various datasets and were repeated multiple times to ensure accuracy. The results showed a significant improvement in performance for the AVX Binary Search across all tests, indicating the efficacy of using intrinsics for optimization purposes. 10 | 11 | ![alt_text](https://github.com/MariaGrozdeva/Design_and_analysis_of_algorithms_FMI_2021-2022/blob/main/Advanced-Intel_intrinsics%20_%20BS_optimization/Tests.png) 12 | -------------------------------------------------------------------------------- /Sem_08/Task02_recoverSolution.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void printSolution(const std::vector& paintingPrices, const std::vector& maxProfits) 6 | { 7 | std::vector solution; 8 | int i = maxProfits.size() - 1; 9 | 10 | while (i >= 0) 11 | { 12 | if (i == 0) 13 | { 14 | solution.push_back(paintingPrices[0]); 15 | i--; 16 | } 17 | else if (maxProfits[i] == maxProfits[i - 1]) 18 | { 19 | i--; 20 | } 21 | else 22 | { 23 | solution.push_back(paintingPrices[i]); 24 | i -= 2; 25 | } 26 | } 27 | 28 | std::cout << "The solution is: "; 29 | for (int i = solution.size() - 1; i >= 0; i--) 30 | std::cout << solution[i] << ' '; 31 | std::cout << std::endl; 32 | } 33 | 34 | size_t theft(const std::vector& paintingsPrices) 35 | { 36 | std::vector maxProfits(paintingsPrices.size()); // We don't need to keep track of all n max profits. 37 | // We only need to keep the last three elements (maxProfits[k-2], maxProfits[k-1], maxProfits[k]). 38 | // Thus we optimise the spatial complexity from O(n) to O(1). 39 | 40 | maxProfits[0] = paintingsPrices[0]; 41 | maxProfits[1] = std::max(maxProfits[0], maxProfits[1]); 42 | 43 | for (size_t i = 2; i < paintingsPrices.size(); i++) 44 | maxProfits[i] = std::max(paintingsPrices[i] + maxProfits[i - 2], maxProfits[i - 1]); 45 | 46 | printSolution(paintingsPrices, maxProfits); 47 | return maxProfits[paintingsPrices.size() - 1]; 48 | } 49 | 50 | int main() 51 | { 52 | std::vector paintingsPrices = { 7, 8, 13, 20, 1, 18, 19 }; 53 | std::cout << "The most expensive collection the thief can steal costs " << theft(paintingsPrices) << '.' << std::endl; 54 | } 55 | -------------------------------------------------------------------------------- /Sem_05/Task03.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Pick.hpp" 3 | 4 | template 5 | size_t partition(T* arr, size_t len, T p) 6 | { 7 | T pivot = p; 8 | size_t i = 0, j = len - 1; 9 | 10 | while (true) 11 | { 12 | while (arr[j] > pivot) 13 | j--; 14 | 15 | while (arr[i] < pivot) 16 | i++; 17 | 18 | if (i < j) 19 | std::swap(arr[i], arr[j]); 20 | else 21 | return j; 22 | } 23 | } 24 | 25 | size_t maximalStocksMinimalStockholders(size_t* stocks, size_t len, size_t goal) 26 | { 27 | if (len == 1) // If we have only one element, we take it, so the starting index of the stocks we take is 0. 28 | return 0; 29 | 30 | size_t mid = len / 2; 31 | 32 | size_t median = PICK(stocks, len, mid); // O(n) 33 | 34 | partition(stocks, len, median); // O(n) 35 | 36 | size_t totalStocksCount = 0; 37 | for (size_t i = mid; i < len; i++) // O(n) 38 | totalStocksCount += stocks[i]; 39 | 40 | if (totalStocksCount == goal) 41 | return mid; 42 | 43 | if (totalStocksCount > goal) 44 | return mid + maximalStocksMinimalStockholders(stocks + mid, len - mid, goal); 45 | 46 | if (totalStocksCount < goal) 47 | return maximalStocksMinimalStockholders(stocks, mid, goal - totalStocksCount); 48 | } 49 | size_t maximalStocksMinimalStockholders(size_t* stocks, size_t len) 50 | { 51 | size_t sum = 0; 52 | for (size_t i = 0; i < len; i++) 53 | sum += stocks[i]; 54 | (sum /= 2)++; // We want to buy more than half of the stocks. 55 | 56 | size_t resultIndex = maximalStocksMinimalStockholders(stocks, len, sum); 57 | return len - resultIndex; 58 | } 59 | 60 | int main() 61 | { 62 | size_t stocks[] = { 39,4,6,1,2,66,33 }; 63 | std::cout << maximalStocksMinimalStockholders(stocks, sizeof(stocks) / sizeof(size_t)); 64 | } 65 | -------------------------------------------------------------------------------- /Sem_08/Fibonacci.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | size_t fibonacciRecursive(size_t n) 6 | { 7 | if (n <= 1) 8 | return 1; 9 | 10 | return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2); 11 | } 12 | 13 | size_t fibonacciMemoization(size_t n, std::vector& mem) 14 | { 15 | if (n <= 1) 16 | return 1; 17 | 18 | if (mem[n] != 0) 19 | return mem[n]; 20 | 21 | size_t currentFib = fibonacciMemoization(n - 1, mem) + fibonacciMemoization(n - 2, mem); 22 | mem[n] = currentFib; 23 | 24 | return currentFib; 25 | } 26 | size_t fibonacciMemoization(size_t n) 27 | { 28 | std::vector mem(n + 1, 0); 29 | return fibonacciMemoization(n, mem); 30 | } 31 | 32 | size_t fibonacciDP(size_t n) 33 | { 34 | std::vector fibNums(3); // We need to keep track only of the last two fibonacci numbers and the current one. 35 | 36 | fibNums[0] = fibNums[1] = 1; 37 | 38 | for (size_t i = 2; i <= n; i++) 39 | { 40 | fibNums[2] = fibNums[1] + fibNums[0]; 41 | fibNums[0] = fibNums[1]; 42 | fibNums[1] = fibNums[2]; 43 | } 44 | 45 | return fibNums[2]; 46 | } 47 | 48 | int main() 49 | { 50 | time_t start, end; 51 | 52 | std::cout << "Fibonacci recursive..."; 53 | start = std::time(NULL); 54 | fibonacciRecursive(45); 55 | end = std::time(NULL); 56 | std::cout << "\n execution took " << end - start << " second(s)\n\n"; 57 | 58 | std::cout << "Fibonacci with memoization..."; 59 | start = std::time(NULL); 60 | fibonacciMemoization(45); 61 | end = std::time(NULL); 62 | std::cout << "\n execution took " << end - start << " second(s)\n\n"; 63 | 64 | std::cout << "Fibonacci with dynamic programming..."; 65 | start = std::time(NULL); 66 | fibonacciDP(45); 67 | end = std::time(NULL); 68 | std::cout << "\n execution took " << end - start << " second(s)\n\n"; 69 | } -------------------------------------------------------------------------------- /Advanced-Timer_Scheduler/TaskPool.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "InvokableTask.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class TaskPool 16 | { 17 | public: 18 | explicit TaskPool(unsigned threadsCount = std::thread::hardware_concurrency()) 19 | { 20 | try 21 | { 22 | for (size_t i = 0; i < threadsCount; i++) 23 | { 24 | m_threads.emplace_back(&TaskPool::workerThread, this); 25 | } 26 | } 27 | catch (...) 28 | { 29 | m_isRunning = false; 30 | m_haveWorkEvent.notify_all(); 31 | throw; 32 | } 33 | } 34 | 35 | ~TaskPool() 36 | { 37 | m_isRunning = false; 38 | m_haveWorkEvent.notify_all(); 39 | for (auto&& t : m_threads) 40 | { 41 | t.join(); 42 | } 43 | } 44 | 45 | TaskPool(const TaskPool&) = delete; 46 | TaskPool& operator=(const TaskPool&) = delete; 47 | TaskPool(TaskPool&&) = delete; 48 | TaskPool& operator=(TaskPool&&) = delete; 49 | 50 | template 51 | void run(FunctionType&& f) 52 | { 53 | std::unique_lock lock(m_workMutex); 54 | m_tasksQueue.emplace(std::move(f)); 55 | lock.unlock(); 56 | 57 | m_haveWorkEvent.notify_one(); 58 | } 59 | 60 | private: 61 | std::atomic_bool m_isRunning = true; 62 | std::vector m_threads; 63 | std::queue m_tasksQueue; 64 | std::condition_variable m_haveWorkEvent; 65 | std::mutex m_workMutex; 66 | 67 | void workerThread() 68 | { 69 | while (m_isRunning) 70 | { 71 | std::unique_lock lock(m_workMutex); 72 | m_haveWorkEvent.wait(lock, [this] { return !m_tasksQueue.empty() || !m_isRunning; }); 73 | 74 | if (!m_isRunning) 75 | { 76 | return; 77 | } 78 | 79 | InvokableTask toExecute = std::move(m_tasksQueue.front()); 80 | 81 | m_tasksQueue.pop(); 82 | lock.unlock(); 83 | 84 | toExecute(); 85 | } 86 | } 87 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Теми от семинарите по "Дизайн и анализ на алгоритми", летен семестър 2021/2022, спец. "Компютърни науки", първи поток ## 2 | 3 | - **Тема 1 (26.02.2022)** : Анализ на алгоритми. Анализ на времевата сложност. Асимптотични сравнения на функции. 4 | - **Тема 2 (05.03.2022)** : Анализ на итеративни алгоритми чрез сумиране. Анализ на рекурсивни алгоритми. Рекурентни уравнения. Методи за решаване на рекурентни уравнения. 5 | - **Тема 3 (12.03.2022)** : Мастър теорема. Коректност на итеративни алгоритми. Доказателство за коректност с инвариант на цикъла. Доказателство за приключване (терминация) на итеративни алгоритми с полуинвариант. Задачи за съставяне на "оптимални" алгоритми. 6 | - **Тема 4 (19.03.2022)** : Задачи върху темата "Анализ на алгоритми". Подготовка за първо контролно. 7 | - **Тема 5 (28.03.2022)** : Алгоритми за търсене. Алгоритъмът PICK. Алгоритъмът на Кадан. Приложения в задачи. 8 | - **Тема 6 (04.04.2022)** : Алгоритми за сортиране. Примери за задачи, използващи сортиране за постигане на оптимална сложност. Приложение на merge и partition. 9 | - **Тема 7 (11.04.2022)** : Сортиране в линейно време. Въведение в Динамичното програмиране. 10 | - **Тема 8 (18.04.2022)** : Динамично програмиране. Мемоизация. 11 | - **Тема 9 (09.05.2022)** : Нетегловни графи. Алгоритми върху графи (първа част) - BFS/DFS, изследване на цикличност на графи, двуделност на графи, топологично сортиране, намиране на силно свързани компоненти. 12 | - **Тема 10 (16.05.2022)** : Тегловни графи. Алгоритми върху графи (втора част) - Dijkstra, Bellman-Ford, Floyd–Warshall, Prim, Kruskal. 13 | - **Тема 11 (23.05.2022)** : Алчни алгоритми (greedy algorithms). 14 | - **Тема 12 (31.05.2022)** : Долни граници. Доказателство за долна граница чрез дърво за вземане на решения, чрез "игра срещу противник" и чрез редукция. 15 | - **Тема 13 (06.06.2022)** : Алгоритмична неподатливост. Класове P и NP. Полиномиални редукции. NP-трудност и NP-пълнота. SAT и основни NP-пълни задачи. 16 | 17 | ## Other (advanced) topics 18 | - **Тема** : Intel intrinsics - оптимизация на Двоично търсене. 19 | - **Тема** : Memory allocator. 20 | - **Тема** : Timer scheduler - система за управление на еднократни и повтарящи се таймери. 21 | -------------------------------------------------------------------------------- /Advanced-Intel_intrinsics _ BS_optimization/Intel_intrinsics-Binary_search.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* 5 | * @param hayStack - the input data that will be searched in 6 | * @param needles - the values that will be searched 7 | * @param indices - the indices of the needles (or -1 if the needle is not found) 8 | */ 9 | static void AVXBinarySearch(const int* hayStack, std::size_t hayStackCount, const int* needles, std::size_t needlesCount, int* indices) 10 | { 11 | const int needlesPerIteration = 8; 12 | 13 | __m256i onesVector = _mm256_set1_epi32(1); 14 | __m256i minusOnesVector = _mm256_set1_epi32(-1); 15 | 16 | for (size_t i = 0; i < needlesCount; i += needlesPerIteration) 17 | { 18 | __m256i indexVector = minusOnesVector; 19 | __m256i needleVector = _mm256_loadu_si256(reinterpret_cast(needles + i)); 20 | 21 | __m256i leftVector = _mm256_setzero_si256(); 22 | __m256i rightVector = _mm256_set1_epi32(hayStackCount - 1); 23 | if (hayStack[0] == hayStack[hayStackCount - 1]) 24 | rightVector = leftVector; 25 | 26 | __m256i leftGtRightVector = _mm256_setzero_si256(); 27 | 28 | while (_mm256_movemask_epi8(leftGtRightVector) != -1) 29 | { 30 | leftGtRightVector = _mm256_cmpgt_epi32(leftVector, rightVector); 31 | __m256i leftNotGtRightVector = _mm256_andnot_si256(leftGtRightVector, minusOnesVector); 32 | 33 | // mid = left + (right - left) / 2 34 | __m256i midPositionVector = _mm256_blendv_epi8(onesVector, 35 | _mm256_add_epi32(leftVector, _mm256_srli_epi32(_mm256_sub_epi32(rightVector, leftVector), 1)), leftNotGtRightVector); 36 | __m256i midElementVector = _mm256_blendv_epi8(onesVector, 37 | _mm256_i32gather_epi32(hayStack, midPositionVector, sizeof(int)), leftNotGtRightVector); 38 | 39 | __m256i equalVector = _mm256_blendv_epi8(onesVector, _mm256_cmpeq_epi32(needleVector, midElementVector), leftNotGtRightVector); 40 | __m256i greaterVector = _mm256_blendv_epi8(onesVector, _mm256_cmpgt_epi32(needleVector, midElementVector), leftNotGtRightVector); 41 | __m256i smallerOrEqualVector = _mm256_andnot_si256(greaterVector, minusOnesVector); 42 | 43 | indexVector = _mm256_blendv_epi8(indexVector, midPositionVector, equalVector); 44 | leftVector = _mm256_blendv_epi8(leftVector, _mm256_add_epi32(midPositionVector, onesVector), 45 | _mm256_and_si256(greaterVector, leftNotGtRightVector)); 46 | rightVector = _mm256_blendv_epi8(rightVector, _mm256_sub_epi32(midPositionVector, onesVector), 47 | _mm256_and_si256(smallerOrEqualVector, leftNotGtRightVector)); 48 | } 49 | _mm256_storeu_si256(reinterpret_cast<__m256i*>(indices + i), indexVector); 50 | } 51 | } 52 | 53 | int main() 54 | { 55 | 56 | } -------------------------------------------------------------------------------- /Sem_06/README.md: -------------------------------------------------------------------------------- 1 | **Задача 1:** Даден е масив arr от цели числа. За този масив са изпълнени следните условия: 2 | - За всяко i,j, ако i < j и arr[i], arr[j] са **четни**, то **arr[i] <= arr[j]**. 3 | - За всяко i,j, ако i < j и arr[i], arr[j] са **нечетни**, то **arr[i] >= arr[j]**. 4 | 5 | *Предложете алгоритъм*, който **сортира масива** за време **O(n)**. 6 | 7 | **Пример**: 8 | X = [2, 5, 8, 3, 12, 14, 20, -1] 9 | X' = [-1, 2, 3, 5, 8, 12, 14, 20] 10 | 11 | --- 12 | 13 | **Задача 2:** Даден е масив arr от цели числа. 14 | **Инверсия** в масив наричаме всяка двойка индекси , такива че **1 <= i < j <= n и A[i] > A[j]**. 15 | 16 | *Предложете алгоритъм*, който **намира броя на инверсиите** в масива arr за време **O(nlgn)**. 17 | 18 | --- 19 | 20 | **Задача 3:** Даден е масив arr от положителни реални числа - дължини на отсечки. 21 | 22 | *Предложете алгоритъм*, който **проверява дали съществуват три отсечки, които да образуват триъгълник** за време **O(nlgn)**. 23 | 24 | **Твърдение:** Ако съществуват три отсечки в масива arr, които образуват триъгълник, то след сортиране на масива съществуват три отсечки, които образуват триъгълник и са разположени ***непосредствено една след друга***. 25 | 26 | **Доказателство:** 27 | Нека **arr[k]**, **arr[r]** и **arr[p]** са дължините на страните на някой триъгълник. Б.О.О. нека **k < r < p**. Но arr е сортиран и arr[k], arr[r] и arr[p] са дължини на страни на триъгълник. Следователно **arr[k] + arr[r] > arr[p]**. 28 | От k < r < p следва, че **k <= r-1**, **p >= r+1**. Оттук, заедно с това, че arr е сортиран, получаваме, че **arr[k] <= arr[r-1]** и **arr[p] >= arr[r+1]**. 29 | Тогава **arr[r-1] + arr[r] >= arr[k] + arr[r] > arr[p] >= arr[r+1]**. 30 | Получихме, че **arr[r-1] + arr[r] > arr[r+1]**. 31 | Но това са три последователни числа в сортирания масив arr, такива че сборът на двете по- малки надхвърля най- голямото. Следователно това са страни на триъгълник. 32 | 33 | --- 34 | 35 | **Задача 4:** Даден е масив arr от цели числа и сума K. 36 | 37 | *Предложете алгоритъм*, който **проверява дали съществуват два елемента в масива, чиято сума е K** за време **O(nlgn)**. 38 | 39 | **Пример**: 40 | ||| 41 | |--|--| 42 | |*Вход:*|*Вход:*| 43 | |X = [8, 7, 2, 5, 3, 1], K = 10|Y = [5, 2, 6, 8, 1, 9], K = 12| 44 | |*Изход:*|*Изход:*| 45 | Pair found (8, 2) (или Pair found (7, 3))|Pair not found| 46 | 47 | --- 48 | 49 | **Задача 5:** Даден е масив arr от цели числа. 50 | 51 | *Предложете алгоритъм*, който **пренарежда елементите на масива така, че след всяко положително число да стои отрицателно**. Ако повече от половината числа са положителни/отрицателни, то съответният остатък от числа да бъде в края на масива. 52 | Алгоритъмът да използва **константна допълнителна памет**! 53 | 54 | **Пример**: 55 | X = [9, -3, 5, -2, -8, -6, 1, 3] 56 | X' = [5, -2, 9, -6, 1, -8, 3, -3] 57 | -------------------------------------------------------------------------------- /Sem_10/Task03.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | class Graph 6 | { 7 | private: 8 | std::vector>> adj; 9 | size_t verticesCount; 10 | 11 | bool oriented; 12 | 13 | public: 14 | Graph(size_t verticesCount, bool oriented); 15 | void addEdge(size_t start, size_t end, int weight); 16 | 17 | private: 18 | void DFS_rec(size_t start, std::vector& visited, std::vector& result) const; 19 | size_t areConnected(size_t start, size_t end) const; 20 | 21 | Graph removeEdgesWithBiggerWeight(size_t weight) const; 22 | 23 | public: 24 | bool isPartOfMST(size_t start, size_t end) const; 25 | }; 26 | 27 | void Graph::DFS_rec(size_t start, std::vector& visited, std::vector& result) const 28 | { 29 | visited[start] = true; 30 | result.push_back(start); 31 | 32 | for (auto it = adj[start].begin(); it != adj[start].end(); ++it) 33 | { 34 | if (visited[(*it).first]) 35 | continue; 36 | 37 | DFS_rec((*it).first, visited, result); 38 | } 39 | } 40 | size_t Graph::areConnected(size_t start, size_t end) const 41 | { 42 | std::vector visited(verticesCount); 43 | std::vector result; 44 | 45 | DFS_rec(start, visited, result); 46 | 47 | return visited[end]; 48 | } 49 | 50 | Graph Graph::removeEdgesWithBiggerWeight(size_t weight) const 51 | { 52 | Graph res(verticesCount, false); 53 | 54 | for (size_t i = 0; i < verticesCount; i++) 55 | { 56 | for (auto it = adj[i].begin(); it != adj[i].end(); ++it) 57 | { 58 | if ((*it).second < weight) 59 | res.addEdge(i, (*it).first, (*it).second); 60 | } 61 | } 62 | return res; 63 | } 64 | 65 | bool Graph::isPartOfMST(size_t start, size_t end) const 66 | { 67 | size_t weight = -1; 68 | for (auto it = adj[start].begin(); it != adj[start].end(); ++it) 69 | if ((*it).first == end) 70 | weight = (*it).second; 71 | 72 | if (weight == -1) 73 | return false; // no edge between start and end 74 | 75 | Graph lighterGraph = removeEdgesWithBiggerWeight(weight); 76 | return !lighterGraph.areConnected(start, end); 77 | } 78 | 79 | 80 | Graph::Graph(size_t verticesCount, bool oriented) : adj(verticesCount), verticesCount(verticesCount), oriented(oriented) 81 | {} 82 | void Graph::addEdge(size_t start, size_t end, int weight) 83 | { 84 | adj[start].push_back(std::make_pair(end, weight)); 85 | if (!oriented) 86 | adj[end].push_back(std::make_pair(start, weight)); 87 | } 88 | 89 | 90 | int main() 91 | { 92 | Graph g(9, false); 93 | g.addEdge(0, 1, 4); 94 | g.addEdge(0, 7, 8); 95 | g.addEdge(1, 2, 8); 96 | g.addEdge(1, 7, 11); 97 | g.addEdge(2, 3, 7); 98 | g.addEdge(2, 5, 4); 99 | g.addEdge(3, 4, 9); 100 | g.addEdge(3, 5, 14); 101 | g.addEdge(5, 4, 10); 102 | g.addEdge(6, 5, 2); 103 | g.addEdge(7, 6, 1); 104 | g.addEdge(7, 8, 7); 105 | g.addEdge(8, 2, 2); 106 | g.addEdge(8, 6, 6); 107 | 108 | std::cout << g.isPartOfMST(2, 5); 109 | } -------------------------------------------------------------------------------- /Sem_05/Pick.hpp: -------------------------------------------------------------------------------- 1 | // PICK finds the k-th smallest number in A[0...n-1]. 2 | // Worst-case time complexity: n. 3 | // Worst-case memory complexity: log n. 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | const int ROWS = 21; // ROWS must be an odd number greater than 5 13 | const int MIDDLE = 11; // MIDDLE must be ROWS/2 (rounded upwards). 14 | const int N0 = 200; // N0 must be greater than ROWS. 15 | 16 | 17 | template 18 | void sort(T M[], int start, int final) 19 | { 20 | 21 | for (int i = start + 1, j; i < final; ++i, j = i) 22 | { 23 | j = i; 24 | while ((j > start) && (M[j] < M[j - 1])) 25 | { 26 | std::swap(M[j], M[j - 1]); 27 | --j; 28 | } 29 | } 30 | } 31 | 32 | template 33 | int partitionPick(T arr[], int len, int splitter, bool smallerFirst) 34 | { 35 | // This algorithm is similar to Hoare's partition 36 | // but does not put the splitter between the two groups. 37 | int i = -1; 38 | int j = len; 39 | 40 | while (true) 41 | { 42 | 43 | do 44 | { 45 | ++i; 46 | } while ((smallerFirst && arr[i] < splitter) || (!smallerFirst && arr[i] > splitter)); 47 | do 48 | { 49 | --j; 50 | 51 | if (j <= i) 52 | return i; 53 | 54 | } while ((smallerFirst && arr[j] >= splitter) || (!smallerFirst && arr[j] <= splitter)); 55 | 56 | std::swap(arr[i], arr[j]); 57 | } 58 | } 59 | 60 | template 61 | T PICK(T A[], int n, int k) 62 | { 63 | int s, f, i, j, L1, L2; 64 | T M; 65 | 66 | while (true) 67 | { 68 | 69 | if (n < N0) 70 | { 71 | sort(A, 0, n); 72 | return A[k - 1]; 73 | } 74 | 75 | // Split the array into a table ROWS x (n / ROWS) 76 | // and find the median of each column: 77 | 78 | s = 0; 79 | f = ROWS; 80 | while (f <= n) 81 | { 82 | sort(A, s, f); 83 | s = f; 84 | f += ROWS; 85 | } 86 | if (s < n) 87 | sort(A, s, n); 88 | 89 | // Move the medians of the columns 90 | // to the beginning of the array: 91 | i = 0; 92 | j = MIDDLE; 93 | s = 0; 94 | f = ROWS; 95 | while (f <= n) 96 | { 97 | std::swap(A[i], A[j]); 98 | s = f; 99 | f += ROWS; 100 | j += ROWS; 101 | ++i; 102 | } 103 | if (s < n) 104 | { 105 | j = (s + n - 1) >> 1; 106 | std::swap(A[i], A[j]); 107 | } 108 | 109 | // Find the median of the medians: 110 | M = PICK(A, i, i >> 1); 111 | 112 | // Calculate the rank of M among the elements of A: 113 | L1 = 0; 114 | L2 = 0; 115 | 116 | for (i = 0; i < n; ++i) 117 | { 118 | if (A[i] < M) 119 | ++L1; 120 | 121 | if (A[i] <= M) 122 | ++L2; 123 | } 124 | 125 | // Check if M is the answer: 126 | if ((L1 < k) && (k <= L2)) 127 | return M; 128 | 129 | // Erase 1/4 of the elements: 130 | if (k <= L1) 131 | n = partitionPick(A, n, M, true); // Put the small numbers at the beginning: 132 | else 133 | { 134 | n = partitionPick(A, n, M, false); // Put the big numbers at the beginning: 135 | k -= L2; 136 | } 137 | 138 | } // The tail recursion has been replaced with a loop. 139 | } -------------------------------------------------------------------------------- /Advanced-Timer_Scheduler/Source.cpp: -------------------------------------------------------------------------------- 1 | #include "TimerScheduler.hpp" 2 | 3 | #include 4 | #include 5 | 6 | using systemClock = std::chrono::system_clock; 7 | using steadyClock = std::chrono::steady_clock; 8 | using s_t = std::chrono::seconds; 9 | using ns_t = std::chrono::nanoseconds; 10 | 11 | // ST - single timer, RT - repeating timer 12 | // NB: The printing of the repeating timers might be messed because of the concurrent execution 13 | int main() 14 | { 15 | TimerScheduler s(2); 16 | s.start(); 17 | 18 | std::cout << "Starting single timers at " << systemClock::now() << std::endl; 19 | 20 | s.scheduleSingle("1", s_t(3), []() 21 | { 22 | std::cout << std::endl << "Executing first ST at " << systemClock::now() << std::endl; 23 | } 24 | ); 25 | s.scheduleSingle("2", s_t(4), []() 26 | { 27 | std::cout << std::endl << "Executing second ST at " << systemClock::now() << std::endl; 28 | std::this_thread::sleep_for(s_t(7)); 29 | } 30 | ); 31 | s.scheduleSingle("3", s_t(5), [](int a, double b) 32 | { 33 | std::cout << std::endl << "Executing third ST with params " << a << " and " << b << " at " << systemClock::now() << std::endl; 34 | }, 35 | 5, 23 36 | ); 37 | 38 | std::this_thread::sleep_for(s_t(15)); // wait the single timers 39 | 40 | ////////////////////////////////////////////////// 41 | 42 | std::cout << std::endl << "Starting repeating timers at " << systemClock::now() << std::endl; 43 | 44 | s.scheduleRepeat("4", TimerScheduler::IntervalReps{ s_t(2), 3 }, []() {std::cout << std::endl << "Executing first RT at " << systemClock::now() << std::endl; }); 45 | s.scheduleRepeat("5", TimerScheduler::IntervalReps{ s_t(2), 4 }, []() {std::cout << std::endl << "Executing second RT at " << systemClock::now() << std::endl; }); 46 | s.scheduleRepeat("6", TimerScheduler::IntervalReps{ s_t(2), 5 }, [](int a, int b) {std::cout << std::endl << "Executing third RT at " << systemClock::now() << std::endl; }, 2, 222); 47 | s.scheduleRepeat("7", TimerScheduler::IntervalReps{ s_t(2) }, []() {std::cout << std::endl << "Executing unbounded RT " << systemClock::now() << std::endl; }); 48 | 49 | std::this_thread::sleep_for(s_t(20)); // wait the repeating timers to be executed several times 50 | 51 | std::cout << std::endl << "Updating the interval of the unbounded repeating timer from 2 to 4 secs" << std::endl; 52 | s.updateInterval("7", s_t(4)); 53 | 54 | std::this_thread::sleep_for(s_t(15)); // wait the unbounded timer to be executed several times with its new interval 55 | 56 | std::cout << std::endl << "Cancelling the unbounded repeating timer" << std::endl; 57 | s.cancelTimer("7"); 58 | 59 | std::cout << std::endl << "Currently scheduled timers (0 is expected): " << s.currentlyScheduled() << std::endl; 60 | std::cout << std::endl << "Waiting some time to see that the canceled timer won't be executed again..." << std::endl; 61 | std::this_thread::sleep_for(s_t(10)); 62 | 63 | // We can also schedule a single timer and get its result when it's ready 64 | std::cout << std::endl << "Starting single timer" << std::endl; 65 | auto res = s.scheduleSingle("1", s_t(0), [](int param) { return sqrt(param); }, 121); 66 | std::cout << std::endl << "Getting its result... " << res.get() << std::endl; 67 | 68 | return 0; 69 | } -------------------------------------------------------------------------------- /Advanced-Timer_Scheduler/SchedulableTask.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "InvokableTask.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | class SchedulableTask 10 | { 11 | public: 12 | template 13 | explicit SchedulableTask(FunctionType&& f) 14 | : m_task{ std::make_shared(std::forward(f)) } 15 | {} 16 | 17 | template 18 | explicit SchedulableTask(FunctionType&& f, size_t hash) 19 | : m_task{ std::make_shared(std::forward(f)) } 20 | , m_hash{ hash } 21 | {} 22 | 23 | template 24 | explicit SchedulableTask(FunctionType&& f, std::chrono::steady_clock::duration interval, size_t maxReps) 25 | : m_task{ std::make_shared(std::forward(f)) } 26 | , m_interval{ interval } 27 | { 28 | if (maxReps > 0) 29 | { 30 | m_maxReps = maxReps; 31 | } 32 | } 33 | 34 | template 35 | explicit SchedulableTask(FunctionType&& f, size_t hash, std::chrono::steady_clock::duration interval, size_t maxReps) 36 | : m_task{ std::make_shared(std::forward(f)) } 37 | , m_hash{ hash } 38 | , m_interval{ interval } 39 | { 40 | if (maxReps > 0) 41 | { 42 | m_maxReps = maxReps; 43 | } 44 | } 45 | 46 | SchedulableTask(const SchedulableTask&) = delete; 47 | SchedulableTask& operator=(const SchedulableTask&) = delete; 48 | 49 | SchedulableTask(SchedulableTask&& other) noexcept 50 | : m_task{ std::move(other.m_task) } 51 | , m_isEnabled{ std::move(other.m_isEnabled) } 52 | , m_interval{ std::move(other.m_interval) } 53 | , m_maxReps{ std::move(other.m_maxReps) } 54 | , m_hash{ std::move(other.m_hash) } 55 | { 56 | } 57 | 58 | std::shared_ptr clone() 59 | { 60 | return std::shared_ptr(new SchedulableTask(this)); 61 | } 62 | 63 | void invoke() 64 | { 65 | setEnabled(false); 66 | m_task->invoke(); 67 | setEnabled(true); 68 | } 69 | 70 | void setEnabled(bool isEnabled) 71 | { 72 | *m_isEnabled = isEnabled; 73 | } 74 | bool isEnabled() const 75 | { 76 | return *m_isEnabled; 77 | } 78 | 79 | void setInterval(std::chrono::steady_clock::duration interval) 80 | { 81 | m_interval = interval; 82 | } 83 | std::optional interval() const 84 | { 85 | return m_interval; 86 | } 87 | 88 | void decrementMaxReps() 89 | { 90 | if (m_maxReps == 0) 91 | { 92 | return; 93 | } 94 | m_maxReps = m_maxReps.value() - 1; 95 | } 96 | std::optional maxReps() const 97 | { 98 | return m_maxReps; 99 | } 100 | 101 | std::optional hash() const 102 | { 103 | return m_hash; 104 | } 105 | 106 | private: 107 | std::shared_ptr m_task; 108 | std::shared_ptr m_isEnabled = std::make_shared(true); 109 | std::optional m_interval; 110 | std::optional m_maxReps; 111 | std::optional m_hash; 112 | 113 | SchedulableTask(SchedulableTask* st) 114 | : m_task{ st->m_task } 115 | , m_isEnabled{ st->m_isEnabled } 116 | , m_interval{ st->m_interval } 117 | , m_maxReps{ st->m_maxReps } 118 | , m_hash{ st->m_hash } 119 | {} 120 | }; -------------------------------------------------------------------------------- /Sem_08/README.md: -------------------------------------------------------------------------------- 1 | # Динамично програмиране 2 | 3 | ### Задача 1: 4 | Студент се намира пред стълба с n стъпала. Той се изкачва по стълбата, като на всяка крачка взима или по едно, или по две стъпала. Съставете бърз алгоритъм, който пресмята по колко начина студентът може да се изкачи по стълбата. 5 | 6 | ### Решение (fibonacci.cpp (recursive/memoization/DP)): 7 | Нека X_n е броят на начините за изкачване на стълба с n стъпала. Лесно се вижда, че X_1 = 1, X_2 = 2. С последната крачка студентът изкачва или едно, или две стъпала. Ето защо X_k = X_{k−1} + X_{k−2}, k ≥ 3. Чрез това уравнение пресмятаме X_k за k от 3 до n и връщаме X_n (получават се **числа на Фибоначи**). Този алгоритъм има **времева сложност O(n)**. 8 | 9 | ### Memoization 10 | ![alt_text](https://i.ibb.co/rdn7x4x/FibMem.png) 11 | 12 | ### Dynamic programming 13 | ![alt_text](https://i.ibb.co/XYpz54P/FibDP.png) 14 | 15 | --- 16 | 17 | ### Задача 2: 18 | Крадец влиза в картинна галерия с n картини с цени съответно a_1, a_2, .., a_n лева. Картините са подредени последователно и крадецът знае за повреда в алармената система, която се активира, ако бъдат откраднати две съседни картини, но не се активира, ако бъде открадната картина, без да се пипат съседните. 19 | 1. Предложете бърз алгоритъм, **изчисляващ цената на най-скъпата колекция, която крадецът може да отмъкне безнаказано**. 20 | 2. Разширете алгоритъма така, че **да изчисли кои картини да вземе крадецът**. 21 | 22 | ### Решение (Task02.cpp): 23 | **Идея на алгоритъма:** При изчисляването на maxProfits[k] има две възможности: крадецът да вземе или да не вземе k-тата картина. Ако той реши да не вземе k-тата картина, тогава максималната му печалба е колкото от първите k-1 картини, т.е. maxProfits[k-1]. Ако крадецът вземе k-тата картина, то максималната му печалба е колкото от първите k-2 картини, т.е. maxProfits[k-2], плюс стойността на k-тата картина, т.е. paintingsPrices[k]. Тук имаме k-2, а не k-1, тъй като, щом крадецът е решил да вземе k-тата картина, той трябва да се откаже от (k-1)-та. 24 | 25 | Количеството допълнителна памет, използвано от алгоритъма, може да се оптимизира: 26 | елементите на масива maxProfits не са нужни през цялото време на изпълнение на алгоритъма; 27 | достатъчно е да пазим три последователни елемента, а именно maxProfits[k-2], maxProfits[k-1], maxProfits[k]. Това **намалява на порядък сложността по памет**. 28 | 29 | --- 30 | 31 | ### Задача 3: 32 | Дадени са три масива A[1..n], B[1..n] и C[1..n]. 33 | Предложете алгоритъм, който **намира най- голямата стойност на сума от n събираеми**, която може да се образува като се спазват следните правила: 34 | 1. k-тото събираемо е A[k], B[k] или C[k]; 35 | 2. не може да има поредни събираеми от един и същи масив (ако например k-тото събираемо е A[k], то (k+1)-то събираемо е или B[k+1], или C[k+1], но не и A[k+1]). 36 | 37 | Алгоритъмът да работи за **време O(n)** и **памет O(n)**. 38 | **Оптимизирайте паметта до константен брой променливи** от целочислен тип. 39 | 40 | ### Решение (Task03.cpp): 41 | **Демонстрация на алгоритъма** при: 42 | A = { 2, 3, 5, 1, 8, 7, 6 }; 43 | B = { 6, 9, 8, 0, 5, 9, 7 }; 44 | C = { 4, 1, 2, 8, 4, 2, 5 }: 45 | 46 | ![alt_text](https://i.ibb.co/nCNr7Zn/DP.png) 47 | 48 | --- 49 | 50 | ### Задача 4: 51 | Да се намери броя на наредените n-орки (x_1, x_2, .., x_n), състоящи се от цели неотрицателни числа, които са решения на уравнението A_1\*x_1 + A_2\*x_2 + ... + A_n\*x_n = B, по дадени цели положителни числа A_1, A_2, .., A_n и B. 52 | Предложете итеративен алгоритъм, който работи за **време O(n\*B)** и **памет O(n\*B)**. 53 | 54 | ### Решение (Task04.cpp): 55 | **Демонстрация на алгоритъма** при: 56 | A = { 2, 3, 5 }; 57 | B = 9: 58 | 59 | ![alt_text](https://i.ibb.co/tLGmKLS/DP-2.png) 60 | 61 | ### Задача 5: 62 | Да се напише функция **coinChange**, която приема два параметъра – масив от номинали и стойност. Функцията трябва да върне броя начини, по които можем да получим дадената стойност, използвайки наличните номинали от масива. 63 | 64 | **Примери:** 65 | ```js 66 | const denominations = [1, 5, 10, 25] 67 | 68 | coinChange(denominations, 1) // 1 69 | coinChange(denominations, 2) // 1 70 | coinChange(denominations, 5) // 2 71 | coinChange(denominations, 10) // 4 72 | coinChange(denominations, 25) // 13 73 | coinChange(denominations, 45) // 39 74 | coinChange(denominations, 100) // 242 75 | coinChange(denominations, 145) // 622 76 | coinChange(denominations, 1451) // 425663 77 | coinChange(denominations, 14511) // 409222339 78 | ``` 79 | -------------------------------------------------------------------------------- /Sem_10/README.md: -------------------------------------------------------------------------------- 1 | ## Алгортими върху тегловни графи 2 | - Dijkstra's algorithm 3 | - Bellman–Ford 4 | - Floyd–Warshall 5 | - Prim's algorithm 6 | - Kruskal's algorithm 7 | 8 | ## Задачи 9 | ### Задача 1: 10 | Някои от компютрите в дадена мрежа са свързани с ненадеждни връзки: поради шума съобщенията могат да бъдат предадени грешно. За всяка връзка между два компютъра е дадена нейната надеждност: число между 0 и 1, показващо вероятността за правилно предаване на съобщение. За два дадени компютъра s и t **да се определи най-надеждният път между тях**. 11 | 12 | ### Решение: 13 | Разглеждаме граф, чиито **върхове са компютрите**, **ребрата са връзките между компютрите**, а **теглата на ребрата са надеждностите на връзките**. 14 | Използваме **алгоритъма на Дейкстра**. 15 | Неговата времева сложност е O(m + n)*lgn при най-лоши входни данни, ако използваме приоритетна опашка, реализирана чрез пирамида на Фибоначи. 16 | 17 | Директно прилагане на алгоритъма на Дейкстра не ни върши работа, защото дължината на път = сбора от дължините на ребрата, а надеждностите се умножават, вместо да се събират! 18 | 19 | Пример: ако едно съобщение се предава два пъти, първия път - с надеждност 90%, а втория път - с надеждност 80%, то в крайна сметка общата надеждност е 0.8 * 0.9 = 0.72 = 72%. 20 | 21 | За да заменим умножението със събиране, логаритмуваме надеждностите при произволна (но една и съща) основа > 1, например при основа e. 22 | 23 | Възникват два проблема: 24 | - алгоритъмът на Дейкстра работи само с неотрицателни тегла, 25 | а логаритмите на числа между 0 и 1 са отрицателни; 26 | - алгоритъмът на Дейкстра търси най-къс път, т.е. дължина = min, а ние търсим най-надежден път, т.е. надеждност = max <=> ln(надеждност) = max. 27 | 28 | Двата проблема се решават едновременно: сменяме знаците на логаритмите. Тоест обхождаме графа и всяка надеждност p заменяме с –ln p. След това прилагаме алгоритъма на Дейкстра. Първоначалното обхождане е с линейна времева сложност, която е пренебрежимо малка спрямо времето на алгоритъма на Дейкстра. 29 | 30 | ( Вместо първо да логаритмуваме при основа A > 1, а после да сменяме знака, можем направо да логаритмуваме при основа 1/A < 1. ) 31 | 32 | --- 33 | 34 | ### Задача 2: 35 | Знаем курсовете за обмен между различни двойки валути. Искаме да "завъртим" някакво количество от някаква валута S така, че накрая да имаме по-голямо количество от същата валута. 36 | Съставете бърз алгоритъм, който проверява дали това е възможно: 37 | - ако валутата S е точно определена (тоест отначало разполагаме само с нея); 38 | - ако валутата S не е дадена по условие (тоест разполагаме с всевъзможни валути). 39 | 40 | ### Решение: 41 | Разглеждаме ориентиран граф, чиито **върхове са валутите**. Между всеки два върха има по едно ребро в двете посоки. **Теглата на ребрата са валутните курсове**. По-точно, тегло w на реброто от валута X към валута Y означава, че можем да продадем една единица от X и срещу нея да получим w единици от Y. 42 | 43 | При движение по графа теглата на ребрата се умножават: ако сме заменили една единица от X за w1 единици от Y, а всяка от тях - за w2 единици от Z, в крайна сметка за една единица от X сме получили w_1 * w_2 единици от Z. Теглата са положителни реални числа - цели или дробни. Те могат да бъдат както по-големи, така и по-малки от 1. 44 | 45 | **Търсим печеливш цикъл, тоест цикъл с тегло > 1.** 46 | 47 | Както в предишната задача, логаритмуваме валутните курсове (така заменяме умножението със събиране) и обръщаме знаците на логаритмите ( или избираме основа за логаритмуване между 0 и 1). 48 | 49 | Сега вече графът може да съдържа не само положителни тегла, но също отрицателни и нулеви тегла. Търсим цикъл с тегло < 0. Можем да използваме **алгоритъма на Белман-Форд или алгоритъма на Флойд-Уоршал**. 50 | 51 | - Ако цикълът трябва да минава през определен връх S, пускаме алгоритъма на Белман-Форд от върха S. В алгоритъма има проверка специално за отрицателен цикъл през началния връх S. Избираме алгоритъма на Белман—Форд, защото е по-бърз от алгоритъма на Флойд-Уоршал. 52 | - Ако търсим отрицателен цикъл където и да било в графа, използваме алгоритъма на Флойд-Уоршал, защото той е по-бърз от n-кратно изпълнение на алгоритъма на Белман-Форд. 53 | 54 | --- 55 | 56 | ### Задача 3 (Изпит): 57 | Конструирайте ефикасен алгоритъм, който получава като вход неориентиран свързан тегловен граф G и някакво ребро e ∈ E и връща **дали съществува минимално покриващо дърво на G, което съдържа e**. 58 | 59 | ![alt_text](https://i.ibb.co/1T8FRhS/MST.jpg) 60 | 61 | **Пример:** 62 | *Вход: 2 5, Изход: Истина* 63 | *Вход: 1 7, Изход: Лъжа* 64 | 65 | ### Решение: 66 | Построяваме граф G' = (V, E'), където E' са всички ребра от E с тегла по-малки от w(e), където 'e' е реброто от входа. Това очевидно може да стане във време O(|V| + |E|). След това намираме свързаните компоненти на G' във време O(|V| + |E'|). Нека краищата на реброто 'e' са върхове u и v. Ако u и v са в различни свързани компоненти на G', връщаме ДА, в противен случай - връщаме НЕ. Коректността следва директно от МПД-теоремата. Сложността по време е O(|V| + |E|). 67 | -------------------------------------------------------------------------------- /Sem_07/README.md: -------------------------------------------------------------------------------- 1 | ## Сортиране в линейно време. Задачи, използващи идеята на CountingSort. 2 | 3 | ### Задача (Семестриално контролно 2022г.) 4 | Представете си игра между X и Y, в която Y разполага с n естествени числа a_1, ..., a_n. Известно е, че съществуват l, m ∈ N+, такива че l < m и ∀i ∈ [1, n] ∶ a_i ∈ [l, m]. X знае l и m, но не знае числата a_1, ..., a_n. Играта се състои в това X да задава въпроси на Y от вида **"Колко числа измежду a_1, ..., a_n са по-големи или равни на a и по-малки или равни на b"**. Може да мислите, че въпросите на X се състоят само от наредени двойки (a, b), където l ≤ a < b ≤ m. Y трябва да отговори коректно на всяко запитване на X. От вас се иска да намерите алгоритъм за Y, който по наредена двойка (a, b) отговаря във време **O(1)** на въпроса на X. На Y е разрешено да извърши предварителна обработка на данните във време **O(n + (m − l))**. 5 | Опишете предварителната обработка, която Y извършва. 6 | Опишете алгоритъма, по който Y отговаря на X в константно време. 7 | 8 | ### Задача 1 (Първо липсващо число): 9 | Даден е масив от n цели положителни числа, които могат да са много големи. Предложете алгоритъм, който **намира първото цяло положително число, липсващо в масива**. 10 | 11 | ### Решение: 12 | Очевидно отговорът е някое от числата 1, 2, 3, ..., n+1. 13 | 14 | Тривиалният алгоритъм проверява първо дали масивът съдържа 1, после 2 и т.н. до n. Всяка проверка изисква линейно време. В най-лошия случай (когато масивът съдържа всички числа от 1 до n) се правят n проверки. Следователно **времевата сложност на този алгоритъм е O(n^2 )**. 15 | 16 | Второ възможно решение е да сортираме масива за време O(nlgn), a после да търсим първото липсващо число за линейно време: 17 | 18 | ![alt_text](https://i.ibb.co/tZLCr2r/first-Missing-Number-Naive.png) 19 | 20 | **Времевата сложност на този алгоритъм е O(nlgn) + O(n) = O(nlgn)**, което е по-добре от предишния алгоритъм, но все още не е най-бързо. 21 | 22 | Тъй като първото липсващо число е от 1 до n+1, можем да приложим **модификация на CountingSort**, работеща в линейно време. (Оригиналният вариант е неприложим, понеже числата в масива могат да са много големи). 23 | Това решение (Task01_not-in-place.cpp) очевидно работи **в линейно време O(n)** и ползва **допълнителна памет** (за масива C) **в размер O(n)**, при това без да променя оригиналния масив. Ако такава промяна е допустима, то можем да спестим паметта за масива C и да сведем допълнителната памет до константен брой променливи от примитивен тип (напр. броячи) - Task01_in-place.cpp. 24 | 25 | ### Задача 2: 26 | Даден е масив от n цели числа. Предложете алгоритъм, който **намира индексите на две числа от масива, чиято разлика се дели на 3n** (ако няма такива, да се отпечата подходящо съобщение). Алгоритъмът да работи за **време O(n)**. 27 | 28 | ### Решение: 29 | Отново ще използваме идеята за броене на CountingSort. Ще броим не самите числа, а **остатъците им при деление на 3n** 30 | (защото разликата на две числа се дели на 3n <=> те дават равни остатъци при деление на 3n). 31 | 32 | Истинските бройки (колко точно пъти се среща всеки остатък) не ни интересуват. **За всеки остатък се интересуваме само дали се среща поне два пъти**. Ако да - при повторно срещане се интересуваме къде е било първото му срещане (първото и второто срещане са индексите, които търсим). Ето защо в помощния масив ще пазим не броячи, а позиции (индекси). Тъй като даден остатък може да не се среща изобщо, ще инициализираме помощния масив с невалидна стойност (например -1). 33 | 34 | ## Въведение в динамичното програмиране. 35 | 36 | **Динамичното програмиране** се използва в задачи, при които за всеки достатъчно голям вход, решението на задачата се свежда до решения върху подзадачи, които обаче **не са независими, а имат общи подподзадачи**. 37 | При тези задачи може да измислим алгоритъм, който не е рекурсивен, а **работи от долу нагоре** и съхранява решенията за общите подподзадачи, при което отпада преизчисляването на едни и същи решения отново и отново и отново. В това е същността на схемата Динамично Програмиране. 38 | 39 | ### Задача 3: 40 | Разполагаме с калкулатор, който може да изпълнява следните операции: 41 | 42 | - Умножение на число X по 2 43 | - Умножение на число X по 3 44 | - Събиране на число X с 1-ца 45 | 46 | Дадено е цяло положително число n. Предложете алгоритъм, **който пресмята най- малкия брой операции, които трябва да изпълним, за да получим числото n** (започваме прилагането на операциите от числото 1). 47 | 48 | **Пример:** 49 | *Вход: 962340 50 | Изход: 17* 51 | 52 | Обяснение към примера: 53 | 1 -> 3 -> 9 -> 27 -> 54 -> 55 -> 165 -> 495 -> 1485 -> 4455 -> 8910 -> 17820 -> 17821 -> 53463 -> 160389 -> 160390 -> 481170 -> 962340 54 | 55 | ### Решение: 56 | **Рекурсивна декомпозиция:** 57 | minOperations(1) = 0 58 | minOperations(n) = 1 + min{ minOperations(n-1), minOperations(n/2), minOperations(n/3) } 59 | 60 | Имаме ли припокриване (общи подподзадачи, които преизчисляваме) ? 61 | Да. 62 | Следователно решаваме задачата по схемата Динамично програмиране. 63 | 64 | DP[0...n]: 65 | DP[i] - минималният брой операции, с които можем да получим числото i. 66 | DP[i] = 1 + min{ DP[n-1], DP[i/2] (ако i е четно), DP[i/3] (ако i се дели на 3) } 67 | Резултатът ще се пази в DP[n]. 68 | -------------------------------------------------------------------------------- /Sem_11/README.md: -------------------------------------------------------------------------------- 1 | # Алчни алгоритми (greedy algorithms) 2 | 3 | Изчислителните задачи, които алчните алгоритми решават, са оптимизационни задачи. Например, задачата за намиране на минимално покриващо дърво е именно оптимизационна. 4 | 5 | При алчните алгоритми се прави поредица от **локално оптимални избори**, която води до **глобално оптимално решение**. 6 | 7 | При някои задачи (например МПД) локалната алчност е печеливша стратегия. Има обаче задачи, в които проявата на локална алчност води до решение, което не е оптимално. Всички NP-трудни задачи са такива. 8 | 9 | ## Задачи 10 | 11 | ### Задача 1 (Activity Selection Problem): 12 | Дадено е множество от мероприятия, всяко от които се характеризира с начален час и краен час. Съставете алгоритъм, който избира **възможно най-голям брой мероприятия, така че никои две от тях да не се застъпват** (допуска се краят на едното да съвпада с началото на следващото). 13 | 14 | ### Решение: 15 | **Пример:** Нека мероприятията се провеждат в следните времеви интервали: **[0 ; 13]**, **[2 ; 5]**, **[4 ; 6]**, **[5 ; 9]**, **[8 ; 14]**, **[11 ; 12]**, **[15 ; 20]**. 16 | 17 | Алчната стратегия се състои в това да вземем първото възможно мероприятие (за да можем след него да вземем максимален брой). Трябва да уточним обаче в какъв ред разглеждаме мероприятията, т.е. най-напред трябва да извършим подходящо сортиране. 18 | 19 | Трябва да **сортираме мероприятията по техните краища** и после вече да прилагаме алчна стратегия (т.е. всеки път да добавяме към нашия списък първото мероприятие, което не се застъпва с никое от досега избраните). В конкретния пример резултатът е [2 ; 5], [5 ; 9], [11 ; 12], [15 ; 20], т.е. четири мероприятия, което е максималният възможен брой. 20 | 21 | **Коректност на алгоритъма:** 22 | Първо ще докажем, че най-рано завършващото мероприятие непременно е в поне едно от оптималните решения. Избираме произволно оптимално множество. Нека първото (по краен час) мероприятие в това множество не е първото от всички мероприятия. Тогава заменяме първото мероприятие на множеството с най-рано завършващото от всички мероприятия. След тази замяна крайният час на първото мероприятие на множеството ще стане по-ранен. Значи, това мероприятие ще продължава да не се застъпва с останалите мероприятия в множеството (които не се застъпват едно с друго). Следователно новото множество пак е оптимално, защото то съдържа същия (максималният) брой две по две незастъпващи се мероприятия. Обаче новото оптимално множество вече съдържа най-рано завършващото от всички мероприятия. 23 | 24 | Ето защо няма да сбъркаме, ако, конструирайки оптимално множество стъпка по стъпка, вземем най-рано завършващото мероприятие на първата стъпка от алгоритъма. Тогава всички други мероприятия, които ще вземем, или завършват преди неговото начало, или започват след неговия край (за да не се застъпват с него). Първият вариант е невъзможен: иначе най-рано завършващото мероприятие не би било такова. Остава другият вариант. Затова отсяваме оставащите мероприятия, като премахваме всички, започващи преди края на най-рано завършващото мероприятие. От останалите трябва да вземем колкото може повече, т.е. решаваме същата задача за останалите мероприятия. По същата логика няма да сбъркаме (т.е. ще стигнем до оптимално решение), ако пак вземем най-рано завършващото от останалите мероприятия и т.н. 25 | 26 | Този алчен алгоритъм изисква време **O(nlgn)** заради сортирането, където n е броят на мероприятията. (Вторият етап — построяването на максимално множество, след като масивът е сортиран — се изпълнява за линейно време.) 27 | 28 | --- 29 | 30 | ### Задача 2: 31 | Даден е стринг с дължина 2n, състоящ се от символите '[' и ']'. Стринг е балансиран <=> може да бъде представен във вида s1[s2], където s1 и s2 са балансирани стрингове. Намерете минималния брой размени, с които можем да балансираме подаден стринг. 32 | 33 | --- 34 | 35 | ### Задача 3: 36 | Дадени са банкноти с номинал 1, 7 и 10. 37 | Съставете алгоритъм, който намира най-малкия брой банкноти, с които може да се изплати сумата n точно (без ресто). 38 | 39 | ### Решение: 40 | **Наблюдение 1:** Няма смисъл да използваме повече от две банкноти с номинал 7, защото три банкноти с номинал 7 могат да се заменят с две банкноти с номинал 10 и една банкнота с номинал 1 : 3\*7 = 21 = 2\*10 + 1\*1. Така едно оптимално решение (с три банкноти) се заменя с друго оптимално решение (със същия брой банкноти). Ограничавайки до 0, 1 или 2 броя на банкнотите с номинал 7, губим някои от оптималните решения, но не всички. Това е допустимо, понеже търсим едно оптимално решение, а не всички такива. 41 | 42 | **Наблюдение 2:** Можем да ограничим и броя на банкнотите с номинал 1. Броят им е не повече от шест (защото седем банкноти с номинал 1 могат да се заменят с една банкнота с номинал 7), и то само ако няма банкноти с номинал 7. А ако има, то броят на банкнотите с номинал 1 е максимум две (защото три банкноти с номинал 1 и една с номинал 7 могат да се заменят с една банкнота с номинал 10). 43 | 44 | Накратко: Ако cnt1 и cnt7 са съответно броят на банкнотите с номинал 1 и номинал 7, то за всяко n съществува оптимално решение измежду следните наредени двойки (cnt7; cnt1): 45 | **(0; 0), (0; 1), (0; 2), (0; 3), (0; 4), (0; 5), (0; 6); (1; 0), (1; 1), (1; 2); (2; 0), (2; 1), (2; 2)**. 46 | 47 | За да решим задачата, ще приложим **модификация** на следния алчен алгоритъм: 48 | - Използваме колкото може повече банкноти с номинал 10. 49 | - Като остане сума, по-малка от 10, използваме (ако може) една банкнота с номинал 7. 50 | - Остатъка (ако има такъв) изплащаме с банкноти с номинал 1. 51 | 52 | :bangbang: Директно приложен алчният алгоритъм не би работил коректно. 53 | ***Пример:*** За n = 15 алчният алгоритъм намира представяне с шест банкноти (10+1+1+1+1+1 = 15) вместо с минималния брой три (7+7+1 = 15). 54 | 55 | Нека разгледаме следната таблица (само със сумите от 0 до 16 поради горните две наблюдения): 56 | 57 | ![alt_text](https://i.ibb.co/LgBb0Hj/nominals.png) 58 | 59 | Не е трудно да се види, че от случаите, изброени в таблицата, алчният алгоритъм греши само при n = 14, 15 и 16. Понеже използва банкноти с номинал 10 докогато е възможно, той греши при всички стойности на n, завършващи на 4, 5 или 6 (и различни от 4, 5 и 6). В останалите случаи алчният алгоритъм работи коректно. Следователно алчният алгоритъм може да бъде поправен така: ако числото n завършва на някоя от цифрите 4, 5 и 6 (и е различно от 4, 5 и 6), то трябва да използваме една банкнота по-малко с номинал 10, а вместо нея си служим с две банкноти с номинал 7. Остатъка (ако има) изплащаме с банкноти с номинал 1. 60 | 61 | ![alt_text](https://i.ibb.co/jRBrRKW/nominals-Code.png) 62 | 63 | 1) Когато n завършва на 0, 1, 2 или 3, няма поправка: използваме колкото може повече банкноти с номинал 10. В тези случаи остатъкът е по-малък от 7, затова се изплаща с банкноти с номинал 1. 64 | ***Примери:*** 65 | - 253 = 250 + 3 = 25 × 10 + 3 × 1 — общо 28 банкноти. 66 | - 461 = 460 + 1 = 46 × 10 + 1 × 1 — общо 47 банкноти. 67 | --- 68 | 2) Когато n завършва на 7, 8 или 9, пак няма поправка: използваме колкото може повече банкноти с номинал 10. Сега остатъкът е поне 7, затова освен банкнотите с номинал 1 има и една с номинал 7. 69 | ***Примери:*** 70 | - 519 = 510 + 9 = 51 × 10 + 1 × 7 + 2 × 1 — общо 54 банкноти. 71 | - 367 = 360 + 7 = 36 × 10 + 1 × 7 — общо 37 банкноти. 72 | --- 73 | 3) Когато n завършва на 4, 5 или 6, има поправка: банкнотите с номинал 10 са с една по -малко. Вместо нея използваме две банкноти с номинал 7. Евентуалния остатък изплащаме с банкноти с номинал 1. 74 | ***Примери:*** 75 | - 106 = 90 + 16 = 9 × 10 + 2 × 7 + 2 × 1 — общо 13 банкноти. 76 | - 875 = 860 + 15 = 86 × 10 + 2 × 7 + 1 × 1 — общо 89 банкноти. 77 | -------------------------------------------------------------------------------- /Sem_10/GraphAlgorithms/GraphAlgorithms_Pt2.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "UnionFind.h" 7 | 8 | using Edge = std::tuple; 9 | void printEdge(const Edge& e) 10 | { 11 | size_t start = std::get<0>(e); 12 | size_t end = std::get<1>(e); 13 | int weight = std::get<2>(e); 14 | 15 | std::cout << start << ' ' << end << ' ' << weight << std::endl; 16 | } 17 | 18 | class Graph 19 | { 20 | private: 21 | std::vector>> adj; 22 | size_t verticesCount; 23 | 24 | bool oriented; 25 | 26 | public: 27 | Graph(size_t verticesCount, bool oriented); 28 | void addEdge(size_t start, size_t end, int weight); 29 | 30 | void Dijkstra(size_t start, std::vector& distances) const; // O((n+m)*lgn) 31 | void BellmanFord(size_t start, std::vector& distances) const; // O(n*(n+m)) 32 | void FloydWarshall(std::vector>& distances) const; // O(n^3) 33 | 34 | int Prim(std::vector& mst) const; // O((n+m)*lgn) 35 | int Kruskal(std::vector& mst) const; // O(mlgm) 36 | }; 37 | 38 | void Graph::Dijkstra(size_t start, std::vector& distances) const 39 | { 40 | distances.resize(verticesCount, SIZE_MAX); 41 | 42 | struct VertexDistancePair 43 | { 44 | size_t vertex; 45 | size_t distFromStart; 46 | 47 | bool operator<(const VertexDistancePair& other) const 48 | { 49 | return distFromStart > other.distFromStart; 50 | } 51 | }; 52 | 53 | std::priority_queue q; 54 | 55 | distances[start] = 0; 56 | q.push({ start, 0 }); 57 | 58 | while (!q.empty()) 59 | { 60 | VertexDistancePair current = q.top(); 61 | q.pop(); 62 | 63 | size_t currentVertex = current.vertex; 64 | for (auto it = adj[currentVertex].begin(); it != adj[currentVertex].end(); ++it) 65 | { 66 | size_t currentSuccessor = (*it).first; 67 | size_t currentWeight = (*it).second; 68 | 69 | if (distances[currentVertex] + currentWeight < distances[currentSuccessor]) 70 | { 71 | distances[currentSuccessor] = distances[currentVertex] + currentWeight; 72 | q.push({ currentSuccessor, distances[currentSuccessor] }); 73 | } 74 | } 75 | } 76 | } 77 | void Graph::BellmanFord(size_t start, std::vector& distances) const 78 | { 79 | distances.resize(verticesCount, INT_MAX); 80 | distances[start] = 0; 81 | 82 | bool hasChanged = true; 83 | for (size_t t = 0; t < verticesCount - 1; t++) 84 | { 85 | if (!hasChanged) 86 | break; 87 | hasChanged = false; 88 | 89 | for (size_t i = 0; i < verticesCount; i++) 90 | { 91 | if (distances[i] == INT_MAX) 92 | continue; 93 | 94 | for (auto it = adj[i].begin(); it != adj[i].end(); ++it) 95 | { 96 | if (distances[i] + (*it).second < distances[(*it).first]) 97 | { 98 | distances[(*it).first] = distances[i] + (*it).second; 99 | hasChanged = true; 100 | } 101 | } 102 | } 103 | } 104 | 105 | for (size_t i = 0; i < verticesCount; i++) // check for a negative cycle 106 | { 107 | for (auto it = adj[i].begin(); it != adj[i].end(); ++it) 108 | { 109 | if ((distances[i] != INT_MAX) && ((distances[i] + (*it).second) < distances[(*it).first])) 110 | throw "Negative cycle detected!"; 111 | } 112 | } 113 | } 114 | void Graph::FloydWarshall(std::vector>& distances) const 115 | { 116 | distances.resize(verticesCount, std::vector(verticesCount, INT_MAX)); 117 | 118 | // mark all edges 119 | for (size_t i = 0; i < verticesCount; i++) 120 | { 121 | for (auto it = adj[i].begin(); it != adj[i].end(); ++it) 122 | distances[i][(*it).first] = (*it).second; 123 | } 124 | 125 | // min distance from i to i is 0 for every vertex 126 | for (size_t i = 0; i < verticesCount; i++) 127 | distances[i][i] = 0; 128 | 129 | for (size_t k = 0; k < verticesCount; k++) // intermediate vertices 130 | { 131 | for (size_t i = 0; i < verticesCount; i++) // start vertex 132 | { 133 | for (size_t j = 0; j < verticesCount; j++) // end vertex 134 | { 135 | if ((distances[i][k] != INT_MAX && distances[k][j] != INT_MAX) && (distances[i][j] > distances[i][k] + distances[k][j])) 136 | distances[i][j] = distances[i][k] + distances[k][j]; 137 | } 138 | } 139 | } 140 | } 141 | 142 | int Graph::Prim(std::vector& mst) const 143 | { 144 | if (oriented) 145 | throw "The graph should NOT be oriented!"; 146 | 147 | int mstWeight = 0; 148 | size_t addedEdges = 0; 149 | 150 | std::vector visited(verticesCount, false); 151 | 152 | auto comp = [](const Edge& lhs, const Edge& rhs) { return std::get<2>(lhs) > std::get<2>(rhs); }; 153 | std::priority_queue, std::vector>, decltype(comp)> q(comp); 154 | 155 | q.push(std::make_tuple(0, 0, 0)); // virtual edge for the start 156 | 157 | while (addedEdges < verticesCount - 1) 158 | { 159 | auto currentEdge = q.top(); 160 | q.pop(); 161 | 162 | size_t currentEdgeStart = std::get<0>(currentEdge); 163 | size_t currentEdgeEnd = std::get<1>(currentEdge); 164 | int currentEdgeWeight = std::get<2>(currentEdge); 165 | 166 | if (visited[currentEdgeEnd]) 167 | continue; 168 | 169 | mstWeight += currentEdgeWeight; 170 | 171 | if (currentEdgeStart != currentEdgeEnd) // in order not to count the virtual edge 172 | { 173 | addedEdges++; 174 | std::cout << "Added edge: " << currentEdgeStart << "----" << currentEdgeEnd << " with weight: " << currentEdgeWeight << std::endl; 175 | mst.emplace_back(currentEdgeStart, currentEdgeEnd, currentEdgeWeight); 176 | } 177 | visited[currentEdgeEnd] = true; 178 | 179 | for (auto it = adj[currentEdgeEnd].begin(); it != adj[currentEdgeEnd].end(); ++it) 180 | q.push(std::make_tuple(currentEdgeEnd, (*it).first, (*it).second)); 181 | } 182 | 183 | return mstWeight; 184 | } 185 | int Graph::Kruskal(std::vector& mst) const 186 | { 187 | if (oriented) 188 | throw "The graph should NOT be oriented!"; 189 | 190 | size_t mstWeight = 0; 191 | size_t addedEdges = 0; 192 | 193 | std::vector edges; 194 | for (size_t i = 0; i < adj.size(); i++) 195 | for (auto it = adj[i].begin(); it != adj[i].end(); ++it) 196 | edges.emplace_back(i, (*it).first, (*it).second); 197 | 198 | std::sort(edges.begin(), edges.end(), [](const Edge& lhs, const Edge& rhs) 199 | { 200 | return std::get<2>(lhs) < std::get<2>(rhs); 201 | }); 202 | 203 | UnionFind uf(verticesCount); 204 | 205 | for (size_t i = 0; addedEdges < verticesCount - 1; i++) 206 | { 207 | auto currentEdge = edges[i]; 208 | 209 | size_t currentEdgeStart = std::get<0>(currentEdge); 210 | size_t currentEdgeEnd = std::get<1>(currentEdge); 211 | int currentEdgeWeight = std::get<2>(currentEdge); 212 | 213 | if (!uf.Union(currentEdgeStart, currentEdgeEnd)) // If edge "start---end" is added, it will form a cycle 214 | continue; 215 | 216 | std::cout << "Added edge: " << currentEdgeStart << "----" << currentEdgeEnd << " with weight: " << currentEdgeWeight << std::endl; 217 | 218 | mst.emplace_back(currentEdgeStart, currentEdgeEnd, currentEdgeWeight); 219 | mstWeight += currentEdgeWeight; 220 | addedEdges++; 221 | } 222 | 223 | return mstWeight; 224 | } 225 | 226 | 227 | Graph::Graph(size_t verticesCount, bool oriented) : adj(verticesCount), verticesCount(verticesCount), oriented(oriented) 228 | {} 229 | void Graph::addEdge(size_t start, size_t end, int weight) 230 | { 231 | adj[start].push_back(std::make_pair(end, weight)); 232 | if (!oriented) 233 | adj[end].push_back(std::make_pair(start, weight)); 234 | } 235 | 236 | 237 | int main() 238 | { 239 | 240 | } 241 | -------------------------------------------------------------------------------- /Sem_09/GraphAlgorithms_Pt1.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class Graph 9 | { 10 | private: 11 | std::vector> adj; 12 | size_t verticesCount; 13 | 14 | bool oriented; 15 | 16 | public: 17 | Graph(size_t verticesCount, bool oriented); 18 | void addEdge(size_t start, size_t end); 19 | Graph getTransposedGraph() const; 20 | 21 | private: 22 | bool DFS_containsCycle(std::vector& visited, std::vector& stack, size_t currentVertex) const; 23 | bool BFS_isBipartite(std::vector& visited, size_t start) const; 24 | void DFSrec_topologicalSorting(std::vector& visited, std::stack& currentRes, size_t currentVertex) const; 25 | 26 | void DFS_rec_SCC(const Graph& g, std::vector& visited, std::vector& lastlyVisited, size_t currentVertex) const; 27 | void DFSrec_SCC(const Graph& g, std::vector topoSort) const; 28 | 29 | public: 30 | void BFS(size_t start, size_t end, std::vector& shortestPath) const; 31 | bool isCyclic() const; 32 | bool isBipartite() const; 33 | void topologicalSorting(std::vector& topoSort) const; 34 | void SCC() const; 35 | }; 36 | 37 | void Graph::BFS(size_t start, size_t end, std::vector& shortestPath) const 38 | { 39 | if (start >= verticesCount || end >= verticesCount) 40 | throw "Invalid vertices"; 41 | if (start == end) 42 | { 43 | shortestPath.push_back(start); 44 | return; 45 | } 46 | 47 | std::queue q; 48 | std::vector prev(verticesCount); 49 | std::vector visited(verticesCount, false); 50 | 51 | q.push(start); 52 | visited[start] = true; 53 | 54 | while (!q.empty()) 55 | { 56 | size_t current = q.front(); 57 | q.pop(); 58 | 59 | for (auto it = adj[current].begin(); it != adj[current].end(); ++it) 60 | { 61 | if (visited[*it]) 62 | continue; 63 | prev[*it] = current; 64 | 65 | if (*it == end) 66 | { 67 | size_t temp = *it; 68 | while (temp != start) 69 | { 70 | shortestPath.push_back(temp); 71 | temp = prev[temp]; 72 | } 73 | shortestPath.push_back(start); 74 | std::reverse(shortestPath.begin(), shortestPath.end()); 75 | return; 76 | } 77 | 78 | q.push(*it); 79 | visited[*it] = true; 80 | } 81 | } 82 | } 83 | 84 | bool Graph::DFS_containsCycle(std::vector& visited, std::vector& stack, size_t currentVertex) const 85 | { 86 | visited[currentVertex] = true; 87 | stack[currentVertex] = true; 88 | 89 | for (auto it = adj[currentVertex].begin(); it != adj[currentVertex].end(); ++it) 90 | if (stack[*it] || DFS_containsCycle(visited, stack, *it)) 91 | return true; 92 | 93 | stack[currentVertex] = false; 94 | return false; 95 | } 96 | bool Graph::isCyclic() const 97 | { 98 | if (!oriented) 99 | throw "The graph should be oriented!"; 100 | 101 | std::vector visited(verticesCount, false); 102 | std::vector stack(verticesCount, false); 103 | 104 | for (size_t i = 0; i < visited.size(); i++) 105 | if (!visited[i] && DFS_containsCycle(visited, stack, i)) 106 | return true; 107 | 108 | return false; 109 | } 110 | 111 | bool Graph::BFS_isBipartite(std::vector& visited, size_t start) const 112 | { 113 | std::queue q; 114 | q.push(start); 115 | visited[start] = true; 116 | 117 | while (!q.empty()) 118 | { 119 | size_t current = q.front(); 120 | q.pop(); 121 | 122 | size_t currentColour = visited[current]; 123 | 124 | for (auto it = adj[current].begin(); it != adj[current].end(); it++) 125 | { 126 | if (visited[*it] == currentColour) 127 | return false; 128 | 129 | if (visited[*it]) 130 | continue; 131 | 132 | q.push(*it); 133 | visited[*it] = currentColour == 1 ? 2 : 1; 134 | } 135 | } 136 | 137 | return true; 138 | } 139 | bool Graph::isBipartite() const 140 | { 141 | if (oriented) 142 | throw "The graph should NOT be oriented!"; 143 | 144 | std::vector visited(verticesCount, 0); 145 | 146 | for (size_t i = 0; i < visited.size(); i++) 147 | if (!visited[i] && !BFS_isBipartite(visited, i)) 148 | return false; 149 | 150 | return true; 151 | } 152 | 153 | void Graph::DFSrec_topologicalSorting(std::vector& visited, std::stack& currentRes, size_t currentVertex) const 154 | { 155 | visited[currentVertex] = true; 156 | 157 | for (auto it = adj[currentVertex].begin(); it != adj[currentVertex].end(); ++it) 158 | { 159 | if (visited[*it]) 160 | continue; 161 | 162 | DFSrec_topologicalSorting(visited, currentRes, *it); 163 | } 164 | 165 | currentRes.push(currentVertex); 166 | } 167 | void Graph::topologicalSorting(std::vector& topoSort) const 168 | { 169 | // Can be made to detect a cycle during the topological sorting and thus to avoid traversing the graph twice. 170 | // !! This check shouldn't be done when topologicalSorting is used in SCC. !! 171 | //if (isCyclic()) 172 | // throw "Only acyclic graphs can be sorted topologically!"; 173 | 174 | std::vector visited(verticesCount, false); 175 | std::stack result; 176 | 177 | for (size_t i = 0; i < visited.size(); i++) 178 | { 179 | if (visited[i]) 180 | continue; 181 | 182 | DFSrec_topologicalSorting(visited, result, i); 183 | } 184 | 185 | while (!result.empty()) 186 | { 187 | topoSort.push_back(result.top()); 188 | result.pop(); 189 | } 190 | } 191 | 192 | void Graph::DFS_rec_SCC(const Graph& g, std::vector& visited, std::vector& lastlyVisited, size_t currentVertex) const 193 | { 194 | visited[currentVertex] = true; 195 | lastlyVisited[currentVertex] = true; 196 | 197 | for (auto it = g.adj[currentVertex].begin(); it != g.adj[currentVertex].end(); ++it) 198 | { 199 | if (visited[*it]) 200 | continue; 201 | 202 | DFS_rec_SCC(g, visited, lastlyVisited, *it); 203 | } 204 | } 205 | void Graph::DFSrec_SCC(const Graph& g, std::vector topoSort) const 206 | { 207 | std::vector visited(verticesCount, false); 208 | 209 | std::cout << "The strongly connected components are:" << std::endl; 210 | 211 | for (size_t i = 0; i < topoSort.size(); i++) 212 | { 213 | if (visited[topoSort[i]]) 214 | continue; 215 | 216 | std::vector lastlyVisited(verticesCount, false); 217 | 218 | DFS_rec_SCC(g, visited, lastlyVisited, topoSort[i]); 219 | 220 | for (size_t j = 0; j < lastlyVisited.size(); j++) 221 | { 222 | if (lastlyVisited[j]) 223 | std::cout << j << ' '; 224 | } 225 | std::cout << std::endl; 226 | 227 | lastlyVisited.clear(); 228 | } 229 | } 230 | void Graph::SCC() const 231 | { 232 | std::vector topoSort; 233 | topologicalSorting(topoSort); 234 | 235 | Graph transposed = getTransposedGraph(); 236 | 237 | DFSrec_SCC(transposed, topoSort); 238 | } 239 | 240 | 241 | Graph::Graph(size_t verticesCount, bool oriented) : adj(verticesCount), verticesCount(verticesCount), oriented(oriented) 242 | {} 243 | void Graph::addEdge(size_t start, size_t end) 244 | { 245 | adj[start].push_back(end); 246 | if (!oriented) 247 | adj[end].push_back(start); 248 | } 249 | Graph Graph::getTransposedGraph() const 250 | { 251 | Graph transposedGraph(verticesCount, true); 252 | 253 | for (size_t i = 0; i < adj.size(); i++) 254 | { 255 | for (auto j = adj[i].begin(); j != adj[i].end(); ++j) 256 | transposedGraph.addEdge(*j, i); 257 | } 258 | return transposedGraph; 259 | } 260 | 261 | 262 | int main() 263 | { 264 | Graph g(8, true); 265 | g.addEdge(0, 5); 266 | g.addEdge(1, 0); 267 | g.addEdge(1, 3); 268 | g.addEdge(2, 0); 269 | g.addEdge(2, 3); 270 | g.addEdge(2, 4); 271 | g.addEdge(3, 7); 272 | g.addEdge(4, 2); 273 | g.addEdge(4, 5); 274 | g.addEdge(5, 4); 275 | g.addEdge(6, 1); 276 | g.addEdge(6, 3); 277 | g.addEdge(6, 7); 278 | g.addEdge(7, 6); 279 | 280 | std::vector shP; 281 | g.BFS(0, 7, shP); 282 | for (size_t i = 0; i < shP.size(); i++) 283 | std::cout << shP[i] << " "; 284 | } 285 | -------------------------------------------------------------------------------- /Sem_09/README.md: -------------------------------------------------------------------------------- 1 | # Нетегловни графи 2 | 3 | ### Задача 1: 4 | Няколко банки си дължат взаимно разни суми. Съставете бърз алгоритъм, който да провери **има ли възможност за прихващане между две или повече банки**. 5 | 6 | ### Решение: 7 | Представяме входните данни чрез ориентиран граф. 8 | **Върховете на графа съответстват на банките**, а **ребрата - на задълженията**. 9 | По-точно, ако банката U има задължение към банката V, ще има ребро от U към V, тоест U —> V. 10 | Всяко прихващане съответства на ориентиран цикъл в графа. 11 | **Търсим ориентиран цикъл с помощта на обхождане в дълбочина**. 12 | Този алгоритъм изразходва време O(m + n), където n е броят на банките (върховете на графа), а m - броят на задълженията между банките (тоест ребрата на графа). 13 | 14 | --- 15 | 16 | ### Задача 2: 17 | Да се състави алгоритъм, който проверява **дали дадена схема от зъбчати колела може да работи**. (Някои колела се допират и така предават движението си едно на друго.) 18 | 19 | ### Решение: 20 | Моделираме входните данни чрез неориентиран граф. 21 | **Върховете на графа съответстват на зъбчатите колела**, а **ребрата - на зацепванията**. Тоест между върховете А и В има ребро <=> колелата А и В се зацепват. 22 | Схемата ще работи <=> в графа няма цикъл с нечетна дължина. 23 | 24 | **Теорема на Кьониг: един неориентиран граф е двуделен <=> не съдържа цикли с нечетна дължина.** 25 | 26 | Следователно схемата ще работи <=> **графът е двуделен <=> върховете (колелата) могат да се оцветят в два цвята**, съответстващи на посоките на въртене - цвят "по часовника" и цвят "срещу часовника". (Не е важно, ако това може да стане поне по два начина: един начин и неговият негатив.) 27 | 28 | --- 29 | 30 | ### Задача 3: 31 | Върху куфара на един турист има лепенки от различни градове, които туристът е посетил. Някои лепенки се припокриват частично, тоест може да се случи лепенката А да е поставена върху лепенката В. 32 | Съставете алгоритъм, който **намира реда на посещаване на градовете** (тоест реда на поставяне на лепенките върху куфара). Естествено, този ред може да не е определен еднозначно; в такъв случай да се изведе един възможен ред. 33 | 34 | ### Решение: 35 | Използваме ориентиран граф, чиито **върхове съответстват на лепенките**, а **ребрата - на припокриванията на лепенките**. 36 | Ако лепенката В е поставена върху лепенката А, то градът В е посетен след града А, затова слагаме ребро от А към В, тоест А —> В. 37 | Трябва да подредим лепенките (градовете) така, че ребрата да сочат само от ляво надясно. **Това става с помощта на топологично сортиране**, а то използва обхождане в дълбочина. 38 | Времето на алгоритъма е линейно: O(m + n), където n = броя на лепенките (върховете на графа), m = броя на припокриванията (ребрата на графа). 39 | 40 | ![alt_text](https://i.ibb.co/TtGhH5B/topoSort.png) 41 | 42 | --- 43 | 44 | ### Задача 4: 45 | В някакъв форум сме попаднали на обяви от вида: 46 | - "Заменям велосипед за чифт ски."; 47 | - "Заменям третия том на 'Винету' за втория том на същата книга."; 48 | - "Заменям чисто нов компютър за пълен комплект лекции по ДАА." 49 | 50 | и други подобни. 51 | Ние самите имаме ненужен предмет X, а ни трябва предмет Y. За съжаление, във форума липсва обява "Заменям Y за X." Обаче може да има начин да постигнем целта си с няколко замени. Естествено, искаме замените да са възможно най-малко: всяка замяна струва преговори, срещи, време... 52 | Съставете бърз алгоритъм, който **да ни помогне да получим Y срещу X с възможно най-малко замени**. 53 | 54 | ### Решение: 55 | Представяме входните данни чрез ориентиран граф. 56 | **Върховете на графа съответстват на предметите** (томовете на "Винету", велосипеда, ските, компютъра, записките по ДАА и т.н.). **Ребрата на графа съответстват на обявите**. По-точно, обявата " Заменям предмета U за предмета V." се представя така: V —> U. 57 | Графът съдържа два специални върха X и Y (съответно предмета, който имаме, и предмета, който търсим). Всяка редица от замени е път в графа. Търсим най-къса редица от замени, тоест **най-къс път от X до Y**. 58 | За целта използваме **търсене в ширина**. Този алгоритъм решава задачата за време O(m + n), където n е броят на предметите (върховете), m - броят на обявите (ребрата). 59 | 60 | --- 61 | 62 | ### Задача 5: 63 | Даден е списък от "връзки" между учебни предмети: 64 | "За да учим диференциална геометрия, трябва преди това да сме учили математически анализ и аналитична геометрия." и други подобни. 65 | Съставете бърз алгоритъм, който ни казва **как да се подредят предметите в учебния план на студентите**. 66 | 67 | ### Решение: 68 | Представяме входните данни чрез ориентиран граф. 69 | **Върховете на графа съответстват на предметите**, а **ребрата - на връзките**. Посока на ребрата е от по-ранен към по-късен предмет. По-точно, връзката "За да учим предмет X, трябва преди това да сме учили предмети Y и Z." се представя така: Y —> X и Z —> X. 70 | Задачата се решава чрез **топологично сортиране на графа**. 71 | 72 | --- 73 | 74 | ### Задача 6: 75 | Експертна система съдържа теореми от тип импликации между разни свойства (твърдения), например: 76 | - "Ако едно число се дели на 6, то се дели на 3."; 77 | - "Ако едно число е точен квадрат, то не може да бъде просто." 78 | 79 | и така нататък. 80 | 81 | Да се състави бърз алгоритъм, който **по дадена съвкупност от теореми разделя свойствата на групи от еквивалентни свойства**, например: 82 | "едно число завършва на нула" <=> "числото се дели на 10" <=> "числото се дели на 2 и на 5 ". 83 | Друга група: 84 | "едно естествено число е точен квадрат" <=> "в разлагането му на прости множители всички степенни показатели са четни" <=> "квадратният му корен е цяло число". 85 | 86 | ### Решение: 87 | Моделираме условието на задачата с ориентиран граф. 88 | **Върховете на графа представляват отделните свойства**, а **ребрата - връзките между тях, изразени чрез теоремите**. Посоките на ребрата съответстват на посоките на импликациите, тоест теорема "Ако U, то V." се представя чрез реброто U —> V. 89 | Две свойства P и Q са равносилни, ако от P следва Q и от Q следва P за една или повече стъпки, тоест ако има път както от P до Q, така и от Q до P. С други думи, две свойства са равносилни <=> 90 | **съответните им върхове в графа са силно свързани**. 91 | 92 | Затова групите от еквивалентни свойства съответстват на **компонентите на силна свързаност**. 93 | Намираме ги чрез обхождане в дълбочина за време O(m + n), където n е броят на свойствата (върховете на графа), а m - броят на дадените теореми (ребрата на графа). 94 | 95 | ![alt_text](https://i.ibb.co/BcYMCmw/SCC.png) 96 | 97 | --- 98 | 99 | ### Задача 7: 100 | За голяма група от хора имаме информация от типа: 101 | "X казва на Y всяка новина, която узнае." 102 | Научили сме някаква новина, но имаме право само на един разговор. Съставете бърз алгоритъм, който ни **връща на кого от групата да кажем новината, за да я научат всички** (Може и да няма такъв човек.) 103 | 104 | ### Решение: 105 | Моделираме задачата с ориентиран граф. 106 | **Върховете на графа са хората от групата**; **има ребро от X към Y <=> X казва на Y всяка новина, която узнае**. 107 | 108 | ***Първи случай: графът е ацикличен***. 109 | Извършваме **топологично сортиране**. 110 | Ако изобщо има връх с желаното свойство, то това е първият връх в получения списък. Дали всички върхове са достижими от първия, 111 | разбираме с помощта на ново обхождане (BFS/DFS). Ако всички са достижими - първият връх е решение. Ако не всички са достижими - няма такъв връх (човек). 112 | 113 | ***Втори случай: графът съдържа цикъл***. 114 | Сега не можем да извършим топологично сортиране. Обаче върховете от една и съща компонента на силна свързаност едновременно имат или нямат желаното свойство; а също научават или не научават новината. 115 | 116 | Затова **намираме компонентите на силна свързаност**. Построяваме граф, съставен от тях (тоест върховете на новия граф 117 | са компонентите на силна свързаност на стария граф). 118 | Прилагаме алгоритъма от първи случай за новия граф. 119 | Задачата има решение за стария граф <=> приложеният алгоритъм ни върне, че има решение на новия. Върхът от новия граф, който е решение, ако има такъв, съответства на една компонента на силна свързаност на стария граф. Който и да е връх от тази компонента на силна свързаност има желаното свойство, тоест можем да съобщим новината на произволен човек от тази компонента на силна свързаност. 120 | -------------------------------------------------------------------------------- /Sem_12/README.md: -------------------------------------------------------------------------------- 1 | # Долни граници 2 | ## Доказателство за долна граница чрез дърво за вземане на решения (decision tree), чрез "игра срещу противник" (adversary argument) и чрез редукция. 3 | 4 | Нека Π е произволна изчислителна задача. **Долна граница върху сложността на Π** е всяка функция φ(n), такава че всеки алгоритъм, който решава Π, работи във време Ω(φ(n)). **Горна граница върху сложността на Π** е всяка функция ψ(n), такава че поне един алгоритъм, който решава Π, работи във време O(ψ(n)). 5 | 6 | ## Задачи 7 | 8 | ### Задача 1 (The Balance Puzzle): 9 | Дадени са 9 номерирани предмета, да речем топки. Осем от тях имат едно и също тегло, а една топка е по-тежка. Нашата цел е да идентифицираме тежката топка, като използваме везни. Везните нямат стандартни теглилки, така че можем да правим единствено измервания от вида: някои топки на едното блюдо срещу други топки на другото блюдо, и да наблюдаваме резултата. Има точно три възможности за резултата: везната да се наклони наляво, везната да се наклони надясно или везната да остане балансирана. 10 | 11 | Докажете "добра" долна граница за броя измервания, които трябва да направим, за да намерим тежката топка. 12 | 13 | ### Решение: 14 | Нека деветте топки са b_1, ..., b_9. Ще докажем долна граница **2 измервания** за намиране на тежката топка, използвайки дърво за вземане на решения. 15 | 16 | ![alt_text](https://i.ibb.co/LhyxG6b/The-balance-puzzle.png) 17 | 18 | Доказателството за долна граница е следното: Схемата трябва да е такава, че да различи една възможност от общо девет, използвайки тернарно дърво на вземане на решение. Тернарно дърво с височина 1 има най-много три листа, така че не може да различава повече от три възможности. Това не ни върши работа за тази задача. Очевидно дървото трябва да има височина поне 2, за да има поне девет листа, с които да може да различава девет възможности. От този аргумент следва, че 2 е долна граница, но НЕ следва, че е достижима долна граница. В случая, тя е достижима, но това следва от показаната конкретна схема. 19 | 20 | --- 21 | 22 | ### Задача 2 (The Twelve-coin Puzzle): 23 | Дадени са 12 номерирани монети. Измежду тях, 11 имат едно и също тегло, а другата — да я наречем "странната" монета — е или по лека, или по- тежка. Нашата задача е да идентифицираме "странната" монета, както и да определим дали е лека или тежка, използвайки везни като тези в горната задача. Монетите са различими на външен вид, примерно са номерирани c_1, ..., c_12. 24 | 25 | Докажете "добра" долна граница за броя измервания, които трябва да направим, за да намерим "странната" монета и нейната "странност". 26 | 27 | ### Решение: 28 | Броят на възможностите е 24: два пъти броя на монетите. Нека i' означава “c_1 е тежка”, i'' означава “c_1 е лека”, ii' означава “c_2 е тежка”, ii'' означава “c_2 е лека”, и така нататък, xii' означава “c_12 е тежка” и xii'' означава “c_12 е лека”. 29 | 30 | ![alt_text](https://i.ibb.co/gvPrYL7/The-twelve-coin-puzzle.png) 31 | 32 | Дотук доказахме, че **три измервания са достатъчни**. 33 | 34 | **Три измервания са необходими**. Тъй като искаме да различим една възможност от общо 24 възможности и всяко измерване има най-много три възможни изхода, долна граница за броя на измерванията е горна граница от log_3(24). 35 | 36 | --- 37 | 38 | ### Задача 3: 39 | Дадени са n подаръци и n кутии. На всеки подарък пасва точно една кутия. Не са позволени сравнения между 2 подаръка или между 2 кутии. Позволени са само сранвния от вида: 40 | - кутияX пасва точно на подаръкX; 41 | - кутияX е голяма за подаръкX; 42 | - кутияX е малка за подаръкX. 43 | 44 | Докажете, че всеки алгоритъм, който открива съответствието между подаръци и кутии работи във време Ω(nlogn). 45 | 46 | ### Решение: 47 | Пасването на подаръците с кутиите е биекция. Следователно свеждаме задачата до намиране на "правилната" биекция от множеството на подаръците към множеството на кутиите. Всички биекции са n!. Това са възможните изходи на нашия алгоритъм. Следователно дървото за вземане на решения има поне n! листа. Разклонеността е 3. Следователно височината на дървото е поне log_3(n), което е точно Ω(nlogn). 48 | 49 | --- 50 | Доказани долни граници: 51 | - **Търсене на стойност в сортиран масив** — Ω(logn); 52 | - **Сортиране чрез директни сравнения** — Ω(nlogn); 53 | - **Намиране на конкретна биекция** — Ω(nlogn); 54 | - **Уникалност на елементи** — Ω(nlogn); 55 | - **Най- близки елементи** — Ω(nlogn); 56 | - **Намиране на мода** — Ω(nlogn); 57 | --- 58 | 59 | ### Задача 4: 60 | Задачата за разпознаване "Свързаност" се дефинира така: общият пример е неориентиран граф G = (V, E), а въпросът е дали G е свързан. 61 | 62 | Докажете, че всеки алгоритъм за Свързаност, който прави достъпи до графа само чрез задаване на въпроси от вида **“Има ли ребро между връх i и връх j?”**, задава поне n^2 въпроса, ако броят на върховете е 2n. 63 | 64 | ### Решение: 65 | Има аргументация чрез противник. Противникът разбива множеството от върховете на две подмножества U и W, които наричаме дяловете, като ∣U∣ = ∣W∣ = n, и след това действа така: Получавайки запитване за върхове i и j, 66 | - ако i и j са в един и същи дял, противникът отговаря ДА, 67 | - ако i и j са в различни дялове, противникът отговаря НЕ. 68 | 69 | Съществуват точно n^2 двуелементни множества {x, y}, такива че x и y са от различни дялове. Ще покажем, че всеки алгоритъм AlgConn за тази задача трябва да запита за всяко от тях. Да допуснем противното: AlgConn е коректен алгоритъм за Свързаност и съществува x ∈ U и съществува y ∈ W, такива че AlgConn не е направил запитване за x и y. 70 | - Ако AlgConn отговори ДА, то противникът конструира несвързан граф, в който U и W са клики и няма нито едно ребро между връх от U и връх от W. По този начин противникът опровергава AlgConn, тъй като всички отговори, които е давал, са консистентни с този граф, а графът не е свързан. 71 | 72 | - Ако AlgConn отговори НЕ, то противникът конструира свързан граф, в който U и W са клики и има ребро между x и y. По този начин противникът опровергава AlgConn, тъй като всички отговори, които е давал, са консистентни с този граф, а графът е свързан. 73 | 74 | --- 75 | 76 | ### Задача 5: 77 | Докажете "добра" долна граница за намиране на втори по големина елемент в масив. 78 | 79 | ### Решение: 80 | 1. Наивният подход е първо да намерим най- големия елемент за n-1 стъпки, после да го премахнем и да намерим втория най- голям елемент за n-2 стъпки. 81 | Този алгоритъм извършва общо 2n-3 стъпки. 82 | 83 | 2. Първо намираме най- големия елемент (за n-1 стъпки). След това "провеждаме мини турнир" само от тези, които са загубили от най- големия. Това са точно logn елемента. За да намерим най- големия от тях (тоест втория най- голям за целия масив), извършваме точно logn-1 сравнения. 84 | Този алгоритъм извършва общо n-1 + logn-1 стъпки и това е точна долна граница за задачата "Втори по големина елемент". 85 | 86 | ![alt_text](https://i.ibb.co/S3NZnWR/Second-Largest.png) 87 | 88 | --- 89 | Нека Π_1 и П_2 са изчислителни задачи. 90 | Грубо казано, от факта, че Π_1 се свежда до Π_2 правим два извода: 91 | - Ако Π_1 е трудна задача (каквото и да значи това), то Π_2 също е трудна. Няма как някаква вътрешна, иманентна сложност да изчезне при редукцията; 92 | - Ако Π_2 е лесна (каквото и точно да значи това), то Π_1 също е лесна. По същата причина: не може иманентната сложност да изчезне при редукцията. 93 | 94 | Важно е да знаем, че следните изводи **НЕ са верни**: 95 | - Ако Π_1 е лека, то Π_2 също е лека. Такъв извод няма. Винаги можем да направим нещата произволно сложни; 96 | - Ако Π_2 е трудна, то Π_1 също е трудна. Такъв извод няма. Причината отново е, че винаги можем да направим нещата сложни. 97 | --- 98 | 99 | ### Задача 6: 100 | Докажете, че всеки алгоритъм, който връща броя на различните стойности в масив, работи за време Ω(nlogn). 101 | 102 | ### Решение: 103 | Допускаме, че съществува алгоритъм uniqueElementsCount, който връща броя на уникалните елементи в масив и работи за време по- бързо от nlogn (например n). 104 | Тогава можем да направим следната редукция: 105 | ```c++ 106 | elementUniqueness(arr[1..n]) 107 | { 108 | res <- uniqueElementsCount(arr) // O(n) 109 | return res == n // O(n) 110 | } 111 | ``` 112 | 113 | Намерихме алгоритъм, който решава elementUniqueness за време O(n). Но имаме долна граница Ω(nlogn). Противоречие. 114 | Следователно всеки алгоритъм, който връща броя на различните стойности в масив, работи за време Ω(nlogn). 115 | 116 | --- 117 | 118 | ### Задача 7: 119 | Можем ли да построим пирамида, която има сложност O(1) за добавяне на елемент и O(1) за извличане на най- малък елемент? 120 | 121 | ### Решение: 122 | Да допуснем, че можем. 123 | Тогава можем да направим следната редукция: 124 | ```c++ 125 | sort(arr[1..n]) 126 | { 127 | Създай масив heap[1..n] 128 | heap <- buildHeap(arr) // O(n) ( n*insert(arr[i]) ) 129 | 130 | for i <- 1 to n // O(n) ( n*extractMin() ) 131 | arr[i] <- heap.extractMin() 132 | } 133 | ``` 134 | 135 | Намерихме алгоритъм, който решава задачата Сортиране за време O(n). Но имаме долна граница Ω(nlogn). Противоречие. 136 | 137 | --- 138 | 139 | ### Задача 8: 140 | Даден е масив от точки , x ∈ R, y ∈ R. 141 | Докажете, че всеки алгоритъм, който сортира точките по ъгъла, който сключва радиус-векторът с Ox, работи във време Ω(nlogn). 142 | 143 | ### Решение: 144 | Допускаме, че съществува алгоритъм sortPoints, който сортира точки по ъгъла, който сключва радиус-векторът с Ox за време по- бързо от nlogn (например n). 145 | Тогава можем да направим следната редукция: 146 | ```c++ 147 | sort(arr[1..n]) 148 | { 149 | Създай масив от наредени двойки Points[1..n] 150 | 151 | for i <- 1 to n // O(n) 152 | Points[i].x <- 5 153 | Points[i].y <- arr[i] 154 | 155 | sortPoints(Points) // O(n) 156 | 157 | for i <- 1 to n // O(n) 158 | arr[i] <- Points[i].y 159 | } 160 | ``` 161 | 162 | Намерихме алгоритъм, който решава задачата Сортиране за време O(n). Но имаме долна граница Ω(nlogn). Противоречие. 163 | -------------------------------------------------------------------------------- /Sem_13/README.md: -------------------------------------------------------------------------------- 1 | # Алгоритмична неподатливост. NP-пълнота и NP-трудност. 2 | 3 | **Дефиниция 1:** Казваме, че **алгоритъм X е ефикасен**, ако сложността по време на X е O(n^k) за някаква положителна константа k. 4 | 5 | **Дефиниция 2:** Казваме, че **изчислителна задача Π е неподатлива**, ако за нея не съществува ефикасен алгоритъм. 6 | 7 | **Дефиниция 3:** **Клас на сложност** (complexity class) е множество изчислителни задачи със сходна сложност. 8 | 9 | --- 10 | 11 | 12 | # Клас на сложност P 13 | Това са **задачите за разпознаване**, за които има ефикасен алгоритъм. 14 | 15 | --- 16 | 17 | ## Недетерминирана машина на Turing (НМТ) 18 | НМТ означава, че на една наредена двойка от състояние и буква съответстват, в общия случай, няколко възможности (може и нула) за “еволюция” на машината, които са наредени тройки от ново състояние, нова буква и движение на главата. 19 | 20 | Как да интепретираме възможността машината да еволюира по няколко различни начина едновременно? Какво означава да има няколко нови състояния и изобщо няколко нови конфигурации? Една смислена интепретация е, че винаги, когато машината трябва да мине в k > 1 нови конфигурации, тя се “размножава”: тя изчезва, но се появяват k нови НМТ, всяка от които се оказва в точно една от тези k конфигурации. Всяко от тези копия “заживява свой живот” според δ (ф-я на прехода). Всички тези копия са в едно и също дискретно време, но са различни в смисъл, че всяко си има свое състояние и своя лента със собствена позиция на главата. Тогава тези копия съществуват и работят едновременно, така че въвеждаме паралелизъм. 21 | 22 | Как НМТ приема и отхвърля? Да видим какво означава такава неограничено размножаваща се машина да приеме или отхвърли входа. При нормалните, детерминирани машини е ясно: приемането е достигане на приемаща конфигурация, а отхвърлянето е достигане на отхвърляща конфигурация. 23 | 24 | **Приемане и отхвърляне от недетерминирана машина:** 25 | Нека M е НМТ и x е неин вход. 26 | - M приема x <=> **поне едно нейно копие** достигне до приемаща конфигурация. 27 | - M отхвърля x <=> **всички нейни копия** достигнат до отхвърляща конфигурация или катастрофират, без никое копие да е достигнало до приемаща конфигурация. 28 | 29 | --- 30 | 31 | # Клас на сложност NP 32 | **NP** е класът на задачите за разпознаване, които се решават в полиномиално време при всякакви входни данни от **недетерминирана** машина на Тюринг. 33 | 34 | Еквивалентно: **NP** е класът на задачите за разпознаване, за които съществува алгоритъм с полиномиална времева сложност при всякакви входни данни, проверяващ отговора ДА на задачата с помощта на допълнителен параметър, наречен сертификат, който зависи от входните данни на задачата и чиято дължина е полиномиална спрямо тяхната. 35 | 36 | --- 37 | 38 | # P vs NP 39 | 1. Щом всяка задача от **P** е в **NP**, то **P ⊆ NP**. 40 | 41 | 2. Най-важният нерешен въпрос в теоретичната информатика е **дали NP ⊆ P**. Иначе казано, **дали P = NP**. 42 | Класът **P** се състои от лесните за решаване задачи, ако приемем, че “лесна за решаване задача” и “задача, за която има полиномиална МТ” са синоними. Класът **NP** се състои от лесните за проверяване задачи, предвид това, че за всеки Да-екземпляр на такава задача, някоя полиномиална НМТ може да провери сертификат за него в полиномиално време. Въпросът дали **P = NP** е въпросът **дали лесно за решаване е същото като лесно за проверяване**. 43 | 44 | ## Полиномиални редукции 45 | **Дефиниция 4:** Нека A и B са алгоритмични задачи. **Полиномиална редукция** (бележим A ∝_p B) от задачата A към задачата B наричаме алгоритъм от следния вид: 46 | ```c++ 47 | A (inputA: входни данни на задачата A) 48 | 1) inputB <- Редукция на входа(inputA) 49 | 2) outputB <- B(inputB) 50 | 3) outputA <- Редукция на изхода(outputB) 51 | 4) return outputA 52 | ``` 53 | --- 54 | 55 | # NP-пълнота 56 | Нека Π е задача за разпознаване. Казваме, че Π е **NP-пълна**, ако: 57 | - Π ∈ **NP**; 58 | - За всяка задача Π' ∈ **NP** съществува полиномиална редукция от Π' към Π (Π' ∝_p Π). 59 | 60 | # NP-трудност 61 | Нека Π е задача за разпознаване. Казваме, че Π е **NP-трудна**, ако за всяка задача Π' ∈ **NP** съществува полиномиална редукция от Π' към Π (Π' ∝_p Π). 62 | 63 | --- 64 | 65 | ![alt_text](https://i.ibb.co/Cm5L0mh/P-vs-NP.png) 66 | 67 | --- 68 | 69 | ### Теорема: 70 | Ако C е **NP-трудна** задача и C ∝_p D, то D също е **NP-трудна** задача. 71 | 72 | **Доказателство:** 73 | Щом C е **NP-трудна** задача, то ∀A ∈ **NP** : A ∝_p C. По условие C ∝_p D. Следователно ∀A ∈ **NP** : A ∝_p D. Затова D е **NP-трудна**. 74 | 75 | --- 76 | 77 | ## SAT (Satisfiability) и теоремата на Cook: 78 | Екземпляр: КНФ φ. 79 | Въпрос: Дали φ е удовлетворима? 80 | 81 | **Теорема на Cook:** SAT е **NP-пълна**. 82 | Доказателство: https://en.wikipedia.org/wiki/Cook%E2%80%93Levin_theorem 83 | 84 | ## 2-SAT: 85 | 2-SAT е частен случай на SAT: всички клаузи са дизюнкции с два операнда. 86 | 87 | ***Пример:*** (¬x ∨ y) ∧ (t ∨ x) ∧ (y ∨ t). 88 | 89 | Задачата 2-SAT ∈ **P**, тоест за нея съществува бърз (полиномиален) алгоритъм; основава се на търсене на компонентите на силна свързаност на подходящ граф. 90 | 91 | ## 3-SAT: 92 | 3-SAT е частен случай на SAT: всички клаузи са дизюнкции с три операнда. 93 | 94 | ***Пример:*** (¬x ∨ y ∨ t) ∧ (t ∨ x ∨ ¬y). 95 | 96 | 3-SAT е **NP-пълна**. 97 | **Доказателство:** 98 | 1. 3-SAT е в класа **NP**. 99 | 100 | Тук сертификатът ни е булев вектор, съставен от последователните стойности на променливите. Минаваме линейно през φ, замествайки със символите от сертификата, и по този начин, чрез едно линейно по големината на φ обхождане проверяваме дали се получава стойност "истина". 101 | 102 | 2. 3-SAT е **NP-трудна**. 103 | 104 | Използваме по- горната теорема. Знаем, че SAT е **NP-трудна**. 105 | Тогава можем да направим следната полиномиална редукция: 106 | SAT ∝_p 3-SAT по следния начин: 107 | 108 | Нека φ = (x ∨ y ∨ ¬z ∨ t ∨ p) ∧ (...) ∧ ... 109 | Търсим φ', която да е еквивалентна на φ, но в същото време да може да "се обработи" от 3-SAT. Построяваме φ' по следния начин: 110 | φ' = (x ∨ y ∨ A) ∧ (¬A ∨ ¬z ∨ B) ∧ (¬B ∨ t ∨ p) ∧ (...) ∧ ... 111 | 112 | ## Други основни NP-пълни задачи: 113 | - **SubsetSum** (търсене на подмножество с даден сбор) е **NP-пълна** задача. 114 | - **Partition** (разбиване на множество на две части с равни сборове) е **NP-пълна** задача. 115 | - **Knapsack** (Задачата за раницата) е **NP-пълна**. 116 | 117 | ### Задачи върху графи от класа **NP**: 118 | - **“Изоморфен подграф”** е **NP-пълна задача**. 119 | Вход: два графа G и H. 120 | Въпрос: G съдържа ли подграф, изоморфен на H? 121 | 122 | - **“Изоморфни графи”** вероятно е **NP-междинна** задача. 123 | Вход: два графа G и H. 124 | Въпрос: Изоморфни ли са G и H? 125 | 126 | - **Vertex Cover** е **NP-пълна задача**. 127 | Вход: Неориентиран граф G, естествено число k. 128 | Въпрос: Дали G има върхово покриване U, такова че |U| <= k? 129 | 130 | - **Dominating Set** е **NP-пълна задача**. 131 | Вход: Неориентиран граф G, естествено число k. 132 | Въпрос: Дали G има доминиращо множество U, такова че |U| <= k? 133 | 134 | - **Clique** е **NP-пълна задача**. 135 | Вход: Неориентиран граф G, естествено число k. 136 | Въпрос: Дали G има клика U, такава че |U| >= k? 137 | 138 | - **Anticlique** (“Независимо множество”) е **NP-пълна** задача. 139 | 140 | - **“Най -дълъг прост път”** е **NP-пълна** задача. 141 | 142 | **Задачите “Ойлеров път” и “Ойлеров цикъл” са от класа P**. 143 | **Задачите “Хамилтонов път” и “Хамилтонов цикъл” са от класа NP**. 144 | 145 | --- 146 | 147 | ## Задачи 148 | 149 | ### Задача 1: 150 | Докажете, че следната задача е **NP-пълна**: 151 | **Общ пример:** Множество от булеви променливи X = {x_1, ..., x_n} и множество дизюнктивни клаузи C = {c_1, ..., c_m} над X. 152 | **Въпрос:** Дали съществуват поне две различни удовлетворяващи валюации за C? 153 | 154 | ### Решение: 155 | Тази задача е известна като **Double SAT**. 156 | 157 | 1. Double SAT е в класа **NP**. 158 | За целта трябва да предложим къс сертификат и бърз алгоритъм за проверка на отговор да. 159 | Първо сертификатът ни е първата валюация. Минаваме линейно през множеството от дизюнктивни клаузи C, замествайки със символите от сертификата, и по този начин, чрез едно линейно по големината на C обхождане проверяваме дали се получава стойност "истина". Повтаряме същото за втората валюация. 160 | 161 | 2. Double SAT е **NP-трудна**. 162 | За да покажем, че Double SAT е **NP-трудна**, ще направим редукция от 3-SAT (3-SAT ∝_p Double SAT). 163 | 164 | Нека е даден пример на 3-SAT. Ще конструираме пример на Double SAT, такъв че C е удовлетворимо <=> C' има поне две удовлетворяващи валюации. За целта добавяме една нова булева променлива (която не е в X), да я наречем z, и правим следната клауза k = {z, ¬z}. После правим C' ← C ∪ k. 165 | 166 | Да допуснем, че C е удовлетворимо. Тогава C има поне една удовлетворяваща валюация **φ**. Щом **φ** е удовлетворяваща, във всяка клауза от C има поне един литерал **y**, такъв че **φ(y) = 1**. 167 | Конструираме две валюации **Ψ** и **Ψ'** така: рестрикцията на всяка от тях до домейна X е точно като **φ**, а **Ψ(z) = 0** и **Ψ'(z) = 1**. Очевидно **Ψ** и **Ψ'** са различни, имайки различни стойности върху z. 168 | 169 | Ще покажем, че във всяка клауза на c ∈ C' има поне един литерал, който има стойност 1 както под **Ψ**, така и под **Ψ'** : 170 | - ако c ∈ C, това следва от фактите, че **φ** е удовлетворима за C и, че **Ψ** и **Ψ'** върху променливите от X съвпадат със съответните стойности на **φ**; 171 | - ако c = {z, ¬z}, то **Ψ(¬z) = 1** и **Ψ'(z) = 1**. 172 | 173 | Сега да допуснем, че C' е удовлетворима от поне две различни удовлетворяващи валюации **Ψ** и **Ψ'**. Тогава и под **Ψ**, и под **Ψ'** е вярно, че за всяка клауза на C' има литерал, чиято стойност е 1, което веднага влече, че рестрикцията на коя да е от тях върху домейн X е удовлетворяваща валюация за C. 174 | 175 | --- 176 | 177 | ### Задача 2: 178 | Докажете, че следната задача е **NP-пълна**: 179 | **Общ пример:** Множество A, множество B ⊆ 2^A, число k ∈ N. 180 | **Въпрос:** Съществува ли C ⊆ B, такова че |C| ≤ k и ∪C = A? 181 | 182 | ### Решение: 183 | Тази задача е известна като **Set Cover**. 184 | 185 | 1. Set Cover е в класа **NP**. 186 | Сертификатът ни е множеството C. Лесно можем да проверим (симулирайки чрез сертификата детерминирана машина на Тюринг), че |C| ≤ k и ∪C = A. 187 | 188 | 2. Set Cover е **NP-трудна**. 189 | За да покажем, че Set Cover е **NP-трудна**, ще направим редукция от Vertex Cover (Vertex Cover ∝_p Set Cover). 190 | 191 | Нека е даден произволен пример на Vertex Cover. Този пример е наредена двойка (G = (V, E), k'), където G е граф, а k' - естествено число. Ще построим пример (A, B, k) на Set Cover. 192 | Първо, k <- k' . Второ, A <- E. Трето, B се състои от |V| на брой елемента B = {B_1, B_2, ..., B_|V|}, като всяко B_i съответства на точно един връх v_i от графа. Нещо повече, B_i е множеството от точно тези ребра от E, които са инцидентни с v_i . 193 | - Фактът, че (G = (V, E), k') е ДА-пример на Vertex Cover тогава и само тогава, когато (A, B, k) е ДА-пример на Set Cover, вземаме за очевиден. 194 | - Фактът, че конструкцията може да се направи в полиномиално време, е очевиден. Така че действително тук описахме полиномиална редукция. 195 | 196 | --- 197 | 198 | ### Задача 3: 199 | Докажете, че следната задача е **NP-пълна**: 200 | **Общ пример:** Неориентиран свързан граф G и естествено число k. 201 | **Въпрос:** Съществува ли покриващо дърво T на G, т. че максималната степен на връх в T е не по-голяма от k? 202 | 203 | ### Решение: 204 | Принадлежността към **NP** е очевидна. 205 | Задачата е **NP-трудна**. Има тривиална редукция от Хамилтонов път: при k = 2 покриващото дърво е Хамилтонов път. 206 | -------------------------------------------------------------------------------- /Advanced-Timer_Scheduler/TimerScheduler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "SchedulableTask.hpp" 4 | #include "TaskPool.hpp" 5 | 6 | #include 7 | 8 | class TimerScheduler 9 | { 10 | public: 11 | explicit TimerScheduler(unsigned threadsCount = std::thread::hardware_concurrency()) 12 | : m_taskPool{ threadsCount } 13 | {} 14 | 15 | ~TimerScheduler() 16 | { 17 | m_isRunning = false; 18 | m_thereAreTasksToExec.notify_all(); 19 | m_schedulerThread.join(); 20 | } 21 | 22 | TimerScheduler(const TimerScheduler&) = delete; 23 | TimerScheduler& operator=(const TimerScheduler&) = delete; 24 | TimerScheduler(TimerScheduler&&) = delete; 25 | TimerScheduler& operator=(TimerScheduler&&) = delete; 26 | 27 | size_t currentlyScheduled() const 28 | { 29 | std::scoped_lock lock(m_executeTasksMutex); 30 | return m_tasks.size(); 31 | } 32 | 33 | bool updateInterval(const std::string& id, std::chrono::steady_clock::duration newInterval) 34 | { 35 | std::unique_lock lock(m_executeTasksMutex); 36 | 37 | auto taskToBeUpdatedIter = taskIterator(id); 38 | if (taskToBeUpdatedIter == m_tasks.end() || !taskToBeUpdatedIter->second.interval().has_value()) 39 | { 40 | return false; 41 | } 42 | 43 | auto taskToBeUpdatedOldInterval = taskToBeUpdatedIter->second.interval().value(); 44 | auto taskNextStartTime = taskToBeUpdatedIter->first - taskToBeUpdatedOldInterval; 45 | auto now = std::chrono::steady_clock::now(); 46 | while (now > taskNextStartTime) 47 | { 48 | taskNextStartTime += newInterval; // keep the scheduling with a fixed sample rate 49 | } 50 | 51 | taskToBeUpdatedIter->second.setInterval(newInterval); 52 | m_tasks.emplace(std::move(taskNextStartTime), std::move(taskToBeUpdatedIter->second)); 53 | m_tasks.erase(taskToBeUpdatedIter); 54 | 55 | lock.unlock(); 56 | 57 | m_thereAreTasksToExec.notify_one(); 58 | return true; 59 | } 60 | 61 | bool cancelTimer(const std::string& id) 62 | { 63 | std::scoped_lock lock(m_executeTasksMutex); 64 | 65 | auto currentTaskIter = taskIterator(id); 66 | if (currentTaskIter == m_tasks.end()) // the task has already finished or just doesn't exist 67 | { 68 | return false; 69 | } 70 | m_tasks.erase(currentTaskIter); 71 | return true; 72 | } 73 | 74 | void start() 75 | { 76 | try 77 | { 78 | m_schedulerThread = std::thread([this] 79 | { 80 | while (m_isRunning) 81 | { 82 | std::unique_lock lock(m_executeTasksMutex); 83 | m_thereAreTasksToExec.wait(lock, [this] { return !m_tasks.empty() || !m_isRunning; }); 84 | 85 | if (!m_isRunning) 86 | { 87 | return; 88 | } 89 | 90 | auto nextTaskTimepoint = m_tasks.begin()->first; 91 | if (m_thereAreTasksToExec.wait_until(lock, nextTaskTimepoint) == std::cv_status::no_timeout) 92 | { 93 | continue; 94 | } 95 | 96 | executeTasks(); 97 | } 98 | }); 99 | } 100 | catch (...) 101 | { 102 | m_isRunning = false; 103 | m_thereAreTasksToExec.notify_all(); 104 | throw; 105 | } 106 | } 107 | 108 | template 109 | std::future> scheduleSingle(std::chrono::steady_clock::duration&& timeout, TaskFunction&& func) 110 | { 111 | auto invokableTask = [t = std::forward(func)] { return t(); }; 112 | 113 | std::packaged_task()> taskWrapper(invokableTask); 114 | std::future> future = taskWrapper.get_future(); 115 | 116 | addTask(std::move(std::chrono::steady_clock::now() + timeout), SchedulableTask(std::move(taskWrapper))); 117 | return future; 118 | } 119 | 120 | template 121 | std::future> scheduleSingle(std::chrono::steady_clock::duration&& timeout, TaskFunction&& func, Args&&... args) 122 | { 123 | auto invokableTask = [t = std::forward(func), params = std::make_tuple(std::forward(args)...)] 124 | { 125 | return std::apply(t, params); 126 | }; 127 | 128 | std::packaged_task()> taskWrapper(invokableTask); 129 | std::future> future = taskWrapper.get_future(); 130 | 131 | addTask(std::move(std::chrono::steady_clock::now() + timeout), SchedulableTask(std::move(taskWrapper))); 132 | return future; 133 | } 134 | 135 | template 136 | std::future> scheduleSingle(std::string&& id, std::chrono::steady_clock::duration&& timeout, TaskFunction&& func, Args&&... args) 137 | { 138 | auto invokableTask = [t = std::forward(func), params = std::make_tuple(std::forward(args)...)] 139 | { 140 | return std::apply(t, params); 141 | }; 142 | 143 | std::packaged_task()> taskWrapper(invokableTask); 144 | std::future> future = taskWrapper.get_future(); 145 | 146 | addTask(std::move(std::chrono::steady_clock::now() + timeout), SchedulableTask(std::move(taskWrapper), m_hasher(id))); 147 | return future; 148 | } 149 | 150 | struct IntervalReps 151 | { 152 | std::chrono::steady_clock::duration interval; 153 | size_t maxReps = 0; 154 | }; 155 | 156 | template 157 | void scheduleRepeat(IntervalReps&& intervalReps, TaskFunction&& func) 158 | { 159 | auto invokableTask = [t = std::forward(func)] { return t(); }; 160 | addTask(std::move(std::chrono::steady_clock::now()), SchedulableTask(std::move(invokableTask), intervalReps.interval, intervalReps.maxReps)); 161 | } 162 | 163 | template 164 | void scheduleRepeat(IntervalReps&& intervalReps, TaskFunction&& func, Args&&... args) 165 | { 166 | auto invokableTask = [t = std::forward(func), params = std::make_tuple(std::forward(args)...)] 167 | { 168 | return std::apply(t, params); 169 | }; 170 | addTask(std::move(std::chrono::steady_clock::now()), SchedulableTask(std::move(invokableTask), intervalReps.interval, intervalReps.maxReps)); 171 | } 172 | 173 | template 174 | void scheduleRepeat(std::string&& id, IntervalReps&& intervalReps, TaskFunction&& func, Args&&... args) 175 | { 176 | auto invokableTask = [t = std::forward(func), params = std::make_tuple(std::forward(args)...)] 177 | { 178 | return std::apply(t, params); 179 | }; 180 | addTask(std::move(std::chrono::steady_clock::now()), SchedulableTask(std::move(invokableTask), m_hasher(id), intervalReps.interval, intervalReps.maxReps)); 181 | } 182 | 183 | private: 184 | TaskPool m_taskPool; 185 | std::map m_tasks; 186 | std::thread m_schedulerThread; 187 | std::atomic_bool m_isRunning = true; 188 | std::hash m_hasher; 189 | std::condition_variable m_thereAreTasksToExec; 190 | mutable std::mutex m_executeTasksMutex; 191 | 192 | void addTask(std::chrono::steady_clock::time_point&& timepoint, SchedulableTask&& st) 193 | { 194 | if (!m_isRunning) 195 | { 196 | return; 197 | } 198 | 199 | std::unique_lock lock(m_executeTasksMutex); 200 | if (exists(st.hash())) 201 | { 202 | return; 203 | } 204 | m_tasks.emplace(std::move(timepoint), std::move(st)); 205 | lock.unlock(); 206 | 207 | m_thereAreTasksToExec.notify_one(); 208 | } 209 | 210 | void executeTasks() 211 | { 212 | decltype(m_tasks) repeatingTasks; 213 | std::vector keysToRemove; 214 | 215 | auto lastTaskToExecuteIter = m_tasks.upper_bound(std::chrono::steady_clock::now()); 216 | for (auto it = m_tasks.begin(); it != lastTaskToExecuteIter; ++it) 217 | { 218 | std::shared_ptr toInvoke = it->second.clone(); 219 | if (it->second.isEnabled()) 220 | { 221 | keysToRemove.push_back(it->first); 222 | m_taskPool.run([toInvoke] { toInvoke->invoke(); }); 223 | 224 | if (it->second.interval().has_value()) 225 | { 226 | if (it->second.maxReps().has_value()) 227 | { 228 | size_t remainingReps = it->second.maxReps().value() - 1; 229 | it->second.decrementMaxReps(); 230 | if (remainingReps == 0) 231 | { 232 | continue; 233 | } 234 | } 235 | 236 | auto taskInterval = it->second.interval().value(); 237 | auto taskNextStartTime = it->first + taskInterval; 238 | auto now = std::chrono::steady_clock::now(); 239 | while (now > taskNextStartTime) // keep the scheduling with a fixed sample rate 240 | { 241 | taskNextStartTime += taskInterval; 242 | } 243 | 244 | repeatingTasks.emplace(std::move(taskNextStartTime), std::move(it->second)); 245 | } 246 | } 247 | } 248 | for (const auto& key : keysToRemove) 249 | { 250 | auto it = m_tasks.find(key); 251 | if (it != m_tasks.end()) 252 | { 253 | m_tasks.erase(it); 254 | } 255 | } 256 | m_tasks.merge(repeatingTasks); 257 | } 258 | 259 | bool exists(std::optional hash) const 260 | { 261 | if (!hash.has_value()) 262 | { 263 | return false; 264 | } 265 | 266 | return std::any_of(m_tasks.begin(), m_tasks.end(), [hash = hash.value()](auto&& it) { 267 | if (it.second.hash().has_value()) 268 | { 269 | return hash == it.second.hash().value(); 270 | } 271 | return false; 272 | }); 273 | } 274 | 275 | decltype(m_tasks)::iterator taskIterator(const std::string& id) 276 | { 277 | return std::find_if(m_tasks.begin(), m_tasks.end(), [hash = m_hasher(id)](auto&& it) { 278 | if (it.second.hash().has_value()) 279 | { 280 | return hash == it.second.hash().value(); 281 | } 282 | return false; 283 | }); 284 | } 285 | }; -------------------------------------------------------------------------------- /Advanced-Memory_Allocator/МemoryАllocator.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #if __linux__ != 0 12 | #include 13 | 14 | static uint64_t timer_nsec() 15 | { 16 | #if defined(CLOCK_MONOTONIC_RAW) 17 | const clockid_t clockid = CLOCK_MONOTONIC_RAW; 18 | 19 | #else 20 | const clockid_t clockid = CLOCK_MONOTONIC; 21 | 22 | #endif 23 | 24 | timespec t; 25 | clock_gettime(clockid, &t); 26 | 27 | return t.tv_sec * 1000000000UL + t.tv_nsec; 28 | } 29 | 30 | #elif _WIN64 != 0 31 | #define NOMINMAX 32 | #include 33 | 34 | static struct TimerBase 35 | { 36 | LARGE_INTEGER freq; 37 | TimerBase() 38 | { 39 | QueryPerformanceFrequency(&freq); 40 | } 41 | } timerBase; 42 | 43 | // the order of global initialisaitons is non-deterministic, do 44 | // not use this routine in the ctors of globally-scoped objects 45 | static uint64_t timer_nsec() 46 | { 47 | LARGE_INTEGER t; 48 | QueryPerformanceCounter(&t); 49 | 50 | return 1000000000ULL * t.QuadPart / timerBase.freq.QuadPart; 51 | } 52 | 53 | #elif __APPLE__ != 0 54 | #include 55 | 56 | static struct TimerBase 57 | { 58 | mach_timebase_info_data_t tb; 59 | TimerBase() 60 | { 61 | mach_timebase_info(&tb); 62 | } 63 | } timerBase; 64 | 65 | // the order of global initialisaitons is non-deterministic, do 66 | // not use this routine in the ctors of globally-scoped objects 67 | static uint64_t timer_nsec() 68 | { 69 | const uint64_t t = mach_absolute_time(); 70 | return t * timerBase.tb.numer / timerBase.tb.denom; 71 | } 72 | 73 | #endif 74 | 75 | struct MemoryBlock 76 | { 77 | void* ptr = nullptr; 78 | size_t len = 0; 79 | 80 | MemoryBlock() = default; 81 | MemoryBlock(void* ptr, size_t len) : ptr(ptr), len(len) 82 | {} 83 | inline operator bool() const 84 | { 85 | return len && ptr; 86 | } 87 | }; 88 | 89 | class AllocatorBase 90 | { 91 | protected: 92 | size_t usedMemory = 0; 93 | size_t maxUsedMemory = 0; 94 | 95 | void* malloc(size_t size) 96 | { 97 | usedMemory += size; 98 | if (usedMemory > maxUsedMemory) 99 | { 100 | maxUsedMemory = usedMemory; 101 | } 102 | return std::malloc(size); 103 | } 104 | void free(void* ptr, size_t size) 105 | { 106 | usedMemory -= size; 107 | return std::free(ptr); 108 | } 109 | 110 | public: 111 | inline MemoryBlock alloc(size_t size) 112 | { 113 | return MemoryBlock(); 114 | } 115 | inline void free(MemoryBlock& block) 116 | {} 117 | size_t getMaxUsedMemory() const 118 | { 119 | return maxUsedMemory; 120 | } 121 | }; 122 | 123 | class DefaultAllocator : public AllocatorBase 124 | { 125 | public: 126 | MemoryBlock alloc(size_t size) 127 | { 128 | return MemoryBlock(AllocatorBase::malloc(size), size); 129 | } 130 | 131 | void free(MemoryBlock& block) 132 | { 133 | AllocatorBase::free(block.ptr, block.len); 134 | } 135 | }; 136 | 137 | static size_t align(size_t n) 138 | { 139 | const size_t sizeOfIntptr = sizeof(intptr_t); 140 | if ((sizeOfIntptr == 4 && n <= 4) || (sizeOfIntptr == 8 && n <= 8)) 141 | { 142 | return sizeOfIntptr; 143 | } 144 | 145 | size_t v = n; 146 | v--; 147 | v |= v >> 1; 148 | v |= v >> 2; 149 | v |= v >> 4; 150 | v |= v >> 8; 151 | v |= v >> 16; 152 | v++; 153 | return v; 154 | } 155 | 156 | class MemoryAllocator : public AllocatorBase 157 | { 158 | private: 159 | struct Header 160 | { 161 | Header* prev = nullptr; 162 | Header* next = nullptr; // here we store if the block is used 163 | size_t size; 164 | size_t physicalPrevSize = 0; 165 | 166 | void setUsed(bool used) 167 | { 168 | unsigned mask = used; 169 | next = reinterpret_cast(reinterpret_cast(next) | mask); 170 | } 171 | bool isUsed() const 172 | { 173 | unsigned mask = 1; 174 | return mask & (reinterpret_cast(next)); 175 | } 176 | Header* getPrev() const 177 | { 178 | return prev; 179 | } 180 | Header* getNext() const 181 | { 182 | unsigned mask = 1; 183 | mask = ~mask; 184 | return reinterpret_cast(reinterpret_cast(next) & mask); 185 | } 186 | }; 187 | 188 | void* memory = nullptr; 189 | Header* begin = nullptr; 190 | size_t memorySize = 0; 191 | 192 | Header* initHeader(void* memory, size_t size) const 193 | { 194 | Header* initializedHeader = reinterpret_cast(memory); 195 | initializedHeader->prev = initializedHeader->next = nullptr; 196 | initializedHeader->size = size; 197 | initializedHeader->physicalPrevSize = 0; 198 | return initializedHeader; 199 | } 200 | 201 | Header* findFirstNeededChunk(size_t size) const 202 | { 203 | Header* current = begin; 204 | while (current && current->size < size) 205 | { 206 | current = current->next; 207 | } 208 | return current; 209 | } 210 | 211 | Header* getPhysicalPrevBlock(Header* chunk) const 212 | { 213 | if (chunk == memory) 214 | { 215 | return nullptr; 216 | } 217 | char* currentIt = reinterpret_cast(chunk); 218 | currentIt -= (chunk->physicalPrevSize + sizeof(Header)); 219 | return reinterpret_cast(currentIt); 220 | } 221 | 222 | Header* getPhysicalNextBlock(Header* chunk) const 223 | { 224 | char* currentIt = reinterpret_cast(chunk) + chunk->size + sizeof(Header); 225 | if (currentIt >= reinterpret_cast(memory) + memorySize) 226 | { 227 | return nullptr; 228 | } 229 | return reinterpret_cast(currentIt); 230 | } 231 | 232 | void mergeBlocksRightWasTaken(Header* left, Header* right) 233 | { 234 | left->size = left->size + right->size + sizeof(Header); 235 | setPhysicalPrevOnNext(left); 236 | } 237 | 238 | void mergeBlocksLeftWasTaken(Header* left, Header* right) 239 | { 240 | left->size = left->size + right->size + sizeof(Header); 241 | setPhysicalPrevOnNext(left); 242 | 243 | left->next = right->next; 244 | left->prev = right->prev; 245 | if (right->next) 246 | { 247 | right->next->prev = left; 248 | } 249 | if (right->prev) 250 | { 251 | right->prev->next = left; 252 | } 253 | if (right == begin) 254 | { 255 | begin = left; 256 | } 257 | } 258 | 259 | void mergeThree(Header* first, Header* mid, Header* last) // first and last were free 260 | { 261 | first->size = first->size + mid->size + last->size + 2 * sizeof(Header); 262 | setPhysicalPrevOnNext(first); 263 | removeFromFreeList(first); 264 | removeFromFreeList(last); 265 | 266 | first->next = begin; 267 | begin = first; 268 | } 269 | 270 | void split(Header* chunk, size_t neededBytes) 271 | { 272 | const size_t remainingMemoryWithoutTheNewHeader = chunk->size - sizeof(Header); 273 | const size_t minChunkSize = 16; 274 | if (chunk->size == neededBytes || chunk->size <= minChunkSize || remainingMemoryWithoutTheNewHeader <= neededBytes * 2) 275 | { 276 | return; // no splitting needes 277 | } 278 | 279 | chunk->size = neededBytes; 280 | Header* rightChunk = initHeader( 281 | reinterpret_cast(reinterpret_cast(chunk) + neededBytes + sizeof(Header)), 282 | remainingMemoryWithoutTheNewHeader - chunk->size); 283 | 284 | rightChunk->prev = chunk; 285 | rightChunk->next = chunk->next; 286 | rightChunk->physicalPrevSize = neededBytes; 287 | chunk->next = rightChunk; 288 | 289 | if (rightChunk->next) 290 | { 291 | rightChunk->next->prev = rightChunk; 292 | } 293 | 294 | Header* rightPhysicalNext = getPhysicalNextBlock(rightChunk); 295 | if (rightPhysicalNext) 296 | { 297 | rightPhysicalNext->physicalPrevSize = rightChunk->size; 298 | } 299 | } 300 | 301 | void removeFromFreeList(Header* chunk) 302 | { 303 | Header*& prev = chunk->prev; 304 | Header*& next = chunk->next; 305 | 306 | if (prev) 307 | { 308 | prev->next = next; 309 | } 310 | else 311 | { 312 | begin = next; // chunk is first in the free list 313 | } 314 | 315 | if (next) 316 | { 317 | next->prev = prev; 318 | } 319 | prev = next = nullptr; 320 | } 321 | 322 | void setPhysicalPrevOnNext(Header* chunk) 323 | { 324 | Header* next = getPhysicalNextBlock(chunk); 325 | if (next) 326 | { 327 | next->physicalPrevSize = chunk->size; 328 | } 329 | } 330 | 331 | public: 332 | MemoryAllocator(size_t size = 1024 * 1024 * 512) 333 | { 334 | memory = AllocatorBase::malloc(size); 335 | begin = initHeader(memory, size - sizeof(Header)); 336 | memorySize = size; 337 | } 338 | 339 | ~MemoryAllocator() 340 | { 341 | AllocatorBase::free(memory, memorySize); 342 | } 343 | 344 | MemoryBlock alloc(size_t size) 345 | { 346 | size = align(size); 347 | Header* chunk = findFirstNeededChunk(size); 348 | if (!chunk) 349 | { 350 | throw std::exception("No memory"); 351 | } 352 | 353 | split(chunk, size); 354 | removeFromFreeList(chunk); 355 | chunk->setUsed(1); 356 | return MemoryBlock(reinterpret_cast(chunk) + sizeof(Header), chunk->size); 357 | } 358 | 359 | void free(MemoryBlock& block) 360 | { 361 | if (!block.ptr) 362 | { 363 | return; 364 | } 365 | 366 | Header* toFree = reinterpret_cast(reinterpret_cast(block.ptr) - sizeof(Header)); 367 | toFree->setUsed(0); 368 | 369 | Header* prev = getPhysicalPrevBlock(toFree); 370 | Header* next = getPhysicalNextBlock(toFree); 371 | 372 | if (prev && !prev->isUsed() && next && !next->isUsed()) 373 | { 374 | mergeThree(prev, toFree, next); 375 | } 376 | else if (prev && !prev->isUsed()) 377 | { 378 | mergeBlocksRightWasTaken(prev, toFree); 379 | } 380 | else if (next && !next->isUsed()) 381 | { 382 | mergeBlocksLeftWasTaken(toFree, next); 383 | } 384 | else 385 | { 386 | toFree->next = begin; 387 | begin->prev = toFree; 388 | begin = toFree; 389 | } 390 | } 391 | }; 392 | 393 | //////////////////// 394 | // Tests 395 | //////////////////// 396 | 397 | template 398 | struct Test1 399 | { 400 | static void test(Allocator& allocator, size_t testSize = 1000) 401 | { 402 | std::vector results; 403 | results.reserve(testSize); 404 | 405 | for (size_t i = 0; i < testSize; i++) 406 | { 407 | results.push_back(allocator.alloc(128 * (1 + i % 4))); 408 | } 409 | for (auto& block : results) 410 | { 411 | allocator.free(block); 412 | } 413 | } 414 | }; 415 | 416 | template 417 | struct Test2 418 | { 419 | static void test(Allocator& allocator, size_t testSize = 1000) 420 | { 421 | std::vector results; 422 | results.reserve(testSize * 1.1f); 423 | 424 | for (size_t i = 0; i < testSize; i++) 425 | { 426 | results.push_back(allocator.alloc(i % 2 ? 32 : 64)); 427 | 428 | if (i % 10 == 0) 429 | { 430 | results.push_back(allocator.alloc(4096 * 1024)); 431 | } 432 | } 433 | for (auto& block : results) 434 | { 435 | allocator.free(block); 436 | } 437 | } 438 | }; 439 | 440 | template 441 | struct Test3 442 | { 443 | static void test(Allocator& allocator, size_t testSize = 1000) 444 | { 445 | std::vector results; 446 | results.reserve(testSize); 447 | 448 | for (size_t i = 0; i < testSize; i++) 449 | { 450 | results.push_back(allocator.alloc(i % 2 ? 32 : 2000)); 451 | 452 | // for every 10 blocks free the first 5 of them 453 | if (i % 10 == 4) 454 | { 455 | for (size_t j = 0; j < 5; j++) 456 | { 457 | allocator.free(results[i - j]); 458 | results[i - j].ptr = nullptr; 459 | } 460 | } 461 | } 462 | for (auto& block : results) 463 | { 464 | if (block) 465 | { 466 | allocator.free(block); 467 | } 468 | } 469 | } 470 | }; 471 | 472 | template 473 | struct Test4 474 | { 475 | static void test(Allocator& allocator, size_t testSize = 1000) 476 | { 477 | std::vector results; 478 | results.reserve(testSize * 1.1f); 479 | 480 | for (size_t i = 0; i < testSize; ++i) 481 | { 482 | results.push_back(allocator.alloc(i % 2 ? 32 : 64)); 483 | 484 | if (i % 10 == 0) 485 | { 486 | results.push_back(allocator.alloc(4096 * 1024)); 487 | } 488 | // for every 10 blocks free the first 5 of them 489 | if (i % 10 == 4) 490 | { 491 | for (size_t j = 0; j < 5; j++) 492 | { 493 | allocator.free(results[i - j]); 494 | results[i - j].ptr = nullptr; 495 | } 496 | } 497 | } 498 | for (auto& block : results) 499 | { 500 | if (block) 501 | { 502 | allocator.free(block); 503 | } 504 | } 505 | } 506 | }; 507 | 508 | template