refactor: improve knapsack selection with dynamic scaling, pre-filtering, and greedy budget filling

oh_security
Cizz22 3 months ago
parent d2ecf0849e
commit 19066475c9

@ -1,6 +1,7 @@
from collections import defaultdict from collections import defaultdict
import math
import random import random
from typing import Optional from typing import Optional, List, Dict, Tuple
from uuid import UUID from uuid import UUID
from sqlalchemy import Delete, Select from sqlalchemy import Delete, Select
@ -112,61 +113,88 @@ def greedy_selection(equipments: List[dict], budget: float) -> Tuple[List[dict],
"""Greedy fallback: select items by score until budget is used.""" """Greedy fallback: select items by score until budget is used."""
# Sort by priority_score descending # Sort by priority_score descending
equipments_sorted = sorted(equipments, key=lambda x: x["priority_score"], reverse=True) equipments_sorted = sorted(equipments, key=lambda x: x["priority_score"], reverse=True)
total_cost = 0 current_cost = 0.0
selected, excluded = [], [] selected, excluded = [], []
for eq in equipments_sorted: for eq in equipments_sorted:
if total_cost + eq["cost"] <= budget: cost = eq.get("total_cost", 0.0)
if current_cost + cost <= budget:
selected.append(eq) selected.append(eq)
total_cost += eq["cost"] current_cost += cost
else: else:
excluded.append(eq) excluded.append(eq)
return selected, excluded return selected, excluded
def knapsack_selection(equipments: List[dict], budget: float, scale: int = 10_000_000) -> Tuple[List[dict], List[dict]]: def knapsack_selection(equipments: List[dict], budget: float, scale: Optional[float] = None) -> Tuple[List[dict], List[dict]]:
""" """
Select equipment optimally within budget using 0/1 knapsack DP. Select equipment optimally within budget using 0/1 knapsack DP.
Uses scaling + 1D DP optimization to avoid MemoryError. Uses dynamic scaling + 1D DP optimization to ensure accuracy and avoid MemoryError.
Falls back to greedy if W is too large.
""" """
n = len(equipments) if not equipments:
return [], []
# Filter out items that are strictly above budget right away
# This helps significantly when budget is very low (e.g. 1)
eligible_items = []
strictly_excluded = []
for eq in equipments:
if eq["total_cost"] > budget:
strictly_excluded.append(eq)
else:
eligible_items.append(eq)
if not eligible_items:
return [], strictly_excluded
# Scale costs + budget n = len(eligible_items)
costs = [int(eq["total_cost"] // scale) for eq in equipments]
values = [eq["priority_score"] for eq in equipments] # Dynamic scaling: target W around 2000 for good precision/performance balance
if scale is None:
target_W = 2000
scale = max(1.0, budget / target_W)
costs = [int(math.ceil(eq["total_cost"] / scale)) for eq in eligible_items]
values = [eq["priority_score"] for eq in eligible_items]
W = int(budget // scale) W = int(budget // scale)
# Fallback if W is still too large # Fallback if W is still too large (memory safety)
if W > 1_000_000: if W > 1_000_000:
print("too big")
return greedy_selection(equipments, budget) return greedy_selection(equipments, budget)
# 1D DP array # 1D DP array
dp = [0.0] * (W + 1) dp = [0.0] * (W + 1)
keep = [[False] * (W + 1) for _ in range(n)] # track selection choices keep = [[False] * (W + 1) for _ in range(n)]
for i in range(n): for i in range(n):
cost, value = costs[i], values[i] cost, value = costs[i], values[i]
for w in range(W, cost - 1, -1): for w in range(W, cost - 1, -1):
if dp[w - cost] + value >= dp[w]: # <= FIXED HERE if dp[w - cost] + value >= dp[w]:
dp[w] = dp[w - cost] + value dp[w] = dp[w - cost] + value
keep[i][w] = True keep[i][w] = True
# Backtrack to find selected items # Backtrack
selected, excluded = [], [] selected = []
backtrack_excluded = []
w = W w = W
for i in range(n - 1, -1, -1): for i in range(n - 1, -1, -1):
if keep[i][w]: if keep[i][w]:
selected.append(equipments[i]) selected.append(eligible_items[i])
w -= costs[i] w -= costs[i]
else: else:
excluded.append(equipments[i]) backtrack_excluded.append(eligible_items[i])
excluded = backtrack_excluded + strictly_excluded
# Optional: greedy fill leftover budget with actual values
# This compensates for any conservative rounding from math.ceil
current_total_cost = sum(eq["total_cost"] for eq in selected)
remaining_budget = budget - current_total_cost
# Optional: fill leftover budget with zero-priority items
remaining_budget = budget - sum(eq["total_cost"] for eq in selected)
if remaining_budget > 0: if remaining_budget > 0:
# Sort excluded items by priority score for better greedy fill
excluded.sort(key=lambda x: x["priority_score"], reverse=True)
for eq in excluded[:]: for eq in excluded[:]:
if eq["total_cost"] <= remaining_budget: if eq["total_cost"] <= remaining_budget:
selected.append(eq) selected.append(eq)

Loading…
Cancel
Save