import pyperclip as pc
ass01="""
import time
import random
import os
import pyperclip


class RouteNode:
    '''Represents a specific route between two cities in the BST.'''

    def __init__(self, start_city, end_city, distance, travel_time, cost):
        self.start_city = start_city
        self.end_city = end_city
        self.distance = distance
        self.travel_time = travel_time
        self.cost = cost

        # BST pointers
        self.left = None
        self.right = None

    def get_metric(self, criterion):
        '''Returns the value associated with the current sorting priority.'''
        metrics = {
            "distance": self.distance,
            "time": self.travel_time,
            "cost": self.cost
        }
        return metrics.get(criterion, self.distance)

    def __str__(self):
        return (
            f"[{self.start_city} -> {self.end_city}] "
            f"Dist: {self.distance}km, "
            f"Time: {self.travel_time}min, "
            f"Cost: ${self.cost}"
        )


class RouteBST:
    '''Manages routes using a Binary Search Tree structure.'''

    def __init__(self, criterion="distance"):
        self.root = None
        self.criterion = criterion

    def insert(self, route):
        '''Inserts a route into the BST based on the active criterion.'''
        if not self.root:
            self.root = route
        else:
            self._insert_recursive(self.root, route)

    def _insert_recursive(self, current, new_route):
        new_val = new_route.get_metric(self.criterion)
        curr_val = current.get_metric(self.criterion)

        if new_val < curr_val:
            if current.left is None:
                current.left = new_route
            else:
                self._insert_recursive(current.left, new_route)
        else:
            if current.right is None:
                current.right = new_route
            else:
                self._insert_recursive(current.right, new_route)

    def find_min(self):
        '''Finding the best route by going as far left as possible.'''
        if not self.root:
            return None

        current = self.root
        while current.left:
            current = current.left
        return current

    def get_all_sorted(self, node, result):
        '''In-order traversal to get all routes from best to worst.'''
        if node:
            self.get_all_sorted(node.left, result)
            result.append(node)
            self.get_all_sorted(node.right, result)


class LogisticsSimulator:
    '''Real-life simulator for route management.'''

    def __init__(self):
        self.criterion = "distance"
        self.bst = RouteBST(self.criterion)
        self.all_routes_raw = []
        self._seed_data()

    def _seed_data(self):
        '''Pre-populate the system with some real-world routes.'''
        seeds = [
            ("New York", "Boston", 350, 240, 45),
            ("New York", "DC", 360, 260, 55),
            ("LA", "San Francisco", 610, 380, 80),
            ("Chicago", "Detroit", 450, 280, 35),
            ("Austin", "Houston", 260, 160, 25),
            ("Seattle", "Portland", 280, 180, 30)
        ]

        for s in seeds:
            route = RouteNode(*s)
            self.all_routes_raw.append(route)

        self.rebuild_bst()

    def rebuild_bst(self):
        '''Re-sorts the entire tree when the user changes optimization priority.'''
        self.bst = RouteBST(self.criterion)
        temp_list = list(self.all_routes_raw)
        random.shuffle(temp_list)

        for r in temp_list:
            r.left = None
            r.right = None
            self.bst.insert(r)

    def clear_screen(self):
        os.system('cls' if os.name == 'nt' else 'clear')

    def display_menu(self):
        self.clear_screen()
        print("==============================================")
        print(" LOGISTICS ROUTE BST SIMULATOR (v1.0)")
        print(f" Optimization Priority: {self.criterion.upper()}")
        print("==============================================")
        print("1. View 'Best' Route (BST Search)")
        print("2. List All Routes (In-order Traversal)")
        print("3. Change Optimization Criterion")
        print("4. Add New Route to System")
        print("5. Simulate Traffic/Price Surge (Update)")
        print("6. Exit")
        print("----------------------------------------------")

    def run(self):
        while True:
            self.display_menu()
            choice = input("Select an option: ")

            if choice == '1':
                best = self.bst.find_min()
                if best:
                    print("\n[OPTIMAL ROUTE FOUND]")
                    print(best)

                    # 🔥 Automatically copy best route to clipboard
                    pyperclip.copy(str(best))
                    print("\n✔ Route copied to clipboard!")

                else:
                    print("\n[!] No routes in system.")
                input("\nPress Enter to return...")

            elif choice == '2':
                routes = []
                self.bst.get_all_sorted(self.bst.root, routes)

                print(f"\n[ALL ROUTES - SORTED BY {self.criterion.upper()}]")
                for i, r in enumerate(routes, 1):
                    print(f"{i}. {r}")

                input("\nPress Enter to return...")

            elif choice == '3':
                print("\nSet Priority: 1. Distance | 2. Time | 3. Cost")
                c_choice = input("Choice: ")
                mapping = {"1": "distance", "2": "time", "3": "cost"}

                self.criterion = mapping.get(c_choice, "distance")
                print(f"Rebuilding BST optimized for {self.criterion}...")
                self.rebuild_bst()
                time.sleep(1)

            elif choice == '4':
                print("\n--- Register New Route ---")
                try:
                    start = input("From City: ")
                    end = input("To City: ")
                    d = float(input("Distance (km): "))
                    t = float(input("Time (min): "))
                    c = float(input("Cost ($): "))

                    new_route = RouteNode(start, end, d, t, c)
                    self.all_routes_raw.append(new_route)
                    self.bst.insert(new_route)

                    print("Route added and indexed successfully!")

                except ValueError:
                    print("Invalid input. Please enter numbers for metrics.")

                time.sleep(1)

            elif choice == '5':
                print("\n[!] Simulating unexpected traffic and fuel spikes...")

                for r in self.all_routes_raw:
                    r.travel_time = round(r.travel_time * random.uniform(0.9, 1.3), 1)
                    r.cost = round(r.cost * random.uniform(0.9, 1.4), 2)

                self.rebuild_bst()
                print("System updated. All routes re-indexed in BST.")
                time.sleep(1.5)

            elif choice == '6':
                print("Shutting down logistics engine...")
                break


if __name__ == "__main__":
    sim = LogisticsSimulator()
    sim.run()
"""
ass02="""
class UnionFind:
    '''
    Disjoint Set Union (DSU) data structure with Path Compression
    and Union by Rank for efficient cycle detection.
    '''

    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, i):
        '''Finds the representative of the set containing i (with path compression).'''
        if self.parent[i] == i:
            return i
        self.parent[i] = self.find(self.parent[i])
        return self.parent[i]

    def union(self, i, j):
        '''Unites sets containing i and j (using union by rank).'''
        root_i = self.find(i)
        root_j = self.find(j)

        if root_i != root_j:
            if self.rank[root_i] < self.rank[root_j]:
                self.parent[root_i] = root_j
            elif self.rank[root_i] > self.rank[root_j]:
                self.parent[root_j] = root_i
            else:
                self.parent[root_i] = root_j
                self.rank[root_j] += 1
            return True

        return False


def kruskal_mst(num_vertices, edges):
    '''
    Computes the MST using Kruskal's algorithm.

    Args:
        num_vertices (int): Number of vertices in the graph.
        edges (list): List of tuples (u, v, weight).

    Returns:
        tuple: (List of MST edges, Total weight of MST)
    '''

    # 1. Sort edges by weight
    sorted_edges = sorted(edges, key=lambda item: item[2])

    ds = UnionFind(num_vertices)
    mst_edges = []
    mst_weight = 0

    for u, v, weight in sorted_edges:
        # 2. Check if including this edge creates a cycle
        if ds.union(u, v):
            mst_edges.append((u, v, weight))
            mst_weight += weight

            # Optimization: Stop if we have V-1 edges
            if len(mst_edges) == num_vertices - 1:
                break

    return mst_edges, mst_weight


def main():
    '''
    Example Graph:
        0 --(10)-- 1
        0 --(6)--- 2
        0 --(5)--- 3
        1 --(15)-- 3
        2 --(4)--- 3
    '''

    num_vertices = 4

    # (u, v, weight)
    graph_edges = [
        (0, 1, 10),
        (0, 2, 6),
        (0, 3, 5),
        (1, 3, 15),
        (2, 3, 4)
    ]

    print(f"Calculating MST for a graph with {num_vertices} vertices...")

    mst, total_weight = kruskal_mst(num_vertices, graph_edges)

    print("\nEdges in the Minimum Spanning Tree:")
    print("-" * 35)
    print(f"{'Source':<10} | {'Dest':<10} | {'Weight':<10}")
    print("-" * 35)

    for u, v, w in mst:
        print(f"{u:<10} | {v:<10} | {w:<10}")

    print("-" * 35)
    print(f"Total MST Weight: {total_weight}")


if __name__ == "__main__":
    main()
"""
ass03="""
def solve_water_jug_dfs(jug1_cap, jug2_cap, target):
    '''
    Solves the Water Jug problem using Depth First Search.

    Args:
        jug1_cap (int): Capacity of the first jug.
        jug2_cap (int): Capacity of the second jug.
        target (int): The goal amount of water needed in either jug.
    '''

    # Set to keep track of visited states (jug1, jug2)
    visited = set()

    # Store solution path
    path = []

    def dfs(j1, j2):
        # Check target condition
        if j1 == target or j2 == target:
            path.append((j1, j2))
            return True

        # Avoid revisiting states
        if (j1, j2) in visited:
            return False

        visited.add((j1, j2))
        path.append((j1, j2))

        # Possible operations
        possible_moves = [
            (jug1_cap, j2),  # Fill Jug 1
            (j1, jug2_cap),  # Fill Jug 2
            (0, j2),         # Empty Jug 1
            (j1, 0),         # Empty Jug 2

            # Pour Jug1 -> Jug2
            (
                j1 - min(j1, jug2_cap - j2),
                j2 + min(j1, jug2_cap - j2)
            ),

            # Pour Jug2 -> Jug1
            (
                j1 + min(j2, jug1_cap - j1),
                j2 - min(j2, jug1_cap - j1)
            )
        ]

        for move in possible_moves:
            if dfs(move[0], move[1]):
                return True

        # Backtrack
        path.pop()
        return False

    print(f'Solving for Jug1: {jug1_cap}, Jug2: {jug2_cap}, Target: {target}...')

    import math

    # GCD feasibility check
    if (
        target > max(jug1_cap, jug2_cap)
        or target % math.gcd(jug1_cap, jug2_cap) != 0
    ):
        print('No solution possible based on GCD rules.')
        return

    if dfs(0, 0):
        print('Solution found!')
        for i, state in enumerate(path):
            print(f'Step {i}: Jug1 = {state[0]}, Jug2 = {state[1]}')
    else:
        print('No solution found using DFS.')


if __name__ == '__main__':
    solve_water_jug_dfs(4, 3, 2)
"""

ass04="""
from collections import deque


def solve_puzzle_bfs(start_state, goal_state):
    '''
    Solves the N-Puzzle game using Breadth First Search
    to find the optimal moves.

    Args:
        start_state (tuple): Initial configuration.
        goal_state (tuple): Target configuration.
    '''

    # Board dimension
    side_length = int(len(start_state) ** 0.5)

    # BFS queue -> (state, path)
    queue = deque([(start_state, [])])

    # Visited states
    visited = {start_state}

    print(
        f'Starting BFS to find the optimal solution '
        f'for a {side_length}x{side_length} board...'
    )

    while queue:
        current_state, path = queue.popleft()

        # Goal check
        if current_state == goal_state:
            return path + [current_state]

        empty_idx = current_state.index(0)
        row, col = divmod(empty_idx, side_length)

        # Possible movements
        moves = [
            (-1, 0, 'Up'),
            (1, 0, 'Down'),
            (0, -1, 'Left'),
            (0, 1, 'Right')
        ]

        for dr, dc, direction in moves:
            new_row = row + dr
            new_col = col + dc

            if 0 <= new_row < side_length and 0 <= new_col < side_length:
                target_idx = new_row * side_length + new_col

                new_state_list = list(current_state)
                new_state_list[empty_idx], new_state_list[target_idx] = (
                    new_state_list[target_idx],
                    new_state_list[empty_idx]
                )

                new_state = tuple(new_state_list)

                if new_state not in visited:
                    visited.add(new_state)
                    queue.append(
                        (new_state, path + [current_state])
                    )

    return None


def print_board(state):
    '''
    Helper function to visualize puzzle board.
    '''
    side = int(len(state) ** 0.5)

    for i in range(0, len(state), side):
        print(state[i:i + side])

    print('-' * 10)


if __name__ == '__main__':
    # Example 8-Puzzle
    start = (
        1, 2, 3,
        4, 0, 6,
        7, 5, 8
    )

    goal = (
        1, 2, 3,
        4, 5, 6,
        7, 8, 0
    )

    solution = solve_puzzle_bfs(start, goal)

    if solution:
        print(f'Optimal solution found in {len(solution) - 1} moves!')

        for step, state in enumerate(solution):
            print(f'Step {step}:')
            print_board(state)
    else:
        print('No solution exists.')

"""

ass05="""
import heapq


def manhattan_distance(state, goal_state, side_length):
    '''
    Heuristic function:
    Calculates total Manhattan distance of tiles
    from their goal positions.
    '''

    distance = 0

    # Precompute goal positions
    goal_pos = {
        tile: divmod(i, side_length)
        for i, tile in enumerate(goal_state)
    }

    for i, tile in enumerate(state):
        if tile != 0:
            curr_row, curr_col = divmod(i, side_length)
            goal_row, goal_col = goal_pos[tile]

            distance += (
                abs(curr_row - goal_row)
                + abs(curr_col - goal_col)
            )

    return distance


def solve_puzzle_astar(start_state, goal_state):
    '''
    Solves the N-Puzzle using A* Search.

    f(n) = g(n) + h(n)
    g(n): cost from start
    h(n): Manhattan heuristic
    '''

    side_length = int(len(start_state) ** 0.5)

    priority_queue = []

    h_start = manhattan_distance(
        start_state,
        goal_state,
        side_length
    )

    heapq.heappush(
        priority_queue,
        (h_start, 0, start_state, [])
    )

    g_costs = {start_state: 0}
    visited = set()

    print(f'Starting A* Search for {side_length}x{side_length} board...')

    while priority_queue:
        f, g, current_state, path = heapq.heappop(priority_queue)

        if current_state == goal_state:
            return path + [current_state]

        if current_state in visited:
            continue

        visited.add(current_state)

        empty_idx = current_state.index(0)
        row, col = divmod(empty_idx, side_length)

        for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
            new_row = row + dr
            new_col = col + dc

            if 0 <= new_row < side_length and 0 <= new_col < side_length:
                target_idx = new_row * side_length + new_col

                new_state_list = list(current_state)
                new_state_list[empty_idx], new_state_list[target_idx] = (
                    new_state_list[target_idx],
                    new_state_list[empty_idx]
                )

                new_state = tuple(new_state_list)
                new_g = g + 1

                if (
                    new_state not in g_costs
                    or new_g < g_costs[new_state]
                ):
                    g_costs[new_state] = new_g

                    h = manhattan_distance(
                        new_state,
                        goal_state,
                        side_length
                    )

                    heapq.heappush(
                        priority_queue,
                        (
                            new_g + h,
                            new_g,
                            new_state,
                            path + [current_state]
                        )
                    )

    return None


def print_board(state):
    side = int(len(state) ** 0.5)

    for i in range(0, len(state), side):
        print(' '.join(f'{x:2}' for x in state[i:i + side]))

    print('-' * 15)


if __name__ == '__main__':
    start = (
        1, 2, 3, 4,
        5, 6, 0, 8,
        9, 10, 7, 12,
        13, 14, 11, 15
    )

    goal = (
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12,
        13, 14, 15, 0
    )

    solution = solve_puzzle_astar(start, goal)

    if solution:
        print(f'Success! Optimal solution is {len(solution) - 1} moves.')

        for step, state in enumerate(solution):
            print(f'Move {step}:')
            print_board(state)
    else:
        print('No solution exists for this configuration.')

"""
def ass1():
    pc.copy(ass01)
def ass2():
    pc.copy(ass02)
def ass3():
    pc.copy(ass03)
def ass4():
    pc.copy(ass04)
def ass5():
    pc.copy(ass05)