|
| 1 | +""" |
| 2 | +This function builds a Directed Acyclic Graph representation of the Knapsack problem. |
| 3 | +This allows us to solve knapsack-style problems with Shortest Path algorithms. |
| 4 | +See https://github.com/Matti02co/graph-based-scheduling for more details. |
| 5 | +
|
| 6 | +The graph consists of n+2 layers: |
| 7 | +- Layer 0 contains the source node 's'. |
| 8 | +- Layer n+1 contains the sink node 't'. |
| 9 | +- Intermediate layers (1..n) correspond to the n items. |
| 10 | +
|
| 11 | +Each intermediate layer j has (capacity + 1) nodes: j0, j1, ..., jC, |
| 12 | +where jk represents the state of having considered the first j items |
| 13 | +with a total weight of k so far. |
| 14 | +
|
| 15 | +From each node jk there are at most two outgoing edges: |
| 16 | +- Skip item j+1 (weight and cost remain the same). |
| 17 | +- Take item j+1 (only if total weight + item's weight ≤ capacity), with a cost |
| 18 | + equal to the negative of the item's value (we solve via shortest path). |
| 19 | +
|
| 20 | +All nodes in the last layer are connected to 't' with zero-cost edges. |
| 21 | +
|
| 22 | +In this representation, every path from 's' to 't' corresponds to a feasible |
| 23 | +Knapsack solution, and the shortest path (negative costs) corresponds to |
| 24 | +the maximum total value selection. |
| 25 | +""" |
| 26 | + |
| 27 | +from typing import Any |
| 28 | + |
| 29 | + |
| 30 | +def generate_knapsack_graph( |
| 31 | + capacity: int, weights: list[int], values: list[int] |
| 32 | +) -> list[dict[str, Any]]: |
| 33 | + """ |
| 34 | + Generate a Directed Acyclic Graph (DAG) representation of the 0/1 Knapsack problem. |
| 35 | +
|
| 36 | + Parameters |
| 37 | + ---------- |
| 38 | + capacity : int |
| 39 | + Maximum weight capacity of the knapsack. |
| 40 | + weights : list[int] |
| 41 | + List of item weights. |
| 42 | + values : list[int] |
| 43 | + List of item values. |
| 44 | +
|
| 45 | + Returns |
| 46 | + ------- |
| 47 | + list[dict] |
| 48 | + List of edges, each represented as a dictionary with: |
| 49 | + - 'from': start node |
| 50 | + - 'to': end node |
| 51 | + - 'cost': edge cost (negative item value when item is included) |
| 52 | + - 'label': description of the decision |
| 53 | +
|
| 54 | + Test |
| 55 | + -------- |
| 56 | + >>> edges = generate_knapsack_graph(5, [2, 3], [10, 20]) |
| 57 | + >>> len(edges) > 0 |
| 58 | + True |
| 59 | + >>> any(edge['label'].startswith("take_item") for edge in edges) |
| 60 | + True |
| 61 | + >>> any(edge['label'].startswith("skip_item") for edge in edges) |
| 62 | + True |
| 63 | + >>> edges[-1]['to'] == 't' |
| 64 | + True |
| 65 | + >>> # Check a specific edge: first item, weight 2, value 10 |
| 66 | + >>> first_take_edge = next(edge for edge in edges if edge['label'] == "take_item_0") |
| 67 | + >>> first_take_edge['from'] == (0, 0) |
| 68 | + True |
| 69 | + >>> first_take_edge['to'] == (1, 2) |
| 70 | + True |
| 71 | + >>> first_take_edge['cost'] == -10 |
| 72 | + True |
| 73 | + """ |
| 74 | + |
| 75 | + n = len(weights) |
| 76 | + edges = [] |
| 77 | + |
| 78 | + # Generate a layer for each item, with (capacity + 1) nodes |
| 79 | + for i in range(n): |
| 80 | + for w in range(capacity + 1): |
| 81 | + weight = weights[i] |
| 82 | + value = values[i] |
| 83 | + |
| 84 | + # Edge for skipping the current item |
| 85 | + edges.append( |
| 86 | + { |
| 87 | + "from": (i, w), |
| 88 | + "to": (i + 1, w), |
| 89 | + "cost": 0, # no value added |
| 90 | + "label": f"skip_item_{i}", |
| 91 | + } |
| 92 | + ) |
| 93 | + |
| 94 | + # Edge for taking the item, only if within capacity |
| 95 | + if w + weight <= capacity: |
| 96 | + edges.append( |
| 97 | + { |
| 98 | + "from": (i, w), |
| 99 | + "to": (i + 1, w + weight), |
| 100 | + "cost": -value, # negative cost to solve SPP |
| 101 | + "label": f"take_item_{i}", |
| 102 | + } |
| 103 | + ) |
| 104 | + |
| 105 | + # Source node and initial edge |
| 106 | + edges.append({"from": "s", "to": (0, 0), "cost": 0, "label": "start"}) |
| 107 | + |
| 108 | + # Edges from all final states to the sink node |
| 109 | + for w in range(capacity + 1): |
| 110 | + edges.append({"from": (n, w), "to": "t", "cost": 0, "label": f"end_{w}"}) |
| 111 | + |
| 112 | + return edges |
0 commit comments