Skip to content

Commit 6cda13d

Browse files
committed
Add algorithm to generate a graph from knapsack problem
1 parent f662b63 commit 6cda13d

File tree

1 file changed

+112
-0
lines changed

1 file changed

+112
-0
lines changed

knapsack/knapsack_graph_generation.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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

Comments
 (0)