Problem p03019 (Python)

Buggy submission (status: Time Limit Exceeded):
import os
import sys

import numpy as np

if os.getenv("LOCAL"):
    sys.stdin = open("_in.txt", "r")

sys.setrecursionlimit(2147483647)
INF = float("inf")


def bisect_right_callable(fn, x, lo, hi):
    """
    lo から hi-1 のうち、fn の結果が x 以下となる、最も右の値 + 1
    bisect.bisect_right と同じ
    https://docs.python.org/ja/3/library/bisect.html
    :param callable fn:
    :param x:
    :param int lo: 最小値
    :param int hi: 最大値 + 1
    :return: lo <= ret <= hi
    """
    while lo < hi:
        mid = (lo + hi) // 2
        if x < fn(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo


N, X = list(map(int, sys.stdin.readline().split()))
B, L, U = list(zip(*[map(int, sys.stdin.readline().split()) for _ in range(N)]))

B = np.array(B, dtype=int)
L = np.array(L, dtype=int)
U = np.array(U, dtype=int)

# 全部 L で B 点とると仮定する
target = (B * L).sum()

# こうすれば target と比較できる
scores = B * L + (X - B) * U
scores_i = scores.argsort()

scores = scores[scores_i]
scores_cumsum = np.concatenate((scores[::-1].cumsum()[::-1], [0]))
B = B[scores_i]
L = L[scores_i]
U = U[scores_i]


def get_max_score(x):
    # x 点とったときの最大のスコア

    # 満点の数
    perfects = x // X
    # 途中まで頑張るやつは何点取ればいいか
    half_x = x % X

    max_score = 0
    for i in range(N):
        # i 番目のテストだけ途中まで頑張る
        # 満点の合計
        if i < N - perfects:
            score = scores_cumsum[N - perfects]
        else:
            score = scores_cumsum[N - perfects - 1] - scores[i]
        score += min(half_x, B[i]) * L[i] + (max(B[i], half_x) - B[i]) * U[i]
        max_score = max(max_score, score)
    return max_score


ans = bisect_right_callable(get_max_score, target - 1, 0, B.sum())
print(ans)


Fixed submission:
import os
import sys

import numpy as np

if os.getenv("LOCAL"):
    sys.stdin = open("_in.txt", "r")

sys.setrecursionlimit(2147483647)
INF = float("inf")


def bisect_right_callable(fn, x, lo, hi):
    """
    lo から hi-1 のうち、fn の結果が x 以下となる、最も右の値 + 1
    bisect.bisect_right と同じ
    https://docs.python.org/ja/3/library/bisect.html
    :param callable fn:
    :param x:
    :param int lo: 最小値
    :param int hi: 最大値 + 1
    :return: lo <= ret <= hi
    """
    while lo < hi:
        mid = (lo + hi) // 2
        if x < fn(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo


N, X = list(map(int, sys.stdin.readline().split()))
B, L, U = list(zip(*[map(int, sys.stdin.readline().split()) for _ in range(N)]))

B = np.array(B, dtype=int)
L = np.array(L, dtype=int)
U = np.array(U, dtype=int)

# 全部 L で B 点とると仮定する
target = (B * L).sum()

# こうすれば target と比較できる
scores = B * L + (X - B) * U
scores_i = scores.argsort()

scores = scores[scores_i]
scores_cumsum = np.concatenate((scores[::-1].cumsum()[::-1], [0]))
B = B[scores_i]
L = L[scores_i]
U = U[scores_i]


def get_max_score(x):
    # x 点とったときの最大のスコア

    # 満点の数
    perfects = x // X
    # 途中まで頑張るやつは何点取ればいいか
    half_x = x % X

    border = N - perfects
    ret = []
    if len(B[:border]) > 0:
        # 途中まで頑張るやつを 0 点エリアから選択
        ret.append(
            max(
                scores_cumsum[border]
                + np.minimum(half_x, B[:border]) * L[:border]
                + np.maximum(0, half_x - B[:border]) * U[:border]
            )
        )
    if len(B[border:]) > 0:
        # 途中まで頑張るやつを満点エリアから選択
        ret.append(
            max(
                scores_cumsum[border - 1]
                - scores[border:]
                + np.minimum(half_x, B[border:]) * L[border:]
                + np.maximum(0, half_x - B[border:]) * U[border:]
            )
        )
    return max(0, *ret)


ans = bisect_right_callable(get_max_score, target - 1, 0, B.sum())
print(ans)