File size: 16,395 Bytes
bd86de0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
"""
USSU Algorithm Analyzer v4.0 - ADA Theory & Analysis Tools
Master Theorem, Recurrence Relations, Amortized Analysis, NP-Completeness.
"""
import math
import random
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass


@dataclass
class MasterTheoremResult:
    a: float
    b: float
    f_n: str
    log_b_a: float
    case: int
    solution: str
    explanation: str
    verified: bool


class ADATools:
    """Analysis and Design of Algorithms theoretical tools"""

    # ==================== ASYMPTOTIC NOTATION REFERENCE ====================
    @staticmethod
    def big_o_reference() -> List[Dict]:
        return [
            {"notation": "O(1)", "name": "Constant", "example": "Array access, hash lookup", "class": "P"},
            {"notation": "O(log n)", "name": "Logarithmic", "example": "Binary search, balanced BST", "class": "P"},
            {"notation": "O(√n)", "name": "Square Root", "example": "Prime checking, Grover's algorithm", "class": "P"},
            {"notation": "O(n)", "name": "Linear", "example": "Linear search, array traversal", "class": "P"},
            {"notation": "O(n log n)", "name": "Linearithmic", "example": "Merge sort, heap sort, Dijkstra", "class": "P"},
            {"notation": "O(n²)", "name": "Quadratic", "example": "Bubble sort, matrix multiplication", "class": "P"},
            {"notation": "O(n³)", "name": "Cubic", "example": "Floyd-Warshall, naive matrix mult", "class": "P"},
            {"notation": "O(2ⁿ)", "name": "Exponential", "example": "Recursive Fibonacci, subsets", "class": "NP"},
            {"notation": "O(n!)", "name": "Factorial", "example": "Traveling salesman brute force", "class": "NP"},
        ]

    @staticmethod
    def asymptotic_definitions() -> Dict[str, str]:
        return {
            "Big-O (O)": "f(n) = O(g(n)) if ∃ c > 0, n₀ > 0 such that 0 ≤ f(n) ≤ c·g(n) ∀ n ≥ n₀. Upper bound.",
            "Big-Omega (Ω)": "f(n) = Ω(g(n)) if ∃ c > 0, n₀ > 0 such that 0 ≤ c·g(n) ≤ f(n) ∀ n ≥ n₀. Lower bound.",
            "Big-Theta (Θ)": "f(n) = Θ(g(n)) if ∃ c₁, c₂ > 0, n₀ > 0 such that c₁·g(n) ≤ f(n) ≤ c₂·g(n) ∀ n ≥ n₀. Tight bound.",
            "Little-o (o)": "f(n) = o(g(n)) if ∀ c > 0, ∃ n₀ > 0 such that 0 ≤ f(n) < c·g(n) ∀ n ≥ n₀. Strictly smaller.",
            "Little-omega (ω)": "f(n) = ω(g(n)) if ∀ c > 0, ∃ n₀ > 0 such that 0 ≤ c·g(n) < f(n) ∀ n ≥ n₀. Strictly larger.",
        }

    # ==================== MASTER THEOREM SOLVER ====================
    @staticmethod
    def master_theorem(a: float, b: float, f_n: str, n: int = 1000) -> MasterTheoremResult:
        """
        Solve T(n) = aT(n/b) + f(n)
        f_n should be in form like 'n', 'n^2', 'n*log(n)', '1', etc.
        """
        log_b_a = math.log(a) / math.log(b)

        # Parse f(n) to determine polynomial degree
        f_n_clean = f_n.replace(" ", "").lower()
        k = 0  # polynomial degree of f(n)

        if f_n_clean in ['1', 'c', 'constant']:
            k = 0
        elif 'n^' in f_n_clean or 'n**' in f_n_clean:
            # Extract exponent
            parts = f_n_clean.split('^') if '^' in f_n_clean else f_n_clean.split('**')
            if len(parts) > 1:
                try:
                    k = float(parts[1].split('*')[0].split('/')[0])
                except:
                    k = 1
        elif f_n_clean == 'n':
            k = 1
        elif 'n*log' in f_n_clean or 'nlog' in f_n_clean:
            k = 1  # n log n is n^1 * polylog, treated as n^1 for case 2 extended
        elif 'log' in f_n_clean and 'n' not in f_n_clean:
            k = 0  # log n is polylog

        epsilon = 1e-9
        diff = k - log_b_a

        if diff < -epsilon:
            case = 1
            solution = f"Θ(n^{log_b_a:.3f})"
            explanation = (f"f(n) = O(n^{k}) is polynomially smaller than n^{log_b_a:.3f}. "
                          f"Since k < log_b(a) = {log_b_a:.3f}, Case 1 applies.")
        elif abs(diff) < epsilon:
            case = 2
            if 'log' in f_n_clean:
                solution = f"Θ(n^{log_b_a:.3f} · log² n)" if 'log' in f_n_clean and k == log_b_a else f"Θ(n^{log_b_a:.3f} · log n)"
                explanation = (f"f(n) = Θ(n^{log_b_a:.3f} · log^k n). Case 2 (extended) applies. "
                              "Work is evenly distributed across levels.")
            else:
                solution = f"Θ(n^{log_b_a:.3f} · log n)"
                explanation = (f"f(n) = Θ(n^{log_b_a:.3f}). Case 2 applies. "
                              "Work at each level is roughly equal.")
        else:
            case = 3
            # Check regularity condition (simplified)
            solution = f"Θ({f_n})"
            explanation = (f"f(n) = Ω(n^{k}) is polynomially larger than n^{log_b_a:.3f}. "
                          f"Since k > log_b(a) = {log_b_a:.3f}, Case 3 applies. "
                          "Root dominates total cost.")

        return MasterTheoremResult(a, b, f_n, log_b_a, case, solution, explanation, True)

    # ==================== RECURSION TREE CALCULATOR ====================
    @staticmethod
    def recursion_tree_cost(a: float, b: float, f_n_func, n: float, levels: int = 6) -> Dict:
        """Calculate level-by-level cost for T(n) = aT(n/b) + f(n)"""
        tree = []
        total_cost = 0.0
        current_n = n
        current_nodes = 1

        for level in range(levels):
            if current_n < 1:
                break
            level_cost = current_nodes * f_n_func(current_n)
            tree.append({
                'level': level,
                'nodes': int(current_nodes),
                'subproblem_size': current_n,
                'cost': level_cost,
            })
            total_cost += level_cost
            current_nodes *= a
            current_n /= b

        return {
            'tree': tree,
            'total_cost_approx': total_cost,
            'levels_computed': len(tree),
        }

    # ==================== SUBSTITUTION METHOD DEMO ====================
    @staticmethod
    def substitution_guess(guess: str, recurrence: str, base_case: str) -> Dict:
        """Demonstrate substitution method with a guess"""
        return {
            'method': 'Substitution',
            'recurrence': recurrence,
            'guess': guess,
            'base_case': base_case,
            'steps': [
                f"1. Guess: T(n) = {guess}",
                f"2. Substitute into recurrence: {recurrence}",
                f"3. Verify base case: {base_case}",
                f"4. Use mathematical induction to prove bound.",
            ],
            'note': 'Substitution requires creativity to guess the correct bound. Often used after recursion tree intuition.'
        }

    # ==================== AMORTIZED ANALYSIS ====================
    @staticmethod
    def amortized_dynamic_array(n_operations: int = 20) -> Dict:
        """Aggregate analysis of dynamic array doubling"""
        costs = []
        size = 1
        count = 0

        for i in range(1, n_operations + 1):
            if count == size:
                # Double: copy all elements + insert new
                cost = size + 1  # size copies + 1 insert
                size *= 2
            else:
                cost = 1
            count += 1
            costs.append({'operation': i, 'actual_cost': cost, 'array_size': size, 'elements': count})

        total = sum(c['actual_cost'] for c in costs)
        amortized = total / n_operations

        return {
            'scenario': 'Dynamic Array (Aggregate Analysis)',
            'operations': costs,
            'total_cost': total,
            'amortized_cost': amortized,
            'theoretical': 2.0,  # O(1) amortized, actual approaches 2 per op
            'explanation': 'Expensive doubling operations are rare. Total cost ≤ 2n, so amortized cost is O(1).'
        }

    @staticmethod
    def amortized_binary_counter(n_bits: int = 8, increments: int = 20) -> Dict:
        """Amortized analysis of binary counter using accounting method"""
        counter = [0] * n_bits
        history = []
        total_flips = 0

        for inc in range(1, increments + 1):
            flips = 0
            i = 0
            while i < n_bits and counter[i] == 1:
                counter[i] = 0
                flips += 1
                i += 1
            if i < n_bits:
                counter[i] = 1
                flips += 1
            total_flips += flips
            history.append({
                'increment': inc,
                'flips': flips,
                'counter_state': ''.join(map(str, reversed(counter))),
                'total_flips': total_flips,
            })

        return {
            'scenario': 'Binary Counter (Accounting Method)',
            'history': history,
            'total_flips': total_flips,
            'amortized_per_increment': total_flips / increments,
            'theoretical': 2.0,
            'explanation': 'Each bit flips every 2^i increments. Total flips for n increments ≤ 2n. Amortized cost per increment: O(1).'
        }

    @staticmethod
    def amortized_stack_multipop(operations: List[str]) -> Dict:
        """Accounting method for stack with multipop"""
        stack = []
        history = []
        total_cost = 0

        for op in operations:
            if op.startswith('push'):
                stack.append(int(op.split()[1]))
                cost = 2  # 1 for push, 1 credit stored on item
                total_cost += cost
                history.append({'op': op, 'stack_size': len(stack), 'actual_cost': 1, 'charge': 2, 'credits': len(stack)})
            elif op.startswith('pop'):
                if stack:
                    stack.pop()
                    cost = 0  # Paid by credit
                    total_cost += 0
                    history.append({'op': op, 'stack_size': len(stack), 'actual_cost': 1, 'charge': 0, 'credits': len(stack)})
            elif op.startswith('multipop'):
                k = int(op.split()[1])
                actual = min(k, len(stack))
                for _ in range(actual):
                    stack.pop()
                cost = 0  # Paid by credits
                total_cost += 0
                history.append({'op': op, 'stack_size': len(stack), 'actual_cost': actual, 'charge': 0, 'credits': len(stack)})

        return {
            'scenario': 'Stack with Multipop (Accounting Method)',
            'history': history,
            'total_amortized_cost': total_cost,
            'explanation': 'Charge 2 per push: 1 for push, 1 credit stored. Pop/Multipop use stored credits. Amortized cost per operation: O(1).'
        }

    # ==================== PROBABILISTIC ANALYSIS ====================
    @staticmethod
    def hiring_problem(n_candidates: int) -> Dict:
        """Expected cost of hiring problem (order statistics)"""
        # Expected number of hires = H_n = ln(n) + gamma
        harmonic = sum(1/i for i in range(1, n_candidates + 1))
        return {
            'problem': 'Hiring Problem',
            'n_candidates': n_candidates,
            'expected_hires': harmonic,
            'approximation': math.log(n_candidates) + 0.5772,
            'explanation': 'With random order, expected hires = H_n ≈ ln(n) + γ. Each candidate has 1/i chance of being best so far.'
        }

    @staticmethod
    def randomized_quicksort_analysis(n: int) -> Dict:
        """Expected comparisons for randomized quicksort"""
        # E[comparisons] ≈ 2n ln n - 1.51n
        expected = 2 * n * math.log(n) - 1.51 * n if n > 1 else 0
        return {
            'algorithm': 'Randomized Quicksort',
            'input_size': n,
            'expected_comparisons': expected,
            'expected_time': f'O(n log n) ≈ {expected:.0f} comparisons',
            'explanation': 'Random pivot selection makes running time independent of input distribution. Expected comparisons ≈ 2n ln n.'
        }

    # ==================== NP-COMPLETENESS REFERENCE ====================
    @staticmethod
    def np_completeness_reference() -> Dict[str, List[Dict]]:
        return {
            'complexity_classes': [
                {'class': 'P', 'definition': 'Decision problems solvable in polynomial time by deterministic Turing machine.', 'examples': ['Sorting', 'Shortest Path', 'MST', 'Matching']},
                {'class': 'NP', 'definition': 'Decision problems where solutions can be verified in polynomial time.', 'examples': ['SAT', 'Clique', 'Hamiltonian Cycle', 'Subset Sum']},
                {'class': 'NP-Complete', 'definition': 'Problems in NP that are at least as hard as every problem in NP. If one is in P, all are.', 'examples': ['3-SAT', 'Vertex Cover', 'TSP (decision)', 'Graph Coloring']},
                {'class': 'NP-Hard', 'definition': 'Problems at least as hard as NP-Complete, but not necessarily in NP.', 'examples': ['TSP (optimization)', 'Halting Problem', 'Chess Generalization']},
                {'class': 'co-NP', 'definition': 'Complements of NP problems. Verifying NO answers in polynomial time.', 'examples': ['co-SAT', 'Primality (now in P)']},
            ],
            'reductions': [
                {'from': '3-SAT', 'to': 'Clique', 'type': '≤p'},
                {'from': 'Clique', 'to': 'Vertex Cover', 'type': '≤p'},
                {'from': 'Vertex Cover', 'to': 'Hamiltonian Cycle', 'type': '≤p'},
                {'from': 'Hamiltonian Cycle', 'to': 'TSP', 'type': '≤p'},
            ],
            'open_problems': [
                'P vs NP (Millennium Prize Problem, $1M)',
                'NP vs co-NP',
                'P = BPP? (Randomized algorithms)',
                'Quantum polynomial time (BQP) vs NP',
            ]
        }

    # ==================== PARADIGM COMPARISON ====================
    @staticmethod
    def paradigm_comparison() -> List[Dict]:
        return [
            {
                'paradigm': 'Divide & Conquer',
                'approach': 'Break into independent subproblems, solve recursively, combine.',
                'examples': ['Merge Sort', 'Quick Sort', 'Strassen Matrix', 'Closest Pair'],
                'key_property': 'Optimal substructure',
                'when_to_use': 'Subproblems are independent and non-overlapping.',
                'time_pattern': 'T(n) = aT(n/b) + f(n)',
            },
            {
                'paradigm': 'Dynamic Programming',
                'approach': 'Break into overlapping subproblems, memoize to avoid recomputation.',
                'examples': ['Knapsack', 'LCS', 'Edit Distance', 'Matrix Chain'],
                'key_property': 'Optimal substructure + Overlapping subproblems',
                'when_to_use': 'Subproblems overlap and optimal solution contains optimal subsolutions.',
                'time_pattern': 'Usually polynomial in states',
            },
            {
                'paradigm': 'Greedy',
                'approach': 'Make locally optimal choice at each step, never reconsider.',
                'examples': ['Dijkstra', 'Prim/Kruskal', 'Huffman', 'Activity Selection'],
                'key_property': 'Greedy choice property + Optimal substructure',
                'when_to_use': 'Local optimal leads to global optimal. Proof by exchange argument.',
                'time_pattern': 'Often O(n log n) due to sorting',
            },
            {
                'paradigm': 'Backtracking',
                'approach': 'DFS over state space, prune invalid branches.',
                'examples': ['N-Queens', 'Sudoku', 'Graph Coloring', 'Subset Sum'],
                'key_property': 'State space exploration with constraints',
                'when_to_use': 'Need all solutions or exact answer, exponential space acceptable.',
                'time_pattern': 'Exponential in worst case',
            },
            {
                'paradigm': 'Branch & Bound',
                'approach': 'BFS/DFS with bounds to prune suboptimal branches.',
                'examples': ['TSP', 'Job Assignment', 'Knapsack (0/1)'],
                'key_property': 'Bound function + Priority queue',
                'when_to_use': 'Optimization problems where bounds can prune effectively.',
                'time_pattern': 'Exponential worst, often better in practice',
            },
        ]