920. Number of Music Playlists - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming - Both top-down (memoization) and bottom-up tabulation approaches
- Combinatorics - Understanding how to count arrangements with constraints (permutations with restrictions)
- Modular Arithmetic - Performing operations under modulo to handle large numbers without overflow
- State Space Design - Defining DP states with multiple dimensions (playlist length and distinct songs used)
1. Dynamic Programming (Top-Down)
Intuition
We need to count valid playlists of exactly goal songs using exactly n different songs, where a song can only repeat after k other songs have played. At each position in the playlist, we have two choices: add a new song we have not used yet, or replay an old song (if enough songs have been played since its last appearance). The number of ways to add a new song depends on how many songs we have already used, and the number of ways to replay an old song depends on how many songs are eligible for replay.
Algorithm
- Define a recursive function
count(cur_goal, old_songs)wherecur_goalis the remaining playlist slots andold_songsis the number of distinct songs used so far. - Base cases:
- If
cur_goal == 0andold_songs == n, we have a valid playlist, return1. - If
cur_goal == 0orold_songs > n, this path is invalid, return0.
- If
- Recursive transitions:
- Add a new song: there are
(n - old_songs)new songs available, and this leads tocount(cur_goal - 1, old_songs + 1). - Replay an old song: if
old_songs > k, there are(old_songs - k)songs eligible for replay, leading tocount(cur_goal - 1, old_songs).
- Add a new song: there are
- Memoize results to avoid recomputation.
- Return
count(goal, 0)as the final answer, modulo10^9 + 7.
class Solution:
def numMusicPlaylists(self, n: int, goal: int, k: int) -> int:
mod = 10**9 + 7
dp = {}
def count(cur_goal, old_songs):
if cur_goal == 0 and old_songs == n:
return 1
if cur_goal == 0 or old_songs > n:
return 0
if (cur_goal, old_songs) in dp:
return dp[(cur_goal, old_songs)]
res = (n - old_songs) * count(cur_goal - 1, old_songs + 1)
if old_songs > k:
res += (old_songs - k) * count(cur_goal - 1, old_songs)
dp[(cur_goal, old_songs)] = res % mod
return dp[(cur_goal, old_songs)]
return count(goal, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of songs to listen and is the number of different songs.
2. Dynamic Programming (Bottom-Up)
Intuition
The top-down solution can be converted to a bottom-up approach by iterating through all possible states. We build a 2D table where dp[i][j] represents the number of ways to create a playlist of length i using exactly j distinct songs. We fill this table row by row, starting from the base case and building up to our target state.
Algorithm
- Create a 2D array
dp[goal+1][n+1]initialized to0, withdp[0][0] = 1as the base case (empty playlist with no songs used). - For each playlist length
cur_goalfrom1togoal:- For each count of distinct songs
old_songsfrom1ton:- Add a new song: multiply
dp[cur_goal - 1][old_songs - 1]by(n - old_songs + 1). - Replay an old song (if
old_songs > k): adddp[cur_goal - 1][old_songs] * (old_songs - k). - Store the sum in
dp[cur_goal][old_songs], taking modulo10^9 + 7.
- Add a new song: multiply
- For each count of distinct songs
- Return
dp[goal][n].
class Solution:
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 cur_goal in range(1, goal + 1):
for old_songs in range(1, n + 1):
res = (dp[cur_goal - 1][old_songs - 1] * (n - old_songs + 1)) % mod
if old_songs > k:
res = (res + dp[cur_goal - 1][old_songs] * (old_songs - k)) % mod
dp[cur_goal][old_songs] = res
return dp[goal][n]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of songs to listen and is the number of different songs.
3. Dynamic Programming (Space Optimized)
Intuition
Looking at the bottom-up recurrence, each row only depends on the previous row. This means we do not need to store the entire 2D table. Instead, we can use a single 1D array and update it in place, being careful to preserve the previous row's values where needed. This reduces space from O(goal * n) to O(n).
Algorithm
- Create a 1D array
dp[n+1]initialized to0. - For each playlist length
cur_goalfrom1togoal:- Track
prevto store the value from the previous row that we need before overwriting. - Initialize
prev = 1whencur_goal == 1(base case for adding the first song), otherwiseprev = 0. - For each count of distinct songs
old_songsfrom1ton:- Compute the new value using
prev(for adding a new song) anddp[old_songs](for replaying an old song). - Update
prevto the old value ofdp[old_songs]before overwriting. - Store the result in
dp[old_songs].
- Compute the new value using
- Track
- Return
dp[n].
class Solution:
def numMusicPlaylists(self, n: int, goal: int, k: int) -> int:
mod = 10**9 + 7
dp = [0] * (n + 1)
for cur_goal in range(1, goal + 1):
prev = 1 if cur_goal == 1 else 0
for old_songs in range(1, n + 1):
res = (prev * (n - old_songs + 1)) % mod
if old_songs > k:
res = (res + dp[old_songs] * (old_songs - k)) % mod
prev = dp[old_songs]
dp[old_songs] = res
return dp[n]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of songs to listen and is the number of different songs.
Common Pitfalls
Forgetting the Gap Constraint for Replaying Songs
A song can only be replayed after k other songs have been played. This means with old_songs distinct songs used, only old_songs - k are eligible for replay (if old_songs > k). Forgetting this constraint or using old_songs directly leads to overcounting.
Integer Overflow in Modular Arithmetic
Multiplying two values before taking the modulo can overflow 32-bit integers. Always cast to long or long long before multiplication, then apply modulo. For example, (n - oldSongs) * count can overflow if not handled properly.
Incorrect Base Case Definition
The base case should be dp[0][0] = 1 (one way to have an empty playlist with zero songs used). Setting dp[0][n] = 1 or other incorrect initializations will propagate errors through all subsequent states.
Confusing State Dimensions
The DP state tracks (playlist_length, distinct_songs_used). Confusing which dimension is which, or iterating in the wrong order, leads to accessing uncomputed states or incorrect transitions. In bottom-up, iterate playlist length in the outer loop and distinct songs in the inner loop.
Not Requiring Exactly N Distinct Songs
The final answer must use exactly n distinct songs. A common error is returning dp[goal][any] summed over all possible song counts, when it should specifically be dp[goal][n].