Your music player contains N
different songs and she wants to listen to L
(not necessarily different) songs during your trip. You create a playlist so that:
- Every song is played at least once
- A song can only be played again only if
K
other songs have been played
Return the number of possible playlists. As the answer can be very large, return it modulo 10^9 + 7
.
Example 1:
Input: N = 3, L = 3, K = 1
Output: 6
Explanation: There are 6 possible playlists. [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1].
Example 2:
Input: N = 2, L = 3, K = 0
Output: 6
Explanation: There are 6 possible playlists. [1, 1, 2], [1, 2, 1], [2, 1, 1], [2, 2, 1], [2, 1, 2], [1, 2, 2]
Example 3:
Input: N = 2, L = 3, K = 1
Output: 2
Explanation: There are 2 possible playlists. [1, 2, 1], [2, 1, 2]
Note:
0 <= K < N <= L <= 100
A brute force solution is to check all possible permutations and add those that meet the given requirements to the final count. The runtime is O(L!) * O(L), which is infeasible for a input size up to 100. The following table can be used to guess the required time complexity of the algorithm that solves a problem, assuming a time limit of one second.
Input size | required time complexity |
n <= 10 | O(n !) |
n <= 20 | O(2 ^ n) |
n <= 500 | O(n ^ 3) |
n <= 5000 | O(n ^ 2) |
n <= 10^6 | O(nlogn) or O(n) |
n is large | O(1) or O(logn) |
Anyway, back to business. If we consider only one song's choice at a time, we can solve this problem recursively. For the last picked song, there can only be two cases. Either it is not picked before or it has been picked before.
The recursive formula is as following. If the last song is never picked before, the original problem F(N, L, K) is reduced to F(N - 1, L - 1, K) and we have N songs to choose from. Or the last song has been picked before, the original problem is reduced to F(N, L - 1, K) and we have N - K songs to choose from.(This satisfies the K buffer requirement)
F(N, L, K) = F(N - 1, L - 1) * N + F(N, L - 1, K) * (N - K)
There are two base cases:
1. N == K + 1, this means the number of available songs just meet the buffer requirement. There are N ! permutations and for every permutation, the rest of song choices is fixed since any other ordering breaks the buffering rule.
2. N == L, since each song must be picked at least once, there are N ! possible orderings.
The recursion tree reveals that there will be a lot of redundant recursive computations. For instance, F(N - 1, L - 2) will be computed twice. This is an indication of dynamic programming. A good trick of computing factorials is to use a 1D arrray to store all factorials. This avoids redundant computations as well.
Both dp solutions have a runtime of O((N - K) * (L - K)) and space of O(N * L).
Top down recursion with memoization
class Solution { private int mod = (int)1e9 + 7; private long[] factorial; private long[][] dp; public int numMusicPlaylists(int N, int L, int K) { factorial = new long[N + 1]; dp = new long[N + 1][L + 1]; factorial[0] = 1; for(int i = 1; i <= N; i++) { factorial[i] = factorial[i - 1] * i % mod; } return (int)recursionWithMemo(N, L, K); } private long recursionWithMemo(int n, int l, int k) { if(dp[n][l] == 0) { if(n == l || n == k + 1) { dp[n][l] = factorial[n]; } else { dp[n][l] = (recursionWithMemo(n - 1, l - 1, k) * n + recursionWithMemo(n, l - 1, k) * (n - k)) % mod; } } return dp[n][l]; } }
Bottom up DP
class Solution { public int numMusicPlaylists(int N, int L, int K) { int mod = (int)1e9 + 7; long[] factorial = new long[N + 1]; factorial[0] = 1; for(int i = 1; i <= N; i++) { factorial[i] = factorial[i - 1] * i % mod; } long[][] dp = new long[N + 1][L + 1]; for(int i = K + 1; i <= N; i++) { for(int j = i; j <= L; j++) { if(i == j || i == K + 1) { dp[i][j] = factorial[i]; } else { dp[i][j] = (dp[i - 1][j - 1] * i + dp[i][j - 1] * (i - K)) % mod; } } } return (int)dp[N][L]; } }