Buggy python submission (verdict: Time Limit Exceeded):
#!/usr/bin/env python3
import sys
from collections import defaultdict
import heapq
from copy import deepcopy

# 各重さごとに価値降順でソート
# とっていく
# 重さ合計は w0 * k ~ (w0 + 3) * kのどれか N=100だと15000通りくらい

def solve(N: int, W: int, ws: "List[int]", vs: "List[int]"):
    w0 = ws[0]
    dp = defaultdict(lambda: [-1, []]) # dp[w]: 重さ合計wでの[最大価値, [w, w+1, w+2, w+3の数のリスト]]
    dp[0] = [0, [[0, 0, 0, 0]]]
    Q = []
    items = defaultdict(list)
    for w, v in zip(ws, vs):
        items[w].append(v)
    for w in range(w0, w0+4):
        items[w].sort(reverse=True)
    heapq.heappush(Q, 0)
    while len(Q) > 0:
        w_c = heapq.heappop(Q)
        if w_c > W:
            break
        v_c = dp[w_c][0]
        w_counts = dp[w_c][1]
        for w_count in w_counts:
            for i in range(4):
                if w_count[i] >= len(items[w0+i]):
                    continue
                
                v_n = v_c + items[w0+i][w_count[i]]
                w_n = w_c + w0+i
                if v_n < dp[w_n][0]:
                    continue
                
                w_count_n = deepcopy(w_count)
                w_count_n[i] += 1
                if v_n == dp[w_n][0]:
                    if w_count_n not in dp[w_n][1]:
                        dp[w_n][1].append(w_count_n)
                elif v_n > dp[w_n][0]:
                    dp[w_n][1] = [w_count_n]
                    dp[w_n][0] = v_n
                if w_n not in Q:
                    heapq.heappush(Q, w_n)
    res = 0
    for w in range(W+1):
        if dp[w][0] > res:
            res = dp[w][0]
    print(res)

    return


# Generated by 1.1.4 https://github.com/kyuridenamida/atcoder-tools  (tips: You use the default template now. You can remove this line by using your custom template)
def main():
    def iterate_tokens():
        for line in sys.stdin:
            for word in line.split():
                yield word
    tokens = iterate_tokens()
    N = int(next(tokens))  # type: int
    W = int(next(tokens))  # type: int
    w = [int()] * (N)  # type: "List[int]" 
    v = [int()] * (N)  # type: "List[int]" 
    for i in range(N):
        w[i] = int(next(tokens))
        v[i] = int(next(tokens))
    solve(N, W, w, v)

if __name__ == '__main__':
    main()


Fixed submission:
#!/usr/bin/env python3
import sys
from itertools import accumulate, product

# 各重さごとに価値降順でソート
# とっていく
# 重さ合計は w0 * k ~ (w0 + 3) * kのどれか N=100だと15000通りくらい

def solve(N: int, W: int, ws: "List[int]", vs: "List[int]"):
    w0 = ws[0]
    items = [[] for _ in range(4)]
    accum = [[] for _ in range(4)]
    for w, v in zip(ws, vs):
        items[w-w0].append(v)
    for i in range(4):
        items[i].sort(reverse=True)
        accum[i] = [0] + list(accumulate(items[i]))
    count = [len(items[i]) for i in range(4)]
    
    res = 0
    for nums in product(*[range(count[i] + 1) for i in range(4)]):
        w = sum((w0+i) * nums[i] for i in range(4))
        if w > W:
            continue
        v = sum(accum[i][nums[i]] for i in range(4))
        res = max(v, res)
    
    print(res)
    return


# Generated by 1.1.4 https://github.com/kyuridenamida/atcoder-tools  (tips: You use the default template now. You can remove this line by using your custom template)
def main():
    def iterate_tokens():
        for line in sys.stdin:
            for word in line.split():
                yield word
    tokens = iterate_tokens()
    N = int(next(tokens))  # type: int
    W = int(next(tokens))  # type: int
    w = [int()] * (N)  # type: "List[int]" 
    v = [int()] * (N)  # type: "List[int]" 
    for i in range(N):
        w[i] = int(next(tokens))
        v[i] = int(next(tokens))
    solve(N, W, w, v)

if __name__ == '__main__':
    main()