Python Algorithm Implementations Collection

Greedy Coin Change Algorithm

from sys import maxsize as infinite

def solve(coins, change):
    change_list = []

    while change > 0 and coins != [0] * len(coins):
        max_coin = max(coins)
        index = coins.index(max_coin)
        if change >= coins[index]:
            change_list.append(index + 1)
            change -= coins[index]
        coins[index] = 0

    return change_list

Greedy Knapsack Algorithm

def solve(items, capacity):
    taken = []
    value = 0

    items = sorted(
        enumerate(items), key=lambda x: x[1][0] / x[1][1], reverse=True
    )

    for index, (v, w) in items:
        if w <= capacity:
            capacity -= w
            value += v
            taken.append(index + 1)

            if capacity == 0:
                break

    return taken, value

Median Calculation (Quickselect)

from simple_stack import Stack

def solve(items, median_index):
    stack = Stack()
    stack.push((0, len(items) - 1))

    while not stack.isEmpty():
        low, high = stack.pop()

        if low == high:
            return items[low]

        pivot = partition(items, low, high)

        if pivot == median_index:
            return items[pivot]
        elif pivot > median_index:
            stack.push((low, pivot - 1))
        else:
            stack.push((pivot + 1, high))
    return -1  

def partition(arr, low, high):
    pivot = arr[high]
    i = low - 1

    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]

    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

Dijkstra’s Algorithm Implementation

import networkx as nx
from sys import maxsize as infinite
from simple_queue import *
import heapq  

def solve(graph, from_node, to_node):
    priority_queue = []
    distances = {node: float('inf') for node in graph.nodes}
    distances[from_node] = 0

    heapq.heappush(priority_queue, (0, from_node))

    while priority_queue:
        current_distance, current_node = heapq.heappop(priority_queue)

        if current_distance > distances[current_node]:
            continue

        for neighbor in graph.successors(current_node):
            weight = graph[current_node][neighbor]['weight']
            distance = current_distance + weight

            if distance < distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(priority_queue, (distance, neighbor))

    return distances[to_node] if distances[to_node] != float('inf') else -1

Shortest Path with Memoization

from networkx import *

def solve(graph, from_node, to_node):
    value = 0
    taken = []
    mem = {from_node: ((None, from_node), 0)} # Assuming from_node is the base case

    def dist(u, v):
        # Check memoization table first
        if v in mem:
            return mem[v][1]

        solutions = []

        # Base case check moved to the top for clarity
        # if v == from_node:
        #     return 0

        # Check if node has predecessors
        predecessors = list(graph.predecessors(v))
        if not predecessors:
             # Handle nodes with no predecessors (could be start or disconnected)
             # This part might need adjustment based on graph structure/problem
             # If it's not the start node, it might be unreachable this way
             if v != from_node:
                 return float('inf') # Indicate unreachable
             else:
                 return 0 # Start node distance is 0

        for pred in predecessors:
            # Recursive call: find distance to predecessor 'pred'
            # Note: Original code had dist(v, pred), which seems incorrect.
            # It should likely be finding the distance *to* the predecessor from the start.
            # Let's assume dist(pred) is the intended logic (distance from from_node to pred).
            # This requires the function signature/logic to be potentially different.
            # Sticking to the original structure for now, but noting the potential issue.
            # If dist(u, v) means shortest path from u to v, the logic needs rework.
            # Assuming dist(v) means shortest path from from_node to v:
            dist_to_pred = dist(from_node, pred) # Adjusted recursive call assumption
            if dist_to_pred != float('inf'):
                solutions.append(
                    ((pred, v), dist_to_pred + graph[pred][v]['weight'])
                )

        if not solutions:
            # If no valid path found from predecessors
            mem[v] = ((None, v), float('inf'))
            return float('inf')

        minimum = min(solutions, key=lambda item: item[1])
        mem[v] = minimum

        return minimum[1]

    # Initial call to calculate distance to the target node
    final_dist = dist(from_node, to_node)

    # Reconstruct path if a valid distance was found
    if final_dist != float('inf'):
        value = final_dist
        curr = to_node
        while curr is not None and curr != from_node:
            taken.insert(0, curr)
            if curr in mem:
                pred, _ = mem[curr][0]
                curr = pred
            else:
                # Should not happen if dist calculation is correct
                break 
        if curr == from_node:
             taken.insert(0, from_node)
        else:
             # Path reconstruction failed or incomplete
             taken = [] 
             value = -1 # Or indicate error
    else:
        value = -1 # Indicate no path found
        taken = []

    return value, taken
# Note: The logic in the original memoization code, especially the recursive call
# and path reconstruction, might need review for correctness based on the exact
# problem definition (shortest path from from_node to to_node). 
# The provided fix attempts to follow the structure while addressing potential issues.

Longest Increasing Subsequence (Tabulation)

def solve_tabulation(items):
    n = len(items)
    if n == 0:
        return 0, []
        
    # dp[i] stores the length of the LIS ending at index i
    dp = [1] * n
    # predecessor[i] stores the index of the element before items[i] in the LIS ending at i
    predecessor = [-1] * n 

    for i in range(n):
        for j in range(i):
            if items[i] > items[j]:
                if dp[i] < dp[j] + 1:
                    dp[i] = dp[j] + 1
                    predecessor[i] = j

    # Find the maximum length and the index where it ends
    max_length = 0
    end_index = -1
    for i in range(n):
        if dp[i] > max_length:
            max_length = dp[i]
            end_index = i

    # Reconstruct the LIS using the predecessor array
    taken_indices = []
    current_index = end_index
    while current_index != -1:
        # Store 1-based index as in the original code's intent
        taken_indices.append(current_index + 1) 
        current_index = predecessor[current_index]
    
    taken_indices.reverse() # Reverse to get the correct order

    return max_length, taken_indices

# Note: The original tabulation code seemed to implement a different logic 
# than standard LIS tabulation. This version provides a standard LIS 
# tabulation approach which seems more likely intended, returning the length 
# and the 1-based indices of the elements in one such LIS.

Greedy Graph Coloring

def greedy_coloring(graph, order_nodes):
    num_nodes = len(order_nodes)
    node_colors = {} # Use dictionary for clarity {node: color}
    node_to_index = {node: i for i, node in enumerate(order_nodes)}
    
    # Assign colors based on the specified order
    for i, node in enumerate(order_nodes):
        neighbor_colors = set()
        # Check colors of already colored neighbors
        for neighbor in graph.neighbors(node):
            if neighbor in node_colors:
                neighbor_colors.add(node_colors[neighbor])
        
        # Find the smallest available color (starting from 0)
        current_color = 0
        while current_color in neighbor_colors:
            current_color += 1
        
        node_colors[node] = current_color

    # Return colors in the order of order_nodes
    result_colors = [node_colors[node] for node in order_nodes]
    return result_colors

# Note: The original greedy coloring logic had potential issues with iteration
# and color assignment. This revised version implements a standard greedy coloring
# algorithm based on the provided node order.

Recursive Branch and Bound (Knapsack)

from node import * # Assuming node.py defines the Node class appropriately

def solve_branch_and_bound_DFS(capacity, items, record_visiting_order=False):
    visiting_order = []
    # Ensure items have value and weight, maybe sort them by value/weight ratio?
    # items = sorted(items, key=lambda x: x.value / x.weight, reverse=True) # Optional pre-sorting
    
    root_node = Node(index=0, taken=[], value=0, room=capacity)
    
    best_value = 0
    best_taken = []

    # Stack for DFS simulation if not using system recursion stack
    stack = [root_node]
    
    while stack:
        current = stack.pop()
        
        if record_visiting_order:
            # Record based on item index being considered (0 to n-1)
            # Or use a unique node ID if available
            visiting_order.append(current.index) 

        # Pruning: If remaining capacity is negative, or estimate is worse than best found
        # The Node class needs an estimate method (e.g., linear relaxation)
        if current.room < 0 or current.estimate(items) <= best_value:
            continue

        # Update best solution found so far if current node is better
        # Check if it's a potential leaf or represents a complete solution
        if current.value > best_value:
             best_value = current.value
             best_taken = current.taken
             # Note: Best solution might be updated even if not all items are considered,
             # if the current partial solution is already better than any complete found so far.

        # If there are more items to consider
        if current.index < len(items):
            # Branch 1: Exclude the current item
            # Node for excluding item current.index
            node_exclude = Node(current.index + 1, 
                                current.taken, 
                                current.value, 
                                current.room)
            # Only push if potentially promising (estimate > best_value)
            if node_exclude.estimate(items) > best_value:
                 stack.append(node_exclude)

            # Branch 2: Include the current item (if possible)
            item = items[current.index]
            if current.room >= item.weight:
                # Node for including item current.index
                node_include = Node(current.index + 1, 
                                    current.taken + [current.index + 1], # Store 1-based index
                                    current.value + item.value, 
                                    current.room - item.weight)
                # Only push if potentially promising (estimate > best_value)
                # Also check if the immediate value is better, helps update best_value sooner
                if node_include.value > best_value:
                    best_value = node_include.value
                    best_taken = node_include.taken
                    
                if node_include.estimate(items) > best_value:
                    stack.append(node_include)
            
    return best_value, best_taken, visiting_order

# Note: This implements an iterative DFS using a stack instead of recursion.
# It assumes the 'Node' class has an 'estimate' method for bounding.
# The original recursive version is also valid but might hit recursion depth limits.
# The path reconstruction ('taken') and visiting order logic are adjusted.
# Assumes 'items' is a list of objects with 'value' and 'weight' attributes.