Official

E - プレイリストのジャンル偏り最小化 / Minimizing Genre Imbalance in a Playlist Editorial by admin

gpt-5.5-high

概要

曲をいくつ選ぶかと、各ジャンルから何曲選ぶかに注目します。
「偏りスコアを \(x\) 以下にできる条件」をジャンルごとの選択曲数の上限に言い換え、その上限のもとで選べる最大再生時間を DP で求めます。

考察

まず、ある曲集合を選んだ後、それを並べ替えて偏りスコアを \(x\) 以下にできる条件を考えます。

選んだ曲数を \(s\)、あるジャンルの曲数を \(c\) とします。
このジャンル以外の曲は \(s-c\) 曲あります。

この \(s-c\) 曲を並べると、その前後や間に、このジャンルの曲を入れられる「隙間」が \(s-c+1\) 個できます。

例えば、他ジャンルの曲が だとすると、

_ ○ _ ○ _ ○ _

のように、隙間は \(4\) 個あります。

偏りスコアを \(x\) 以下にしたいなら、各隙間に同じジャンルの曲を高々 \(x\) 曲までしか置けません。
したがって必要な条件は

\[ c \leq x(s-c+1) \]

です。これを変形すると、

\[ (x+1)c \leq x(s+1) \]

なので、

\[ c \leq \left\lfloor \frac{x(s+1)}{x+1} \right\rfloor \]

となります。

実はこの条件は十分条件でもあります。
つまり、選んだ \(s\) 曲について、すべてのジャンルの選択数が

\[ \left\lfloor \frac{x(s+1)}{x+1} \right\rfloor \]

以下なら、偏りスコアを \(x\) 以下に並べることができます。

よって、偏りスコア \(x\) が可能かどうかは、

ある曲数 \(s\) について、各ジャンルから高々
\(L = \left\lfloor \frac{x(s+1)}{x+1} \right\rfloor\) 曲まで選ぶという制限のもとで、
合計再生時間を \(K\) 秒以上にできるか

を判定すればよいです。


次に、各ジャンルから何曲選ぶかを考えます。

あるジャンルから \(t\) 曲選ぶと決めたなら、再生時間の合計を最大にするには、そのジャンルの中で再生時間が長い順に \(t\) 曲選べばよいです。

そのため、ジャンルごとに曲の再生時間を降順ソートし、累積和を持っておきます。

例えば、あるジャンルの曲の再生時間が

\[ 10, 7, 4 \]

なら、累積和は

\[ 0, 10, 17, 21 \]

です。
このジャンルから \(2\) 曲選ぶときの最大再生時間は \(17\) 秒です。


素朴に曲集合を全探索すると \(2^{2N}\) 通りあり、最大で \(2^{300}\) 通りになるため不可能です。
また、\(K\) は最大 \(10^9\) なので、再生時間を DP の状態にすることもできません。

そこで、「選んだ曲数」を DP の状態にします。

アルゴリズム

曲数を \(M = 2N\) とします。

まず、次の DP を考えます。

\[ F[L][s] = \]

各ジャンルから高々 \(L\) 曲まで選べるとき、合計でちょうど \(s\) 曲選んだ場合の最大再生時間。

この \(F[L][s]\) を、すべての \(L\) について前計算します。

DP の作り方

ジャンルごとに処理します。

あるジャンルについて、再生時間の降順累積和を \(P\) とします。
このジャンルから \(t\) 曲選ぶと、得られる最大再生時間は \(P[t]\) です。

ただし、各ジャンルからは高々 \(L\) 曲までなので、

\[ 0 \leq t \leq \min(\text{そのジャンルの曲数}, L) \]

です。

DP の遷移は次のようになります。

\[ dp_{\text{new}}[j+t] = \max(dp_{\text{new}}[j+t], dp[j] + P[t]) \]

ここで、

  • \(j\) はこれまでに選んだ曲数
  • \(t\) は今回のジャンルから選ぶ曲数

です。

これを全ジャンルに対して行うと、固定した \(L\) に対する \(F[L]\) が求まります。


その後、偏りスコア \(x\)\(1\) から順に試します。

\(x\) について、選ぶ曲数 \(s\)\(1\) から \(M\) まで試します。

このとき必要なジャンルごとの上限は

\[ L = \left\lfloor \frac{x(s+1)}{x+1} \right\rfloor \]

です。

もし

\[ F[L][s] \geq K \]

なら、合計再生時間 \(K\) 秒以上の曲集合を選べて、かつ偏りスコアを \(x\) 以下にできます。

\(x\) を小さい順に調べているので、最初に条件を満たした \(x\) が答えです。

なお、すべての曲の再生時間合計が \(K\) 未満なら、そもそも条件を満たすプレイリストは作れないので、最初に \(-1\) を出力します。

計算量

\(M = 2N\) とします。

  • 時間計算量: \(O(M^3) = O(N^3)\)
  • 空間計算量: \(O(M^2) = O(N^2)\)

\(L\) について DP を行い、\(L\) は最大で \(M\) 通りあります。
固定した \(L\) に対する DP は \(O(M^2)\) でできるため、全体で \(O(M^3)\) です。

実装のポイント

  • ジャンルごとに再生時間を降順ソートし、累積和を作ります。

  • DP では不可能な状態を十分小さい値、例えば \(-10^{18}\) で初期化します。

  • \(F[L][s]\) は「ちょうど \(s\) 曲選ぶ」ときの最大再生時間です。
    偏りスコア判定では \(s\) ごとに上限 \(L\) が変わるため、「ちょうど」で管理する必要があります。

  • \(L\) が実際の最大ジャンル曲数を超える場合は、それ以上制限が緩くならないので、同じ DP 結果を使い回せます。

    ソースコード

import sys

def main():
    data = list(map(int, sys.stdin.buffer.read().split()))
    if not data:
        return

    N, K = data[0], data[1]
    M = 2 * N

    genres = [[] for _ in range(M + 1)]
    total = 0
    idx = 2
    for _ in range(M):
        b = data[idx]
        a = data[idx + 1]
        idx += 2
        genres[b].append(a)
        total += a

    if total < K:
        print(-1)
        return

    prefixes = []
    for arr in genres:
        if arr:
            arr.sort(reverse=True)
            p = [0] * (len(arr) + 1)
            s = 0
            for i, v in enumerate(arr, 1):
                s += v
                p[i] = s
            prefixes.append(p)

    prefixes.sort(key=len)

    max_cap = max(len(p) - 1 for p in prefixes)
    NEG = -10**18

    F = [None] * (M + 1)
    f0 = [NEG] * (M + 1)
    f0[0] = 0
    F[0] = f0

    for L in range(1, max_cap + 1):
        dp = [NEG] * (M + 1)
        dp[0] = 0
        max_s = 0

        for p in prefixes:
            c = len(p) - 1
            if c > L:
                c = L

            ndp = [NEG] * (M + 1)
            limit = max_s + 1
            ndp[:limit] = dp[:limit]

            dpl = dp
            ndpl = ndp

            for t in range(1, c + 1):
                add = p[t]
                j = t
                for s in range(limit):
                    v = dpl[s] + add
                    if v > ndpl[j]:
                        ndpl[j] = v
                    j += 1

            max_s += c
            dp = ndp

        F[L] = dp

    for L in range(max_cap + 1, M + 1):
        F[L] = F[max_cap]

    for x in range(1, M + 1):
        xp1 = x + 1
        for s in range(1, M + 1):
            cap = (x * (s + 1)) // xp1
            if F[cap][s] >= K:
                print(x)
                return

if __name__ == "__main__":
    main()

この解説は gpt-5.5-high によって生成されました。

posted:
last update: