Algorithms and Complexity Assignment Solutions
Algorithms and Complexity Assignment Solutions
The dynamic programming approach involves building a table to store sub-problem solutions. The primary idea is to find the optimal place to parenthesize the product to minimize the number of scalar multiplications. The process starts by filling in the cost of multiplying two adjacent matrices, then extends to sets of three matrices, continuing until it covers the entire chain. The key insight is splitting matrices [Ai...Aj] at every possible k and computing the cost of each configuration: cost = m[i][k] + m[k+1][j] + dimensions[i-1]*dimensions[k]*dimensions[j]. It utilizes overlapping subproblems and optimal substructure properties for computation. The minimal value represents the final solution. The approach requires O(n^3) time and O(n^2) space .
Prim's Algorithm builds the MST by growing one edge from an existing tree incrementally, focused on adjacent expansions, often utilizing a priority queue to select the minimum weight edge, leading to a time complexity of O(V^2) or O(E + log V) with Fibonacci heaps. Kruskal's Algorithm begins with the smallest edge and works outwards, adding edges as long as they don't form a cycle. It's more efficient on sparse graphs given its edge-based sorting, and its time complexity is O(E log V). Prim's is inherently greedy within a node-focused scope, contrasting Kruskal's edge-driven emphasis .
Merge Sort has a guaranteed time complexity of O(n log n) as it consistently splits the array in half and merges them back in linear time, whereas Quick Sort's efficiency largely depends on the choice of the pivot. Quick Sort is faster in practice due to better cache performance as it operates in-place and does not require additional memory for merges like Merge Sort. Quick Sort outperforms Merge Sort primarily when the pivot choice prevents skewed partitions, maintaining closer to O(n log n) complexity. If carefully implemented, especially by randomizing pivots or using median-of-three selections, it can perform exceedingly well even on average cases .
Dijkstra's Algorithm fails to find the shortest path in graphs with negative weight edges, as it assumes that once a vertex is processed and its shortest path determined, it need not be reconsidered. In the presence of negative edges, paths may change even after finalization, invalidating calculations. This limitation can be addressed by using the Bellman-Ford algorithm, which can handle negative weights and detect negative cycles, albeit with a higher time complexity of O(VE), where V is the number of vertices and E is the number of edges .
The Ford-Fulkerson method works by repeatedly finding augmenting paths from the source to the sink in a flow network, increasing the flow along these paths until no more augmenting paths can be found. The method's maximal flow depends on selecting augmenting paths using Breadth-First Search (BFS), ensuring efficient updates to path flows without cycles diminishing progress. Infinite loops can arise with irrational numbers, necessitating the choice of integral capacities where applicable. Proper tracking of remaining capacities prevents edge oversaturation. Its time complexity is dictated by O(E*f) steps, where E is the number of edges and f is the maximum flow .
Kruskal's Algorithm involves sorting all the edges of the graph by weight in non-decreasing order. It then iteratively adds edges to the Minimum Spanning Tree (MST) without forming a cycle until (n-1) edges are added, where n is the number of vertices. The critical factors affecting its time complexity are the edge-sorting step, typically O(E log E), where E is the number of edges, and the operations needed to check and manage cycles during the edge-adding process, generally handled by a Disjoint Set Union (DSU) structure .
The substitution method involves guessing a solution and proving it by induction. For T(n)=2T(n/2)+n/log n, assume T(n) = O(n) substantiates a simplistic check. Let's propose T(n) ≤ cn for some constant c, and verify by substitution: T(n) = 2T(n/2) + n/log n ≤ 2(cn/2) + n/log n = cn + n/log n. Because 1/log n decreases towards zero for large n, cn dominates, maintaining T(n) ≤ O(n). Verification using induction complements Master Theorem's spurious compliance, suggesting T(n) = O(n log n), yet attenuation implies T(n) ≤ cn for practical boundary preservation .
To solve T(n) = T(√n) + n via the recursion tree, decompose the problem in several levels. At level i, the input size becomes (√n)^(1/2^i) down to T(1). The depth of the recursion tree can be found by solving n^(1/2^d) = 1; hence, d = log(log(n)). The cost at each level is n, leading to total work represented by a geometric series with d terms, each costing n. Thus, the complexity is O(n log log n), as every division into smaller problems multiplies the temporal complexity by that logarithmic depth without varying .
The Master Theorem can be applied to T(n) = 7T(n/2) + n by matching it to the form T(n) = aT(n/b) + f(n), where a = 7, b = 2, and f(n) = n. Here, f(n) = n is O(n^log_b(a)) because n is O(n^log_2(7)), which makes them asymptotically equal. According to the Master Theorem, if f(n) is Θ(n^c) for c < log_b(a), then T(n) is Θ(n^log_b(a)). Since c = 1 and log_2(7) > 1, T(n) is Θ(n^log_2(7)), indicating a time complexity of O(n^log_2(7)).
Improvements for quick sort regarding pivot selection focus on minimizing the chances of worst-case performance by avoiding consistently poor pivot choices like the smallest or largest element. Selecting the median of three elements (first, middle, last), using randomization, or employing a median-of-medians approach reduces the likelihood of skewed partitions. This practice enhances the average-case performance by balancing subarrays more effectively, maintaining closer to O(n log n) in the average case, compared to the potential O(n^2) with poor pivot choices. Such strategies align the theoretical assurances with practical efficiency gains .