from collections import defaultdict
class Node:
    def __init__(self,key,value):
        self.key = key
        self.val = value
        self.cnt = 1
        self.prev = None
        self.next = None

class DLList:
    def __init__(self):
        self.sentinel = Node(None,None)
        self.sentinel.next = self.sentinel.prev = self.sentinel
        self.size_dllist = 0 # record current dllist length
        
    def add_head(self,node):
        Next = self.sentinel.next
        node.next = Next
        node.prev = self.sentinel
        Next.prev = node
        self.sentinel.next = node 
        self.size_dllist += 1
        
    def remove(self, node):
        if self.size_dllist == 0:
            return
        node.prev.next = node.next
        node.next.prev = node.prev
        self.size_dllist -= 1
        return node
    
    def remove_tail(self):
        if self.size_dllist == 0:
            return 
        node = self.sentinel.prev
        #print('remove taill',node.val)
        node.prev.next =node.next
        node.next.prev = node.prev
        self.size_dllist -= 1
        return node 
        
        
        
class LFUCache:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.size = 0
        self.d = defaultdict() # key:Node
        self.frequent_d = defaultdict(DLList)   # frequency :  DLList
        self.min_freq = 1 # to 
    
    def _update(self,key,value):
        # 1.when get(key) is called:  get node, remove the node, update node val, freq +1, add back to freq_d 
        # 2.1 when put(key) is called and the key exist:
        #                   find node from d, update value, call get(key)
       
        node = self.d[key]
        node.val = value
        old_freq = node.cnt
        self.frequent_d[old_freq].remove(node)
        if self.min_freq == old_freq and not self.frequent_d[old_freq]: # if there is no linkedlist exist at old_freq, increment min_freq
            self.min_freq += 1
        new_freq = old_freq +1 
        node.cnt = new_freq
        self.frequent_d[node.cnt].add_head(node)

    def get(self, key: int) -> int:  # cap does not change 
        if key not in self.d:
            return -1
        #print(self.d.keys())
        node = self.d[key]
        prev_dllist = self.frequent_d[node.cnt]
        prev_dllist.remove(node)
        prev_freq = node.cnt
        cur_freq = prev_freq + 1
        node.cnt = cur_freq
        cur_ddlist = self.frequent_d[node.cnt]
        cur_ddlist.add_head(node)
        return node.val
    
 # 2.2 when put(key) is called and key not exist:
        #                   check if cache size has reached capacity, if yes, delete least frequent one(ensure len(DLList)> 0), otherwise ,create a new node, add_head(node) to DLList  with freq = 1
        
    def put(self, key: int, value: int) -> None:
        if self.cap == 0:
            return 
        if key in self.d:
            self._update(key,value)
        # key not apprear before
        else:
            node = Node(key,value)
            self.d[key] = node
            self.frequent_d[node.cnt].add_head(node)
            self.size += 1
            if self.size > self.cap:
                if self.frequent_d[self.min_freq].size_dllist > 1:
                    node = self.frequent_d[self.min_freq].remove_tail()
                    self.size -= 1
                else:
                    while True:
                        self.min_freq += 1
                        if self.frequent_d[self.min_freq].size_dllist > 0:
                            node = self.frequent_d[self.min_freq].remove_tail()
                            self.size -= 1
                            break
                del self.d[node.key]
                self.min_freq = 1
# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

Without DLList version

class LFUCache:
    def __init__(self, capacity: int):
        self.size = capacity
        self.freq_pair = defaultdict(list)
        self.key_freq = {} # key:freq, can also check if current LFU exceed capacity limit  
        self.least_f = 1
    # tc O(N) sc O(N)
    def get(self, key: int) -> int:
        res = -1
        if key in self.key_freq:
            freq = self.key_freq[key]
            for i,kv in enumerate(self.freq_pair[freq]):
                if kv[0] == key:
                    res = kv[1]
                    del self.freq_pair[freq][i]#self.freq_pair[freq].pop(i)
                    if freq == self.least_f and len(self.freq_pair[freq])==0:
                        self.least_f  += 1
                    break
            new_freq = freq + 1
            self.freq_pair[new_freq].append((key,res))
            self.key_freq[key] = new_freq 
        return res 
    # tc O(N) sc O(N)
    def put(self, key: int, value: int) -> None:
        # need to discuss edge case when capasity = 0 
        if self.size == 0:
            return 
        if key in self.key_freq:
            freq = self.key_freq[key]
            for i,kv in enumerate(self.freq_pair[freq]):
                if kv[0] == key:
                    self.freq_pair[freq].pop(i)
                    # print(f'after pop put {self.freq_pair[freq]}')
                    self.key_freq.pop(key) # don't forget to update hashmap 
                    if freq == self.least_f and len(self.freq_pair[freq])==0:
                        self.least_f  += 1
                    break    
        else:
            freq = 0
        new_freq = freq + 1
        if len(self.key_freq) == self.size:
            k,v = self.freq_pair[self.least_f].popleft()#slite improvement than using list .pop(0)
            self.key_freq.pop(k)
        self.freq_pair[new_freq].append((key,value))
        self.key_freq[key] = new_freq

        self.least_f = min(self.least_f,new_freq)
            


# Your LFUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)