Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The architectural choices, trade-offs, and design patterns for each algorithm ar
* [ADR 0013: Union by Rank with Path Compression for Union-Find](docs/adr/0013-use-union-by-rank-with-path-compression-for-union-find.md)
* [ADR 0014: Bottom-Up Divide-and-Conquer Merge for Merge Sort](docs/adr/0014-use-bottom-up-divide-and-conquer-merge-for-merge-sort.md)
* [ADR 0015: Iterative Pointer Rewiring for Singly Linked List Reversal](docs/adr/0015-use-iterative-pointer-rewiring-for-singly-linked-list-reversal.md)
* [ADR 0016: Bottom-Up Tabulation with Backtracking for 0/1 Knapsack](docs/adr/0016-use-bottom-up-tabulation-with-backtracking-for-01-knapsack.md)

---

Expand All @@ -116,4 +117,5 @@ To ensure uniformity, this repository follows strict standards derived from **PE
11. **Service Layer Input Validation:** The HTTP API validates request bodies via Pydantic schemas and translates algorithm-level `ValueError`s into HTTP 400 responses rather than leaking stack traces; both the MCP server and HTTP API are stateless per call, so no client-supplied data persists across requests.
12. **Fixed Element Universe:** Union-Find validates every `find()`/`union()` call against its initial element set and raises a `ValueError` for unknown elements, preventing silent creation of untracked entries.
13. **Worst-Case DoS Mitigation:** Merge Sort guarantees $O(n \log n)$ even on adversarial input, making it the safer default over Quicksort when sorting untrusted, attacker-influenced data where worst-case scaling matters.
14. **Bounded Traversal Footprint:** The Singly Linked List's `search`/`delete`/`reverse` operations are strictly O(n) iterative walks with no recursion, preventing stack-depth exhaustion on very large untrusted input lists.
14. **Bounded Traversal Footprint:** The Singly Linked List's `search`/`delete`/`reverse` operations are strictly O(n) iterative walks with no recursion, preventing stack-depth exhaustion on very large untrusted input lists.
15. **Iterative DP, No Recursion Limits:** The 0/1 Knapsack solver uses bottom-up tabulation rather than top-down recursion, avoiding Python's `RecursionError` on large item counts.
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This document serves as the long-term architectural roadmap for this learning re
* **Singly Linked List**: Pointer-chained sequential collection supporting traversal, insertion, and reversal operations. (Completed)

### Phase 5: Dynamic Programming & Sequence Analysis
* **0/1 Knapsack Problem**: Tabular matrix memoization framework designed to resolve finite profit boundaries.
* **0/1 Knapsack Problem**: Tabular matrix memoization framework designed to resolve finite profit boundaries. (Completed)
* **Longest Common Subsequence (LCS)**: Relational alignment mapping tracking matching sub-segments within strings.

---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 16. Use Bottom-Up Tabulation with Backtracking for 0/1 Knapsack

* **Status:** Approved
* **Context:** Phase 5 required a solver for the classic 0/1 Knapsack Problem — selecting a subset of items, each usable at most once, to maximize total value without exceeding a fixed weight capacity. A brute-force approach is exponential ($O(2^n)$), motivating a polynomial dynamic programming solution.
* **Decision:** We implemented **bottom-up tabulation**: a `(items + 1) x (capacity + 1)` matrix where `table[i][c]` holds the best value achievable using the first `i` items within capacity `c`, built iteratively rather than via top-down memoized recursion. A backtracking pass then walks the completed matrix in reverse to recover which specific items were selected.
* **Consequences:**
* Achieves $O(n \times \text{capacity})$ time and space complexity — pseudo-polynomial, but efficient for any bounded capacity encountered in practice.
* The iterative tabulation approach avoids Python's recursion depth limits entirely, unlike a naive top-down memoized recursive solution which could hit `RecursionError` on a large item count.
* *Trade-off:* The full `(items + 1) x (capacity + 1)` matrix is retained in memory to support backtracking; a space-optimized single-row variant would reduce memory to $O(\text{capacity})$ but would lose the ability to reconstruct which items were chosen without additional bookkeeping.

14 changes: 14 additions & 0 deletions service/http_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ class LinkedListRequest(BaseModel):
reverse: bool = False


class KnapsackRequest(BaseModel):
weights: List[int]
values: List[int]
capacity: int


class WeightedGraphRequest(BaseModel):
graph: Dict[str, List[List[object]]]
source: str
Expand Down Expand Up @@ -145,6 +151,14 @@ def build_and_query_linked_list(request: LinkedListRequest) -> Dict:
)


@app.post("/dynamic-programming/knapsack")
def dp_knapsack_01(request: KnapsackRequest) -> Dict:
"""Selects a subset of items maximizing total value within a fixed weight capacity."""
return _call(
tools.dp_knapsack_01, request.weights, request.values, request.capacity
)


@app.post("/graphs/dijkstra")
def graph_dijkstra(request: WeightedGraphRequest) -> Dict:
"""Computes single-source shortest paths using Dijkstra's algorithm."""
Expand Down
6 changes: 6 additions & 0 deletions service/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ def build_and_query_linked_list(
return tools.build_and_query_linked_list(values, search_for, reverse)


@mcp.tool()
def dp_knapsack_01(weights: List[int], values: List[int], capacity: int) -> Dict:
"""Selects a subset of items maximizing total value within a fixed weight capacity."""
return tools.dp_knapsack_01(weights, values, capacity)


@mcp.tool()
def graph_dijkstra(graph: tools.WeightedGraph, source: str) -> Dict:
"""Computes single-source shortest paths using Dijkstra's algorithm."""
Expand Down
7 changes: 7 additions & 0 deletions service/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from src.data_structures.dijkstra import dijkstra
from src.data_structures.linked_list import SinglyLinkedList
from src.data_structures.union_find import UnionFind
from src.dynamic_programming.knapsack import knapsack_01
from src.graphs.a_star import a_star
from src.graphs.bellman_ford import bellman_ford
from src.graphs.topological_sort import topological_sort
Expand Down Expand Up @@ -120,6 +121,12 @@ def build_and_query_linked_list(
return result


def dp_knapsack_01(weights: List[int], values: List[int], capacity: int) -> Dict:
"""Selects a subset of items maximizing total value within a fixed weight capacity."""
max_value, selected_indices = knapsack_01(weights, values, capacity)
return {"max_value": max_value, "selected_indices": selected_indices}


def graph_dijkstra(graph: WeightedGraph, source: str) -> Dict:
"""Computes single-source shortest paths using Dijkstra's algorithm."""
distances, predecessors = dijkstra(_to_adjacency_tuples(graph), source)
Expand Down
Empty file.
62 changes: 62 additions & 0 deletions src/dynamic_programming/knapsack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""0/1 Knapsack Problem solver using bottom-up tabular dynamic programming memoization."""

from typing import List, Tuple


def knapsack_01(
weights: List[int], values: List[int], capacity: int
) -> Tuple[int, List[int]]:
"""Selects a subset of items maximizing total value within a fixed weight capacity.

Each item may be taken at most once (0/1 constraint, as opposed to the
unbounded/fractional variants).

Complexity Analysis:
Time Complexity: O(n * capacity) where n = number of items.
Space Complexity: O(n * capacity) for the tabulation matrix.
"""
if len(weights) != len(values):
raise ValueError("Weights and values lists must be the same length.")

# Guard clause against malicious or invalid negative-magnitude inputs
if (
capacity < 0
or any(weight < 0 for weight in weights)
or any(value < 0 for value in values)
):
raise ValueError("Capacity, weights, and values must be non-negative.")

item_count = len(weights)

# table[i][c] = best achievable value using the first i items within capacity c
table = [[0] * (capacity + 1) for _ in range(item_count + 1)]

for i in range(1, item_count + 1):
weight, value = weights[i - 1], values[i - 1]
for c in range(capacity + 1):
# Excluding the current item always remains a valid baseline option
table[i][c] = table[i - 1][c]

# Including the current item is only possible if it fits within capacity
if weight <= c:
table[i][c] = max(table[i][c], table[i - 1][c - weight] + value)

selected_indices = _backtrack_selection(table, weights, capacity)
return table[item_count][capacity], selected_indices


def _backtrack_selection(
table: List[List[int]], weights: List[int], capacity: int
) -> List[int]:
"""Walks the completed tabulation matrix backward to recover which items were chosen."""
selected_indices: List[int] = []
remaining_capacity = capacity

for i in range(len(weights), 0, -1):
# A changed value versus the row above means item (i - 1) was included
if table[i][remaining_capacity] != table[i - 1][remaining_capacity]:
selected_indices.append(i - 1)
remaining_capacity -= weights[i - 1]

selected_indices.reverse()
return selected_indices
56 changes: 56 additions & 0 deletions tests/test_dynamic_programming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Comprehensive evaluation suite tracking 0/1 Knapsack Problem resolution."""

import pytest
from src.dynamic_programming.knapsack import knapsack_01


def test_knapsack_typical_selection():
"""Verifies the optimal value and item selection for a classic textbook scenario."""
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
max_value, selected = knapsack_01(weights, values, capacity=7)

assert max_value == 9
assert selected == [1, 2]


def test_knapsack_zero_capacity():
"""Ensures zero capacity yields zero value and no selected items."""
max_value, selected = knapsack_01([1, 2], [10, 20], capacity=0)

assert max_value == 0
assert selected == []


def test_knapsack_empty_items():
"""Ensures an empty item set yields zero value regardless of capacity."""
max_value, selected = knapsack_01([], [], capacity=10)

assert max_value == 0
assert selected == []


def test_knapsack_item_exceeding_capacity_is_excluded():
"""Ensures an item heavier than the capacity is never selected."""
max_value, selected = knapsack_01([10], [100], capacity=5)

assert max_value == 0
assert selected == []


def test_knapsack_mismatched_lengths_raises():
"""Ensures mismatched weights/values lengths raise a ValueError safely."""
with pytest.raises(ValueError, match="same length"):
knapsack_01([1, 2], [10], capacity=5)


def test_knapsack_rejects_negative_inputs():
"""Ensures negative capacity, weights, or values raise a ValueError safely."""
with pytest.raises(ValueError, match="non-negative"):
knapsack_01([1], [10], capacity=-1)

with pytest.raises(ValueError, match="non-negative"):
knapsack_01([-1], [10], capacity=5)

with pytest.raises(ValueError, match="non-negative"):
knapsack_01([1], [-10], capacity=5)
10 changes: 10 additions & 0 deletions tests/test_service_http_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ def test_http_build_and_query_linked_list():
assert response.json() == {"values": [3, 2, 1], "found": True}


def test_http_dp_knapsack_01():
"""Verifies the Knapsack endpoint returns the optimal value and selected indices."""
response = client.post(
"/dynamic-programming/knapsack",
json={"weights": [1, 3, 4, 5], "values": [1, 4, 5, 7], "capacity": 7},
)
assert response.status_code == 200
assert response.json() == {"max_value": 9, "selected_indices": [1, 2]}


def test_http_graph_dijkstra():
"""Verifies the Dijkstra endpoint computes shortest path distances."""
response = client.post(
Expand Down
9 changes: 9 additions & 0 deletions tests/test_service_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def test_mcp_tool_registry_contains_all_algorithms():
"build_and_query_avl_tree",
"build_and_query_union_find",
"build_and_query_linked_list",
"dp_knapsack_01",
"graph_dijkstra",
"graph_bellman_ford",
"graph_a_star",
Expand Down Expand Up @@ -81,6 +82,14 @@ def test_mcp_build_and_query_linked_list():
assert result == {"values": [3, 2, 1], "found": True}


def test_mcp_dp_knapsack_01():
"""Verifies the Knapsack tool returns the optimal value and selected item indices."""
result = mcp_server.dp_knapsack_01(
weights=[1, 3, 4, 5], values=[1, 4, 5, 7], capacity=7
)
assert result == {"max_value": 9, "selected_indices": [1, 2]}


def test_mcp_graph_dijkstra():
"""Verifies the Dijkstra tool computes shortest path distances."""
graph = {"A": [["B", 1]], "B": [["C", 2]], "C": []}
Expand Down
6 changes: 6 additions & 0 deletions tests/test_service_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ def test_build_and_query_linked_list_without_search_or_reverse():
assert "found" not in result


def test_dp_knapsack_01():
"""Verifies Knapsack wrapper returns the optimal value and selected item indices."""
result = tools.dp_knapsack_01(weights=[1, 3, 4, 5], values=[1, 4, 5, 7], capacity=7)
assert result == {"max_value": 9, "selected_indices": [1, 2]}


def test_graph_dijkstra():
"""Verifies Dijkstra wrapper converts JSON edge lists and computes distances."""
graph = {"A": [["B", 1]], "B": [["C", 2]], "C": []}
Expand Down
Loading