diff --git a/code/python/src/algorithms/rucksack/algorithms.py b/code/python/src/algorithms/rucksack/algorithms.py index 9a58244..4408064 100644 --- a/code/python/src/algorithms/rucksack/algorithms.py +++ b/code/python/src/algorithms/rucksack/algorithms.py @@ -57,15 +57,15 @@ def rucksack_greedy_algorithm( # führe greedy aus: n = len(costs); cost_total = 0; - vector = [ 0 for _ in range(n) ]; + vector = [ 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]; - vector[i] = 1; + vector[i] = Fraction(1); # falls Bruchteile erlaubt sind, füge einen Bruchteil des i. Items hinzu und abbrechen elif fractional: - vector[i] = (max_cost - cost_total)/costs[i]; + vector[i] = Fraction(Fraction(max_cost - cost_total)/Fraction(costs[i]), _normalize=False); break; # ansonsten weiter machen: else: @@ -74,7 +74,8 @@ def rucksack_greedy_algorithm( # Aspekte der Lösung speichern: rucksack = [i for i, v in enumerate(vector) if v > 0]; # Indexes von Items im Rucksack soln = Solution( - vector = vector, + order = order, + choice = vector, items = items[rucksack].tolist(), costs = costs[rucksack].tolist(), values = values[rucksack].tolist(), @@ -85,7 +86,7 @@ def rucksack_greedy_algorithm( repr_rucksack = display_rucksack(items=items[rucksack], costs=costs[rucksack], values=values[rucksack]); print('\x1b[1mEingeschätzte Lösung\x1b[0m'); print(''); - print(f'Mask: {soln.vector}'); + print(f'Mask: [{", ".join(map(str, soln.choice))}]'); print('Rucksack:') print(repr_rucksack); print(''); @@ -126,21 +127,19 @@ def rucksack_branch_and_bound_algorithm( S = Stack(); S.push(vector); while not S.empty(): - lb, u, can_add_all, can_add_none = estimate_lower_bound(mask=S.top(), max_cost=max_cost, costs=costs, values=values, items=items); + lb, choice, order_, pad = estimate_lower_bound(mask=S.top(), max_cost=max_cost, costs=costs, values=values, items=items); if verbose: - logged_steps.append((lb_estimate, lb, str(S), u, can_add_all, can_add_none)); + logged_steps.append((lb_estimate, lb, str(S), choice, order_, pad)); # Update nur nötig, wenn die (eingeschätzte) untere Schranke von A das bisherige Minimum verbessert: A: Mask = S.pop(); if lb < lb_estimate: # Bound, wenn sich A nicht weiter aufteilen lässt od. man A wie eine einelementige Option behandeln kann: - if not A.splittable() or can_add_all or can_add_none: + if not A.splittable() or pad != MaskValue.UNSET: lb_estimate = lb; - if can_add_all: - vector = A.pad_ones(); - elif can_add_none: - vector = A.pad_zeros(); - else: - vector = A; + # falls A als einelementige Menge betrachtet werden kann, ersetze unbekannte Werte: + if pad != MaskValue.UNSET: + A = A.pad(pad); + vector = A; # Branch sonst else: B, C = A.split(); @@ -152,7 +151,8 @@ def rucksack_branch_and_bound_algorithm( # Aspekte der Lösung speichern rucksack = vector.indexes_one; # Indexes von Items im Rucksack soln = Solution( - vector = vector.decision, + order = order, + choice = vector.choice, items = items[rucksack].tolist(), values = values[rucksack].tolist(), costs = costs[rucksack].tolist(), @@ -160,13 +160,13 @@ def rucksack_branch_and_bound_algorithm( # verbose output hier behandeln (irrelevant für Algorithmus): if verbose: - repr = display_branch_and_bound(values=values, steps=logged_steps, order=order); + repr = display_branch_and_bound(values=values, steps=logged_steps); repr_rucksack = display_rucksack(items=items[rucksack], costs=costs[rucksack], values=values[rucksack]); print('\x1b[1mLösung\x1b[0m'); print(''); print(repr); print(''); - print(f'Mask: {soln.vector}'); + print(f'Mask: [{", ".join(map(str, soln.choice))}]'); print('Rucksack:'); print(repr_rucksack); print(''); @@ -184,7 +184,8 @@ def get_sort_order(costs: np.ndarray, values: np.ndarray) -> List[int]: ''' n = len(costs); indexes = list(range(n)); - order = sorted(indexes, key=lambda i: -values[i]/costs[i]); + margin = [ value/cost for cost, value in zip(costs, values) ]; + order = sorted(indexes, key=lambda i: -margin[i]); return order; def estimate_lower_bound( @@ -193,7 +194,7 @@ def estimate_lower_bound( costs: np.ndarray, values: np.ndarray, items: np.ndarray, -) -> Tuple[float, List[float], bool]: +) -> Tuple[float, List[Fraction], List[int], MaskValue]: ''' Wenn partielle Information über den Rucksack festgelegt ist, kann man bei dem unbekannten Teil das Rucksack-Problem @@ -205,25 +206,25 @@ def estimate_lower_bound( indexes_one = mask.indexes_one; indexes_unset = mask.indexes_unset; n = len(mask); - vector = np.zeros(shape=(n,), dtype=float); + 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]); - vector[indexes_one] = 1; + choice[indexes_one] = Fraction(1); # Für Rest des Rucksacks (Items mit unbekanntem Status): cost_rest = max_cost - cost_rucksack; - can_add_all = False; - can_add_none = False; + pad = MaskValue.UNSET; # Prüfe, ob man als Lösung alles/nichts hinzufügen kann: if len(indexes_unset) > 0 and sum(costs[indexes_unset]) <= cost_rest: - can_add_all = True; - vector[indexes_unset] = 1; + pad = MaskValue.ONE; + choice[indexes_unset] = Fraction(1); value_rest = sum(values[indexes_unset]); elif len(indexes_unset) > 0 and min(costs[indexes_unset]) > cost_rest: - can_add_none = True; - vector[indexes_unset] = 0; + 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. @@ -236,11 +237,13 @@ def estimate_lower_bound( fractional = True, verbose = False, ); + choice[indexes_unset] = soln_rest.choice; value_rest = soln_rest.total_value; - vector[indexes_unset] = soln_rest.vector; + # 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, vector.tolist(), can_add_all, can_add_none; + return -value_max_est, choice.tolist(), order.tolist(), pad; diff --git a/code/python/src/algorithms/rucksack/display.py b/code/python/src/algorithms/rucksack/display.py index d433e55..62a942f 100644 --- a/code/python/src/algorithms/rucksack/display.py +++ b/code/python/src/algorithms/rucksack/display.py @@ -10,6 +10,7 @@ from src.thirdparty.maths import *; from src.thirdparty.types import *; from src.models.stacks import *; +from src.models.rucksack import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTS @@ -38,7 +39,7 @@ def display_order( 'order': order, 'values': values, 'costs': costs, - 'u': (values/costs), + 'margin': [str(Fraction(Fraction(value), Fraction(cost))) for cost, value in zip(costs, values)], }) \ .reset_index(drop=True); if one_based: @@ -62,10 +63,11 @@ def display_rucksack( costs: np.ndarray, values: np.ndarray, ) -> str: + render = lambda r: f'{r:g}'; table = pd.DataFrame({ 'items': items.tolist() + ['----', '∑'], - 'costs': costs.tolist() + ['', f'\x1b[92;1m{sum(costs)}\x1b[0m'], - 'values': values.tolist() + ['', f'\x1b[92;1m{sum(values)}\x1b[0m'], + 'costs': list(map(render, costs)) + ['', f'\x1b[92;1m{sum(costs):g}\x1b[0m'], + 'values': list(map(render, values)) + ['', f'\x1b[92;1m{sum(values):g}\x1b[0m'], }); repr = tabulate( table, @@ -82,20 +84,18 @@ def display_rucksack( def display_branch_and_bound( values: np.ndarray, - steps: List[Tuple[float, float, Stack, List[float], bool, bool]], - order: Optional[List[int]] = None, + steps: List[Tuple[float, float, Stack, List[Fraction], List[int], MaskValue]], ) -> str: # füge Summen-Ausdrücke für Greedy-Alg hinzu: rows = []; - used_vectors = []; - for lb_estimate, lb, S, u, can_add_all, can_add_none in steps: - pad = '1' if can_add_all else ('0' if can_add_none else ''); - if u in used_vectors: + used_choices = []; + for lb_estimate, lb, S, choice, order, pad in steps: + if choice in used_choices: expr = f'{lb:g}'; else: - used_vectors.append(u) - expr = display_sum(vector=u, values=values, as_maximum=False, order=order); - rows.append((f'{lb_estimate:g}', expr, pad, S)); + used_choices.append(choice); + expr = display_sum(choice=choice, values=values, as_maximum=False, order=order); + rows.append((f'{lb_estimate:g}', expr, ('' if pad == MaskValue.UNSET else pad.value), S)); table = pd.DataFrame(rows) \ .rename(columns={0: 'b', 1: 'g(TOP(S))', 2: 'pad?', 3: 'S'}) \ @@ -115,17 +115,17 @@ def display_branch_and_bound( # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def display_sum( - vector: List[float], + choice: List[Fraction], values: np.ndarray, order: Optional[List[int]] = None, as_maximum: bool = True, ) -> str: - parts = [ (u, x) for u, x in zip(vector, values)]; + parts = [ (u, x) for u, x in zip(choice, values)]; if not (order is None): parts = [ parts[j] for j in order ]; value = sum([ u*x for u, x in parts]); expr = '+'.join([ - f'{x:g}' if u == 1 else f'{Fraction(str(u))}·{x:g}' + f'{x:g}' if u == 1 else f'{u}·{x:g}' for u, x in parts if u > 0 ]); if as_maximum: diff --git a/code/python/src/models/rucksack/mask.py b/code/python/src/models/rucksack/mask.py index 37c19c8..b294660 100644 --- a/code/python/src/models/rucksack/mask.py +++ b/code/python/src/models/rucksack/mask.py @@ -7,6 +7,7 @@ from __future__ import annotations; +from src.thirdparty.maths import *; from src.thirdparty.types import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -47,8 +48,9 @@ class Mask(): return ''.join([ str(m.value) for m in self.values ]); @property - def decision(self) -> List[int]: - return [ x.value for x in self.values ]; + def choice(self) -> List[Fraction]: + assert all(x != MaskValue.UNSET for x in self.values); + return [ Fraction(x.value) for x in self.values ]; @property def indexes_set(self) -> List[int]: @@ -76,17 +78,11 @@ class Mask(): vector2[self.index] = MaskValue.ONE; return Mask(vector1), Mask(vector2); - def pad_zeros(self) -> Mask: + def pad(self, x: MaskValue) -> Mask: ''' - Completes mask by filling in unset values with zeros + Pads unset values with a give by given value. ''' - return Mask([ MaskValue.ZERO if u == MaskValue.UNSET else u for u in self.values ]); - - def pad_ones(self) -> Mask: - ''' - Completes mask by filling in unset values with zeros - ''' - return Mask([ MaskValue.ONE if u == MaskValue.UNSET else u for u in self.values ]); + return Mask([ x if u == MaskValue.UNSET else u for u in self.values ]); @property def support(self) -> List[int]: diff --git a/code/python/src/models/rucksack/solution.py b/code/python/src/models/rucksack/solution.py index 3d39462..a520d6a 100644 --- a/code/python/src/models/rucksack/solution.py +++ b/code/python/src/models/rucksack/solution.py @@ -5,9 +5,9 @@ # IMPORTS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -from __future__ import annotations -from dataclasses import asdict; +from __future__ import annotations; +from src.thirdparty.maths import *; from src.thirdparty.types import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -24,7 +24,8 @@ __all__ = [ @dataclass class SolutionRaw(): - vector: List[float] = field(); + order: List[int] = field(); + choice: List[Fraction] = field(); items: List[str] = field(); values: List[float] = field(repr=False); costs: List[float] = field(repr=False); @@ -32,16 +33,12 @@ class SolutionRaw(): class Solution(SolutionRaw): @property def support(self) -> List[float]: - return [ i for i, v in enumerate(self.vector) if v > 0 ]; - - @property - def vector_support(self) -> List[float]: - return [ v for v in self.vector if v > 0 ]; + return [ i for i, v in enumerate(self.choice) if v > 0 ]; @property def total_weight(self) -> float: - return sum([ self.vector[i]*x for (i, x) in zip(self.support, self.costs) ]); + return sum([ self.choice[i]*x for (i, x) in zip(self.support, self.costs) ]); @property def total_value(self) -> float: - return sum([ self.vector[i]*x for (i, x) in zip(self.support, self.values) ]); + return sum([ self.choice[i]*x for (i, x) in zip(self.support, self.values) ]);