import heapq
from collections import defaultdict, deque

class FastColor:
    def __init__(self, graph):
        """
        Initialize with adjacency list graph representation
        :param graph: dict {node: set(neighbors)}
        """
        self.original_graph = defaultdict(set, {k: set(v) for k, v in graph.items()})
        self.Gk = None  # Current kernel graph
        self.Gm = []    # Removed independent sets
        self.lb = 0     # Current lower bound
        self.ub = float('inf')  # Current upper bound
        self.best_coloring = {}

    def max_clique_heuristic(self):
        """Algorithm 2: Maximum clique lower bound estimation"""
        candidates = sorted(self.Gk.keys(), 
                          key=lambda x: len(self.Gk[x]), 
                          reverse=True)
        max_clique = []
        for u in candidates:
            if all(v in max_clique for v in self.Gk[u]):
                max_clique.append(u)
        return len(max_clique)

    def bounded_independent_set(self, d):
        """Algorithm 3: Bounded independent set selection"""
        visited = set()
        independent_set = []
        # Sort by ascending degree for LDO strategy
        vertices = sorted(self.Gk.keys(),
                        key=lambda x: len(self.Gk[x]))
        
        for u in vertices:
            if len(self.Gk[u]) < d and u not in visited:
                independent_set.append(u)
                visited.add(u)
                visited.update(self.Gk[u])
        return independent_set

    def color_kernel(self):
        """Algorithm 1: Core-ordered DSATUR implementation"""
        # Phase 1: Core decomposition
        core = self.core_decomposition()
        
        # Phase 2: Initialize DSATUR structures
        saturation = defaultdict(int)
        color = {}
        uncolored = set(self.Gk.keys())
        
        while uncolored:
            # Select vertex with maximum (saturation, core, degree)
            u = max(uncolored, 
                  key=lambda x: (saturation[x], 
                               core[x], 
                               len(self.Gk[x])))
            uncolored.remove(u)
            
            # Find smallest available color
            used = {color[v] for v in self.Gk[u] if v in color}
            c = 1
            while c in used:
                c += 1
            color[u] = c
            
            # Update saturation for neighbors
            for v in self.Gk[u]:
                saturation[v] += 1
                
        return color

    def core_decomposition(self):
        """Linear-time k-core decomposition with bin sorting"""
        degrees = {u: len(self.Gk[u]) for u in self.Gk}
        max_deg = max(degrees.values()) if degrees else 0
        bins = defaultdict(deque)
        for u, d in degrees.items():
            bins[d].append(u)
            
        core = degrees.copy()
        for d in range(max_deg + 1):
            while bins[d]:
                u = bins[d].popleft()
                for v in self.Gk[u]:
                    if core[v] > d:
                        bins[core[v]].remove(v)
                        core[v] = max(core[v] - 1, d)
                        bins[core[v]].append(v)
        return core

    def extend_coloring(self, kernel_coloring):
        """Coloring extension with conflict avoidance"""
        coloring = kernel_coloring.copy()
        # Reverse removal order for extension
        for margin in reversed(self.Gm):  
            for u in margin:
                used = {coloring[v] for v in self.original_graph[u] 
                      if v in coloring}
                c = 1
                while c in used:
                    c += 1
                coloring[u] = c
        return coloring

    def execute(self, max_iter=100):
        """Main algorithm loop"""
        self.Gk = defaultdict(set, 
                            {k: set(v) for k, v in self.original_graph.items()})
        self.Gm = []
        self.lb = 0
        
        for _ in range(max_iter):
            # Step 1: Lower bound update
            clique_size = self.max_clique_heuristic()
            self.lb = max(self.lb, clique_size)
            
            # Step 2: Graph reduction
            S = self.bounded_independent_set(self.lb)
            if not S:
                break
                
            # Remove independent set
            self.Gm.append(S)
            for u in S:
                for v in self.Gk[u]:
                    self.Gk[v].remove(u)
                del self.Gk[u]
            
            # Step 3: Kernel coloring
            kernel_coloring = self.color_kernel()
            
            # Step 4: Coloring extension
            full_coloring = self.extend_coloring(kernel_coloring)
            num_colors = len(set(full_coloring.values()))
            
            # Step 5: Update upper bound
            if num_colors < self.ub:
                self.ub = num_colors
                self.best_coloring = full_coloring
                
            # Early termination
            if self.ub <= self.lb:
                break
                
        return self.best_coloring

# Usage example
if __name__ == "__main__":
    # Example graph from paper's Figure 1
    graph = {
        0: {1, 2},
        1: {0, 2},
        2: {0, 1, 3},
        3: {2}
    }
    
    solver = FastColor(graph)
    coloring = solver.execute()
    print(f"Optimal coloring: {coloring}")
    print(f"Colors used: {len(set(coloring.values()))}")
