Number of Music Playlists: DP with Replay Constraints
LeetCode Number of Music Playlists (problem 920) asks you to count how many valid playlists you can build given some replay constraints. You have n different songs and need to create a playlist of length goal. Every song must appear at least once, and a song can only be replayed after k other songs have been played since its last appearance. Return the count modulo 10^9 + 7.
The problem
Given integers n, goal, and k:
- The playlist has
goalsongs total - Every one of
nsongs must appear at least once - A song can only be replayed if at least
kother songs have been played between occurrences
Return the number of possible playlists, modulo 10^9 + 7.
The brute force approach
You could try to enumerate all sequences of length goal from n songs, then filter to those where every song appears and the replay constraint holds. But with goal up to 100 and n up to 100, the number of raw sequences is astronomical. Even with backtracking, the branching factor is too large to explore directly.
The key observation is that this is a counting problem with overlapping subproblems. At each position in the playlist, your choice depends on how many unique songs you have used so far and what the replay rule allows. That structure points directly at dynamic programming.
The key insight
Define dp[i][j] as the number of valid playlists of length i that contain exactly j unique songs. At each position, you have two choices:
-
Add a new song. If you have used
j-1unique songs so far, you can pick any of then - (j-1)remaining songs. This gives:dp[i-1][j-1] * (n - j + 1). -
Replay an old song. If you already have
junique songs in a playlist of lengthi-1, you can replay one that is not in the lastkpositions. The number of eligible songs ismax(j - k, 0). This gives:dp[i-1][j] * max(j - k, 0).
The replay constraint is the subtle part. If you have j unique songs and k of the most recent ones are off-limits, only j - k songs are available for replay. When j is less than or equal to k, you cannot replay anything at all.
The base case is dp[0][0] = 1 (an empty playlist with zero songs is trivially valid). The answer is dp[goal][n], since you need all n songs to appear.
Step-by-step walkthrough
Let's trace through a small example with n=2, goal=3, k=0. Since k=0, any song can be replayed immediately (no gap required).
Step 1: Base case dp[0][0] = 1
An empty playlist with 0 unique songs: there is exactly 1 way (do nothing).
Step 2: Fill row i=1 (playlist length 1)
dp[1][1] = dp[0][0] * (n - 1 + 1) = 1 * 2 = 2. Pick any of 2 songs as the first song. Replay is impossible (j - k = 1 - 0 = 1, but dp[0][1] = 0).
Step 3: Fill row i=2 (playlist length 2)
dp[2][1] = dp[1][1] * max(1-0, 0) = 2 * 1 = 2 (replay the only known song). dp[2][2] = dp[1][1] * (2-2+1) = 2 * 1 = 2 (add the other new song).
Step 4: Fill row i=3 (playlist length 3)
dp[3][1] = dp[2][1] * max(1-0, 0) = 2 * 1 = 2. dp[3][2] = dp[2][1] * 1 + dp[2][2] * max(2-0, 0) = 2 + 4 = 6. Answer: dp[3][2] = 6.
Step 5: Verify the answer
With songs A and B, the 6 valid playlists of length 3 are: ABA, ABB, BAB, BAA, AAB, BBA. Each uses both songs and respects k=0 (any song can replay immediately).
The code
def numMusicPlaylists(self, n: int, goal: int, k: int) -> int:
MOD = 10**9 + 7
dp = [[0] * (n + 1) for _ in range(goal + 1)]
dp[0][0] = 1
for i in range(1, goal + 1):
for j in range(1, n + 1):
dp[i][j] = dp[i-1][j-1] * (n - j + 1) % MOD
dp[i][j] += dp[i-1][j] * max(j - k, 0) % MOD
dp[i][j] %= MOD
return dp[goal][n]
The outer loop iterates over playlist positions from 1 to goal. The inner loop iterates over the number of unique songs from 1 to n. For each cell, you combine the "add new" and "replay old" transitions. The modulo operation keeps values from overflowing.
You can optimize space to O(n) by only keeping two rows at a time, since each row depends only on the previous row. But the O(goal * n) solution is clean enough for interview purposes and the constraints (both at most 100) make it fast.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| DP (2D table) | O(goal * n) | O(goal * n) |
| DP (space-optimized) | O(goal * n) | O(n) |
With goal and n both at most 100, the 2D table has at most 10,000 cells. Each cell is computed in O(1), making the total work trivial for any modern machine. The space-optimized version uses a single array of length n + 1 and alternates between two rows.
The building blocks
Counting with constrained transitions
The core pattern here is a DP where transitions depend on what has already been used. You see this same structure in Decode Ways, where valid decodings depend on whether the current digit pair forms a valid letter. The playlist problem adds a "cooldown" flavor through the k parameter.
Combinatorial DP with modular arithmetic
Since the answer can be enormous, you work modulo 10^9 + 7 throughout. This is standard for counting problems on LeetCode. The key rule: apply % MOD after every multiplication and addition to prevent overflow. Python handles big integers natively, but the modulo keeps you in the habit for languages that do not.
The "must use all" constraint
The requirement that every song appears at least once is what makes dp[goal][n] the answer rather than summing over all j. This "must cover all items" constraint also appears in problems like Combination Sum IV, where you need specific targets, and in set-cover style counting problems.
Edge cases
- n equals goal with k equals n-1. Every song plays exactly once, and the replay rule never triggers. The answer is
n!(all permutations). - k equals 0. Songs can be replayed immediately. This maximizes the count since there are no cooldown restrictions.
- n equals 1 and goal equals 1 and k equals 0. Only one playlist: the single song. The DP correctly gives
dp[1][1] = 1. - goal equals n. You must play each song exactly once (no room for replays). The answer is
n!regardless ofk, since every position introduces a new song. - Large k close to n. When
kis close ton, replay becomes very restricted. You might be forced to keep introducing new songs for a long time before any replay is possible.
From understanding to recall
The tricky part of this problem is not the code. It is remembering the state definition and the two transitions. When you review this problem, focus on these questions:
- What do
iandjrepresent? (Playlist length and number of unique songs used.) - Why is "add new" multiplied by
n - j + 1? (That is how many songs you have not yet used.) - Why is "replay" multiplied by
max(j - k, 0)? (Only songs outside the lastkpositions are eligible.)
Practice writing the recurrence from scratch. Once you can derive dp[i][j] = dp[i-1][j-1] * (n-j+1) + dp[i-1][j] * max(j-k, 0) without looking, you own this problem. The code follows mechanically from the recurrence.
Related posts
- Climbing Stairs - The simplest DP counting problem that builds intuition for state transitions
- Decode Ways - Another counting DP where constraints limit which transitions are valid
- Combination Sum IV - Counting arrangements with repetition constraints similar to the playlist rule