ads2_2022/code/python/src/algorithms/rucksack/algorithms.py

263 lines
9.4 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.types import *;
from src.thirdparty.maths import *;
from models.generated.config import *;
from src.core.utils import *;
from src.models.rucksack import *;
from src.models.stacks import *;
from src.algorithms.rucksack.display import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'rucksack_greedy_algorithm',
'rucksack_branch_and_bound_algorithm',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# METHOD greedy algorithm
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def rucksack_greedy_algorithm(
max_cost: float,
costs: np.ndarray,
values: np.ndarray,
items: np.ndarray,
fractional: bool,
verbose: bool,
) -> Solution:
'''
Durch den Greedy-Algorithm wird der optimale Wert eines Rucksacks
unter Rücksicht der Kapizitätsschranke eingeschätzt.
NOTE: Wenn man `fractional = True` verwendet, liefert der Algorithmus
eine obere Schranke des maximalen Wertes beim Originalproblem.
'''
# sortiere daten:
order = get_sort_order(costs=costs, values=values);
# verbose output hier behandeln (irrelevant für Algorithmus):
if verbose:
repr = display_order(order=order, costs=costs, values=values, items=items, one_based=True);
print('');
print('\x1b[1mRucksack Problem - Greedy\x1b[0m');
print('');
print(repr);
print('');
# führe greedy aus:
n = len(costs);
cost_total = 0;
choice = [ Fraction(0) for _ in range(n) ];
for i in order:
# füge Item i hinzu, solange das Gesamtgewicht noch <= Schranke
if cost_total + costs[i] <= max_cost:
cost_total += costs[i];
choice[i] = Fraction(1);
# falls Bruchteile erlaubt sind, füge einen Bruchteil des i. Items hinzu und abbrechen
elif fractional:
choice[i] = Fraction(Fraction(max_cost - cost_total)/Fraction(costs[i]), _normalize=False);
break;
# ansonsten weiter machen:
else:
continue;
# Aspekte der Lösung speichern:
rucksack = [i for i, v in enumerate(choice) if v > 0]; # Indexes von Items im Rucksack
soln = Solution(
order = order,
choice = choice,
items = items[rucksack].tolist(),
costs = costs[rucksack].tolist(),
values = values[rucksack].tolist(),
);
# verbose output hier behandeln (irrelevant für Algorithmus):
if verbose:
repr_rucksack = display_rucksack(items=items, costs=costs, values=values, choice=choice);
print('\x1b[1mEingeschätzte Lösung\x1b[0m');
print('');
print(f'Mask: [{", ".join(map(str, soln.choice))}]');
print('Rucksack:')
print(repr_rucksack);
print('');
# Lösung ausgeben
return soln;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# METHOD branch and bound algorithm
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def rucksack_branch_and_bound_algorithm(
max_cost: float,
costs: np.ndarray,
values: np.ndarray,
items: np.ndarray,
verbose: bool,
) -> Solution:
'''
Durch Branch & Bound wird der optimale Wert eines Rucksacks
unter Rücksicht der Kapizitätsschranke exakt und effizienter bestimmt.
'''
order = get_sort_order(costs=costs, values=values);
# verbose output hier behandeln (irrelevant für Algorithmus):
if verbose:
repr = display_order(order=order, costs=costs, values=values, items=items, one_based=True);
print('');
print('\x1b[1mRucksack Problem - Branch & Bound\x1b[0m');
print('');
print(repr);
print('');
logged_steps = [];
step: Step;
mask = empty_mask(n=len(costs));
bound = np.inf;
S = Stack();
S.push(mask);
while not S.empty():
# top-Element auslesen und Bound berechnen:
A: Mask = S.top();
bound_subtree, choice, order_, state = estimate_lower_bound(mask=A, max_cost=max_cost, costs=costs, values=values, items=items);
# für logging (irrelevant für Algorithmus):
if verbose:
step = Step(bound=bound, bound_subtree=bound_subtree, stack_str=str(S), choice=choice, order=order_, indexes=A.indexes_unset, solution=state);
if bound_subtree < bound:
if state is not None:
step.move = EnumBranchAndBoundMove.BOUND;
step.bound = bound_subtree;
else:
step.move = EnumBranchAndBoundMove.BRANCH;
logged_steps.append(step);
S.pop();
# Update nur nötig, wenn die (eingeschätzte) untere Schranke von A das bisherige Minimum verbessert:
if bound_subtree < bound:
# Bound aktualisieren, wenn sich A nicht weiter aufteilen od. wenn sich A wie eine einelementige Option behandeln läst:
if state is not None:
bound = bound_subtree;
mask = state;
# Branch sonst
else:
B, C = A.split();
S.push(B);
# Nur dann C auf Stack legen, wenn mind. eine Möglichkeit in C die Kapazitätsschranke erfüllt:
if sum(costs[C.indexes_one]) <= max_cost:
S.push(C);
# Aspekte der Lösung speichern
rucksack = mask.indexes_one; # Indexes von Items im Rucksack
soln = Solution(
order = order,
choice = mask.choice,
items = items[rucksack].tolist(),
values = values[rucksack].tolist(),
costs = costs[rucksack].tolist(),
);
# verbose output hier behandeln (irrelevant für Algorithmus):
if verbose:
repr = display_branch_and_bound(values=values, steps=logged_steps);
repr_rucksack = display_rucksack(items=items, costs=costs, values=values, choice=mask.choice);
print(repr);
print('');
print('\x1b[1mLösung\x1b[0m');
print('');
print(f'Mask: [{", ".join(map(str, soln.choice))}]');
print('Rucksack:');
print(repr_rucksack);
print('');
# Lösung ausgeben
return soln;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# AUXILIARY METHOD resort
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_sort_order(costs: np.ndarray, values: np.ndarray) -> List[int]:
'''
Sortiert Daten absteigend nach values/costs.
'''
n = len(costs);
indexes = list(range(n));
margin = [ value/cost for cost, value in zip(costs, values) ];
order = sorted(indexes, key=lambda i: -margin[i]);
return order;
def estimate_lower_bound(
mask: Mask,
max_cost: float,
costs: np.ndarray,
values: np.ndarray,
items: np.ndarray,
) -> Tuple[float, List[Fraction], List[int], Optional[Mask]]:
'''
Wenn partielle Information über den Rucksack festgelegt ist,
kann man bei dem unbekannten Teil das Rucksack-Problem
mit Greedy-Algorithmus »lösen«,
um schnell eine gute Einschätzung zu bestimmen.
NOTE: Diese Funktion wird `g(mask)` im Skript bezeichnet.
'''
indexes_one = mask.indexes_one;
indexes_unset = mask.indexes_unset;
n = len(mask);
choice = np.zeros(shape=(n,), dtype=Fraction);
order = np.asarray(range(n));
# Berechnungen bei Items mit bekanntem Status in Rucksack:
value_rucksack = sum(values[indexes_one]);
cost_rucksack = sum(costs[indexes_one]);
choice[indexes_one] = Fraction(1);
# Für Rest des Rucksacks (Items mit unbekanntem Status):
cost_rest = max_cost - cost_rucksack;
state = None;
# Prüfe, ob man als Lösung alles/nichts hinzufügen kann:
if len(indexes_unset) == 0:
state = mask;
value_rest = 0;
elif sum(costs[indexes_unset]) <= cost_rest:
state = mask.pad(MaskValue.ONE);
choice[indexes_unset] = Fraction(1);
value_rest = sum(values[indexes_unset]);
elif min(costs[indexes_unset]) > cost_rest:
state = mask.pad(MaskValue.ZERO);
choice[indexes_unset] = Fraction(0);
value_rest = 0;
# Sonst mit Greedy-Algorithmus lösen:
# NOTE: Lösung ist eine Überschätzung des max-Wertes.
else:
soln_rest = rucksack_greedy_algorithm(
max_cost = cost_rest, # <- Kapazität = Restgewicht
costs = costs[indexes_unset],
values = values[indexes_unset],
items = items[indexes_unset],
fractional = True,
verbose = False,
);
choice[indexes_unset] = soln_rest.choice;
value_rest = soln_rest.total_value;
# Berechne Permutation für Teilrucksack
permute_part(order, indexes=indexes_unset, order=soln_rest.order, in_place=True);
# Einschätzung des max-Wertes:
value_max_est = value_rucksack + value_rest;
# Ausgabe mit -1 multiplizieren (weil maximiert wird):
return -value_max_est, choice.tolist(), order.tolist(), state;