Fractional Cover Problem

p
# https://en.wikipedia.org/wiki/Set_cover_problem

from dataclasses import dataclass
from operator import attrgetter


@dataclass
class Item:
    weight: int
    value: int

    @property
    def ratio(self) -> float:
        """
        Return the value-to-weight ratio for the item.

        Returns:
            float: The value-to-weight ratio for the item.

        Examples:
        >>> Item(10, 65).ratio
        6.5

        >>> Item(20, 100).ratio
        5.0

        >>> Item(30, 120).ratio
        4.0
        """
        return self.value / self.weight


def fractional_cover(items: list[Item], capacity: int) -> float:
    """
    Solve the Fractional Cover Problem.

    Args:
        items: A list of items, where each item has weight and value attributes.
        capacity: The maximum weight capacity of the knapsack.

    Returns:
        The maximum value that can be obtained by selecting fractions of items to cover
        the knapsack's capacity.

    Raises:
        ValueError: If capacity is negative.

    Examples:
    >>> fractional_cover((Item(10, 60), Item(20, 100), Item(30, 120)), capacity=50)
    240.0

    >>> fractional_cover([Item(20, 100), Item(30, 120), Item(10, 60)], capacity=25)
    135.0

    >>> fractional_cover([Item(10, 60), Item(20, 100), Item(30, 120)], capacity=60)
    280.0

    >>> fractional_cover(items=[Item(5, 30), Item(10, 60), Item(15, 90)], capacity=30)
    180.0

    >>> fractional_cover(items=[], capacity=50)
    0.0

    >>> fractional_cover(items=[Item(10, 60)], capacity=5)
    30.0

    >>> fractional_cover(items=[Item(10, 60)], capacity=1)
    6.0

    >>> fractional_cover(items=[Item(10, 60)], capacity=0)
    0.0

    >>> fractional_cover(items=[Item(10, 60)], capacity=-1)
    Traceback (most recent call last):
        ...
    ValueError: Capacity cannot be negative
    """
    if capacity < 0:
        raise ValueError("Capacity cannot be negative")

    total_value = 0.0
    remaining_capacity = capacity

    # Sort the items by their value-to-weight ratio in descending order
    for item in sorted(items, key=attrgetter("ratio"), reverse=True):
        if remaining_capacity == 0:
            break

        weight_taken = min(item.weight, remaining_capacity)
        total_value += weight_taken * item.ratio
        remaining_capacity -= weight_taken

    return total_value


if __name__ == "__main__":
    import doctest

    if result := doctest.testmod().failed:
        print(f"{result} test(s) failed")
    else:
        print("All tests passed")