leetcode
leetcode 801 ~ 850
播放列表的数量

播放列表的数量

难度:

标签:

题目描述

代码结果

运行时间: 33 ms, 内存: 16.0 MB


/*
 思路:
 使用Java Stream对动态规划进行实现,但由于动态规划的特性,Stream实现并不是最佳选择,但可以尝试利用Stream来简化部分代码。
*/
import java.util.*;
import java.util.stream.*;

public class Solution {
    public int numMusicPlaylists(int n, int goal, int k) {
        int MOD = 1000000007;
        long[][] dp = new long[goal + 1][n + 1];
        dp[0][0] = 1;
        IntStream.rangeClosed(1, goal).forEach(i -> {
            IntStream.rangeClosed(1, Math.min(i, n)).forEach(j -> {
                dp[i][j] = dp[i - 1][j - 1] * (n - j + 1) % MOD;
                if (j > k) {
                    dp[i][j] = (dp[i][j] + dp[i - 1][j] * (j - k) % MOD) % MOD;
                }
            });
        });
        return (int) dp[goal][n];
    }
}

解释

方法:

题解使用动态规划的方法,考虑dp数组存储在选择了P首歌曲,已经播放了S首不同的歌曲的情况下的播放列表数量。初始化dp数组为长度L-N+1,其中dp[i]表示选择了i+S首歌的所有可能的播放列表数量。然后通过两层循环,外层循环从2至N-K+1,内层从1至L-N+1,使用状态转移更新dp数组,最后将计算的结果乘以N的阶乘,以包括所有可能的不同歌曲的排列组合,最后结果取模10^9+7。

时间复杂度:

O((N-K)*(L-N) + N)

空间复杂度:

O(L-N+1)

代码细节讲解

🦆
如何正确理解和应用这里提到的动态规划数组dp[i]的定义?
动态规划数组dp[i]的定义是关键于解决问题的核心。在这个问题中,dp[i]代表选择了i+N首歌曲的所有可能的播放列表数量。这里的i表示超过N的额外歌曲数,所以i+N就是目前列表中的总歌曲数。正确理解dp[i]的定义帮助我们构建状态转移方程,将问题从一个规模转移到更大的规模,最终解决整个问题。在应用中,我们从最基本的情况开始,逐步通过已知的状态推算出未知的状态。
🦆
题解中提到,初始化dp数组所有元素为1,这个初始化方法适用于所有情况吗,还是有特定的前提条件?
初始化dp数组所有元素为1是基于特定的前提条件,即至少有一种方式构建播放列表(即选择N首不同的歌曲,每首歌恰好播放一次)。因此,这种初始化适用于当L(播放列表长度)大于等于N(不同歌曲的数量)的情况。如果L小于N,这样的初始化就不再适用,因为不可能在播放列表长度小于歌曲总数的情况下满足每首歌至少播放一次的条件。
🦆
状态转移方程是如何从问题的限制条件(每首歌至少播放一次,且需间隔k首)中得出的?
状态转移方程的设计考虑了每首歌至少播放一次和每首新歌之间至少有K首其他歌曲的间隔规则。动态规划的每一步代表在当前已选歌曲的基础上,增加新的不同的歌曲到播放列表中。每增加一首新歌,都基于前一个状态(较少的歌曲数),考虑新增歌曲的各种放置位置和可能性,从而更新当前的状态。这种递推关系直接反映了每首新歌引入的变化以及如何从已有的播放列表状态转移到新的状态。
🦆
dp数组和最终答案的计算中,为何需要将结果乘以N的阶乘?
将dp数组的最终结果乘以N的阶乘是为了考虑不同歌曲的所有可能排列。dp数组计算的是在满足特定条件下的播放列表数目,不考虑歌曲的具体排列顺序。N的阶乘是用来计算N首不同歌曲的所有可能顺序,这样我们就能得到所有合法的播放列表,确保每种歌曲的排列都被计算在内。这是因为dp数组在计算时仅考虑了歌曲数目的增加,而没有具体到每一首歌的排列顺序。

相关问题