Applications in Computer Science
âšī¸ Why It Matters
Discrete mathematics is the language of computer science. Every algorithm, data structure, and computational system relies on discrete mathematical foundations. This module connects abstract concepts to real-world applications: cryptography secures communications, coding theory prevents data corruption, scheduling optimizes resource allocation, and networking protocols ensure reliable delivery. Understanding these applications is essential for software engineers, security professionals, and AI practitioners.
Algorithm Analysis
DfTime Complexity
Time complexity measures how the number of operations grows with input size . It is expressed using Big-O notation: means grows no faster than asymptotically.
DfSpace Complexity
Space complexity measures the total memory space required by an algorithm as a function of input size.
ThMaster Theorem for Divide-and-Conquer
For : if then ; if then ; if then .
import time
import random
def measure_time(func, *args, runs=100):
total = 0
for _ in range(runs):
start = time.time()
func(*args)
total += time.time() - start
return total / runs
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
sizes = [100, 1000, 5000]
for n in sizes:
arr = random.sample(range(n * 10), n)
t_bubble = measure_time(bubble_sort, arr.copy(), runs=10)
t_merge = measure_time(merge_sort, arr.copy(), runs=10)
print(f"n={n}: Bubble={t_bubble:.4f}s, Merge={t_merge:.4f}s")
Cryptography
DfRSA Encryption
RSA uses the difficulty of factoring (product of two large primes). Public key: . Private key: . Encrypt: . Decrypt: .
DfDiffie-Hellman Key Exchange
Two parties agree on public prime and generator . Alice sends , Bob sends . Shared secret: .
DfHash Function
A hash function maps arbitrary input to fixed-size output. Properties: preimage resistance, second preimage resistance, collision resistance.
def extended_gcd(a, b):
if a == 0:
return b, 0, 1
g, x, y = extended_gcd(b % a, a)
return g, y - (b // a) * x, x
def mod_inverse(a, m):
g, x, _ = extended_gcd(a % m, m)
if g != 1:
raise ValueError("No inverse")
return x % m
def rsa_demo():
p, q = 61, 53
n = p * q
phi = (p - 1) * (q - 1)
e = 17
d = mod_inverse(e, phi)
message = 65
encrypted = pow(message, e, n)
decrypted = pow(encrypted, d, n)
print(f"Public key: ({n}, {e})")
print(f"Private key: {d}")
print(f"Original: {message}")
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {decrypted}")
rsa_demo()
Coding Theory
DfError-Detecting Code
A code adds redundancy to data so errors can be detected. The Hamming distance between two codewords is the number of positions where they differ.
DfHamming Code
Hamming code adds parity bits to detect and correct single-bit errors. For parity bits, it protects data bits.
def hamming_encode(data_bits):
m = len(data_bits)
r = 0
while (1 << r) < m + r + 1:
r += 1
total = m + r
code = [0] * (total + 1)
j = 0
for i in range(1, total + 1):
if i & (i - 1) != 0:
code[i] = int(data_bits[j])
j += 1
for i in range(r):
pos = 1 << i
parity = 0
for j in range(1, total + 1):
if j & pos:
parity ^= code[j]
code[pos] = parity
return code[1:]
def hamming_count_errors(received):
r = 0
while (1 << r) < len(received):
r += 1
error_pos = 0
for i in range(r):
pos = 1 << i
parity = 0
for j in range(len(received)):
if (j + 1) & pos:
parity ^= received[j]
if parity:
error_pos += pos
return error_pos
data = [1, 0, 1, 1]
encoded = hamming_encode(data)
print(f"Data: {data}")
print(f"Encoded: {encoded}")
received = encoded.copy()
received[3] = 1 - received[3]
error = hamming_count_errors(received)
print(f"Error position: {error}")
Scheduling
DfJob Scheduling
Given jobs with durations and deadlines, schedule them to minimize lateness or maximize throughput. Greedy algorithms using sorting by earliest deadline provide optimal solutions for single-machine scheduling.
DfTopological Sort
For tasks with dependencies, topological sort produces a valid execution order. Used in build systems, task managers, and project planning.
from collections import defaultdict, deque
def topological_sort_dfs(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
order = []
visited = set()
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
order.append(node)
for i in range(n):
if i not in visited:
dfs(i)
return order[::-1]
def topological_sort_kahn(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
queue = deque([i for i in range(n) if in_degree[i] == 0])
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return order if len(order) == n else []
edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]
print("DFS order:", topological_sort_dfs(5, edges))
print("Kahn order:", topological_sort_kahn(5, edges))
Networking
DfShortest Path
Dijkstra's algorithm finds shortest paths from a source to all vertices in a weighted graph with non-negative weights. Time complexity: with a priority queue.
DfNetwork Flow
The max-flow min-cut theorem states the maximum flow equals the minimum cut capacity. Used in transportation, communication, and resource allocation.
import heapq
def dijkstra(graph, start, n):
dist = [float('inf')] * n
dist[start] = 0
prev = [-1] * n
pq = [(0, start)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, weight in graph[u]:
if dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
prev[v] = u
heapq.heappush(pq, (dist[v], v))
return dist, prev
def get_path(prev, target):
path = []
current = target
while current != -1:
path.append(current)
current = prev[current]
return path[::-1]
graph = defaultdict(list)
edges = [(0, 1, 4), (0, 2, 1), (1, 3, 1), (2, 1, 2), (2, 3, 5)]
for u, v, w in edges:
graph[u].append((v, w))
dist, prev = dijkstra(graph, 0, 4)
for i in range(4):
path = get_path(prev, i)
print(f"Distance to {i}: {dist[i]}, Path: {path}")
Applications in AI/ML
âšī¸ Applications in AI and Machine Learning
- Optimization: Graph algorithms solve vehicle routing, supply chain, and logistics problems
- Cryptography: Secure ML requires encrypted computation (homomorphic encryption, secure multi-party computation)
- Natural Language Processing: Finite automata for tokenization; graph algorithms for parsing; information theory for compression
- Computer Vision: Graph cuts for image segmentation; shortest paths for object tracking
- Reinforcement Learning: Markov Decision Processes are state-transition graphs; shortest paths relate to value iteration
- Recommendation Systems: Bipartite matching, network flow for resource allocation
- AutoML: Scheduling and resource allocation for neural architecture search
- Edge AI: Coding theory enables efficient data transmission over noisy channels
Common Mistakes
| Mistake | Correction |
|---|---|
| Using when exists | Always check if a better algorithm is available for the problem size |
| Ignoring constant factors in Big-O | Big-O describes asymptotic growth; constants matter for small inputs |
| Forgetting that hashing can have collisions | Hash tables have average but worst case |
| Confusing average and worst case | Quick sort: average, worst |
| Using greedy without proof of optimality | Greedy works for matroids and certain scheduling problems; not always optimal |
| Not considering space-time tradeoffs | Memoization trades space for time; precomputation trades space for query speed |
| Assuming all graphs are sparse | Dense graphs () need different algorithms than sparse graphs |
| Ignoring numerical precision in crypto | Cryptographic operations require arbitrary-precision arithmetic |
Interview Questions
-
What is the time complexity of common sorting algorithms? Bubble: , Merge: , Quick: average, Heap: . Merge and heap are guaranteed; quick is fastest in practice.
-
How does RSA encryption work? Choose primes , compute and . Pick coprime to . Private key . Encrypt: . Decrypt: . Security relies on factoring difficulty.
-
What is dynamic programming and when should you use it? DP solves problems with overlapping subproblems and optimal substructure. Store subproblem results to avoid recomputation. Use when a naive recursive solution has exponential complexity.
-
How does Dijkstra's algorithm work? Greedily relax edges from the closest unvisited vertex using a priority queue. Correct only for non-negative weights. For negative weights, use Bellman-Ford.
-
What is the difference between BFS and DFS for shortest path? BFS finds shortest paths in unweighted graphs. Dijkstra handles weighted graphs. DFS does not guarantee shortest paths but is useful for cycle detection and topological sorting.
-
What is network flow and where is it used? Maximum flow from source to sink in a capacity-constrained network. Applications: transportation, communication bandwidth, bipartite matching, image segmentation.
-
How do error-correcting codes work? Add redundancy bits so errors can be detected and corrected. Hamming distance determines error capability: distance detects errors and corrects errors.
-
What is the relationship between P and NP? P: problems solvable in polynomial time. NP: problems verifiable in polynomial time. P is a subset of NP. Whether P = NP is the most important open problem in computer science.
Practice Problems
đPractice: Time Complexity
Problem: What is the time complexity of binary search?
đĄSolution: Time Complexity
. Each comparison eliminates half the remaining elements. After comparisons, elements remain. We need , so .
đPractice: RSA
Problem: If , and , find the private key .
đĄSolution: RSA
, . Need such that . Using extended Euclidean algorithm: . So .
đPractice: Topological Sort
Problem: Tasks: 0 depends on none, 1 depends on 0, 2 depends on 0, 3 depends on 1 and 2. Find a valid order.
đĄSolution: Topological Sort
Dependencies: . Valid orders: or . Multiple valid topological orderings exist.
đPractice: Shortest Path
Problem: Find shortest path from vertex 0 in graph: edges (0,1,2), (0,2,5), (1,2,1), (1,3,6), (2,3,2).
đĄSolution: Shortest Path
Dijkstra from 0: dist[0]=0, dist[1]=2, dist[2]=3 (via 1), dist[3]=5 (via 2). Shortest path to 3: 0 -> 1 -> 2 -> 3 with total weight 5.
Quick Reference
| Application | Key Concept | Complexity |
|---|---|---|
| Binary Search | Divide and conquer on sorted array | |
| Merge Sort | Recursively split and merge | |
| Dijkstra | Shortest path, non-negative weights | |
| RSA | Modular exponentiation, prime factoring | for -bit numbers |
| Hamming Code | Single error correction | Linear time |
| Topological Sort | DAG ordering | |
| Dynamic Programming | Overlapping subproblems | Problem-dependent |
| Hash Table | Average-case constant lookup | average |
Cross-References
- Graph Theory â 074-discrete-graphs.mdx: Networks, social graphs, routing algorithms
- Trees â 075-discrete-trees.mdx: Decision trees, spanning trees, search trees
- Recurrence Relations â 076-discrete-recurrence.mdx: Algorithm complexity analysis, Master theorem
- Number Theory â 077-discrete-number-theory.mdx: Cryptography, modular arithmetic, hashing
- Boolean Algebra â 078-discrete-boolean.mdx: Circuit design, logic optimization, search queries
- Automata Theory â 079-discrete-automata.mdx: Compilers, pattern matching, protocol verification