Before attempting this problem, you should be comfortable with:
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.
count(cur_goal, old_songs) where cur_goal is the remaining playlist slots and old_songs is the number of distinct songs used so far.cur_goal == 0 and old_songs == n, we have a valid playlist, return 1.cur_goal == 0 or old_songs > n, this path is invalid, return 0.(n - old_songs) new songs available, and this leads to count(cur_goal - 1, old_songs + 1).old_songs > k, there are (old_songs - k) songs eligible for replay, leading to count(cur_goal - 1, old_songs).count(goal, 0) as the final answer, modulo 10^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)Where is the number of songs to listen and is the number of different songs.
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.
dp[goal+1][n+1] initialized to 0, with dp[0][0] = 1 as the base case (empty playlist with no songs used).cur_goal from 1 to goal:old_songs from 1 to n:dp[cur_goal - 1][old_songs - 1] by (n - old_songs + 1).old_songs > k): add dp[cur_goal - 1][old_songs] * (old_songs - k).dp[cur_goal][old_songs], taking modulo 10^9 + 7.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]Where is the number of songs to listen and is the number of different songs.
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).
dp[n+1] initialized to 0.cur_goal from 1 to goal:prev to store the value from the previous row that we need before overwriting.prev = 1 when cur_goal == 1 (base case for adding the first song), otherwise prev = 0.old_songs from 1 to n:prev (for adding a new song) and dp[old_songs] (for replaying an old song).prev to the old value of dp[old_songs] before overwriting.dp[old_songs].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]Where is the number of songs to listen and is the number of different 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.
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.
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.
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.
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].