公式

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

Gemini 3.0 Flash (Thinking)

概要

\(2N\) 曲の中から合計再生時間が \(K\) 秒以上になるように曲を選び、それらを並べたときに同じジャンルが連続する最大個数(偏りスコア)を最小化する問題です。 条件を満たす曲の選び方と並べ方のうち、最小の偏りスコアを求めます。

考察

1. 偏りスコア \(X\) の条件

ある \(M\) 曲を選んだとき、特定のジャンル \(g\) に属する曲数を \(n_g\) とします。この \(M\) 曲をうまく並べて偏りスコアを \(X\) 以下にできる条件を考えます。

ジャンル \(g\) 以外の曲数は \(M - n_g\) です。これらを「仕切り」として使うと、ジャンル \(g\) の曲を置ける場所は、仕切りの前後と間の合計 \((M - n_g + 1)\) 箇所あります。 各箇所に最大 \(X\) 個までジャンル \(g\) の曲を置けるため、以下の不等式が成り立てば偏りスコアを \(X\) 以下に抑えられます。 $\(n_g \leq X \times (M - n_g + 1)\)$

これを \(n_g\) について解くと: $\(n_g \leq \frac{X(M+1)}{X+1}\)\( 曲数は整数なので、\)n_g \leq \lfloor \frac{X(M+1)}{X+1} \rfloor\( となります。つまり、**どのジャンルからも最大 \)C = \lfloor \frac{X(M+1)}{X+1} \rfloor$ 曲までしか選べない**という制約に言い換えられます。

2. 答えの二分探索

偏りスコア \(X\) が小さければ小さいほど条件は厳しくなり、大きければ緩くなります。この単調性を利用して、最小の \(X\) を二分探索で求めます。

3. 判定関数の設計(貪欲法)

固定された \(X\) に対して、条件を満たす曲の選び方があるかを判定します。 選ぶ曲数 \(M\)\(1\) から \(2N\) まで全通り試します。各 \(M\) について、合計再生時間を最大化するには以下の戦略が有効です。 1. 各ジャンル内で、再生時間が長い順に曲を並べておく。 2. 全曲の中から、「そのジャンル内で \(C\) 番目以内」の曲だけを候補とする。 3. 候補の中から再生時間が大きい順に \(M\) 曲選び、その合計が \(K\) 以上になるかを確認する。

アルゴリズム

  1. 前処理:
    • 曲をジャンルごとにグループ化し、各ジャンル内で再生時間の降順にソートします。
    • 全ての曲に対し、「そのジャンル内で何番目に長い曲か(ランク)」を記録します。
  2. 二分探索:
    • 範囲 \([1, 2N]\) で偏りスコア \(X\) を二分探索します。
  3. 判定関数 check(X):
    • \(M = 1\) から \(2N\) までループします。
    • \(M\) に対し、各ジャンルから選べる上限数 \(C = \lfloor \frac{X(M+1)}{X+1} \rfloor\) を計算します。
    • ジャンル内ランクが \(C\) 以下の曲を再生時間の降順に \(M\) 個選び、合計が \(K\) 以上なら true を返します。
  4. 出力:
    • 二分探索で見つかった最小の \(X\) を出力します。一度も条件を満たさなければ \(-1\) を出力します。

計算量

  • 時間計算量: \(O(N^2 \log N)\)
    • 曲のソート: \(O(N \log N)\)
    • 二分探索の回数: \(O(\log N)\)
    • 判定関数: \(M\) のループ (\(2N\) 回) \(\times\) 曲の走査 (\(2N\) 回) = \(O(N^2)\)
    • 全体で \(O(N^2 \log N)\) となり、\(N=150\) では十分に高速です。
  • 空間計算量: \(O(N)\)
    • 曲の情報を保持するリストなどに使用します。

実装のポイント

  • ソートの工夫: 判定関数の中で毎回ソートすると遅くなる可能性があるため、あらかじめ全曲を「再生時間の降順」でソートしておき、判定関数内ではランクの比較だけで \(M\) 曲選ぶようにすると効率的です。

  • \(M\) 曲選べない場合: \(C\) の制限によって合計で \(M\) 曲選べない場合は、その \(M\) については条件不適としてスキップします。

  • \(C=0\) のケア: \(X\)\(M\) が小さいときに \(C=0\) になる場合があります。この場合、そのジャンルからは 1 曲も選べません。

    ソースコード

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

/**
 * Problem Analysis:
 * We need to find the minimum bias score X such that we can select M songs (1 <= M <= 2N)
 * with a total duration >= K and arrange them so that no more than X consecutive songs
 * are of the same genre.
 * 
 * The condition for an arrangement of M songs to have a bias score of at most X is that
 * for each genre g, the number of songs selected from that genre, n_g, must satisfy:
 * n_g <= X * (M - n_g + 1)
 * This simplifies to:
 * n_g * (X + 1) <= X * (M + 1)
 * n_g <= floor(X * (M + 1) / (X + 1))
 * 
 * To maximize the total duration for a fixed bias score X and total song count M, 
 * we should pick the M largest duration songs across all genres, with the restriction 
 * that we can pick at most C = floor(X * (M + 1) / (X + 1)) songs from each genre.
 */

struct Song {
    int duration;
    int rank; // rank of the song within its genre (1-indexed, largest duration is rank 1)
};

int main() {
    // Optimize I/O
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    long long K;
    if (!(cin >> N >> K)) return 0;

    // Group songs by genre
    vector<vector<int>> genre_songs(2 * N + 1);
    for (int i = 0; i < 2 * N; ++i) {
        int b, a;
        cin >> b >> a;
        if (b >= 1 && b <= 2 * N) {
            genre_songs[b].push_back(a);
        }
    }

    // Sort songs within each genre and store them with their genre-rank
    vector<Song> all_songs;
    for (int i = 1; i <= 2 * N; ++i) {
        if (genre_songs[i].empty()) continue;
        sort(genre_songs[i].rbegin(), genre_songs[i].rend());
        for (int j = 0; j < (int)genre_songs[i].size(); ++j) {
            all_songs.push_back({genre_songs[i][j], j + 1});
        }
    }

    // Sort all songs by duration in descending order
    sort(all_songs.begin(), all_songs.end(), [](const Song& a, const Song& b) {
        if (a.duration != b.duration) return a.duration > b.duration;
        return a.rank < b.rank;
    });

    // Check if a bias score X is achievable
    auto check = [&](int X) {
        for (int M = 1; M <= 2 * N; ++M) {
            // Maximum songs allowed per genre for this M and X
            long long C = (long long)X * (M + 1) / (X + 1);
            if (C == 0) continue; 

            long long current_sum = 0;
            int count = 0;
            // Greedily pick the M best available songs
            for (const auto& song : all_songs) {
                if (song.rank <= C) {
                    current_sum += (long long)song.duration;
                    count++;
                    if (count == M) break;
                }
            }
            // If we found exactly M songs and their sum is >= K, X is achievable
            if (count == M && current_sum >= K) return true;
        }
        return false;
    };

    // Binary search for the minimum achievable bias score X
    int low = 1, high = 2 * N;
    int ans = -1;

    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (check(mid)) {
            ans = mid;
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    cout << ans << endl;

    return 0;
}

この解説は gemini-3-flash-thinking によって生成されました。

投稿日時:
最終更新: