Leetcode 920. Number of Music Playlists 容斥原理(O(N log L)) or DP

题意

  • 给你n首不同的歌,有一个L长的播放列表,让你这用这些歌,在满足某种条件的前提下,把播放列表填满,问有多少种填法
  • 两个条件是:1. 每首歌至少用1次;2. 如果一个歌放在了第i个位置上,则下一次它最早只能出现在i+k+1的位置上

思路1

  • 在这一节里,我们主要讨论用容斥原理的做法,复杂度会比dp的来的低一些
  • 我们先考虑,如果没有第一个条件,只有第二个条件是怎么样的情况呢?
  • 这样就非常简单,对于第i首歌来说,它有几种选择呢?很显然,因为每k个位置的歌都不相同,所以有如下结论(设选择数为c):
    c ( i ) = n − k , i &gt; k c ( i ) = n − i − 1 , i &lt; = k c(i) = n - k, \quad i &gt; k \\ c(i) = n - i - 1, \quad i &lt;= k c(i)=nk,i>kc(i)=ni1,i<=k
  • 这样我们可以直接连乘,就可以算出方法数
  • 接下来,我们考虑第二个条件。直观来想,n个歌,每个歌至少用一次的方法数 = n个歌不限制用几次 - 至少有一个歌没出现的方法数(它也就是n-1个不限制用几次的方法数 * 是哪个歌没出现的种类数)
  • 当然直接这么算,是有问题的,因为会减重,所以我们应该加上至少两个歌没出现的方法数,然后又加重了,… 这是直观解释,其实就是要用到容斥原理了
  • 最后的计算公式是:
    R = ∑ i = k + 1 N ( − 1 ) n − i C n n − i A i i − k ( i − k ) L − k R = \sum_{i=k+1}^N (-1)^{n-i} C_n^{n-i}A_i^{i-k}(i-k)^{L - k} R=i=k+1N(1)niCnniAiik(ik)Lk
  • 实现的时候几个注意点:(1)组合数里面有除法,我们通过计算逆元的方式保证同余计算下的正确性(2)可以预处理n以下的阶乘加速求解(3)幂运算通过快速幂计算

思路2

  • 我们考虑用dp求解
  • 这题的状态设计思路还是有一些意思的,和一些经典思路还不太一样
  • dp(i, j)表示用j首歌,覆盖前i个位置方案数,这里注意是不是前j首歌,而是n个里面的任意j首,这和一些经典思路还是不太一样的
  • 递推方程:
    d p ( i , j ) = d p ( i − 1 , j − 1 ) × ( n − j + 1 ) + d p ( i − 1 , j ) × max ⁡ ( j − k , 0 ) dp(i, j) = dp(i-1, j-1) \times (n - j + 1) + dp(i-1, j) \times \max(j - k, 0) dp(i,j)=dp(i1,j1)×(nj+1)+dp(i1,j)×max(jk,0)
  • 稍微解释一些,第一项表示第i个位置要放新歌了,可以是除了前j-1首的任意一首,第二项表示,前i-1个位置已经放了i首歌了,第i个位置可以从前j首歌里选,但是要选之前的k首歌不一样的

实现

容斥

class Solution {
    typedef long long ll;
public:
    static const ll MOD = 1e9+7;
    ll extgcd(ll a,ll b,ll& x,ll& y){
        if (b != 0){
            ll ret = extgcd(b,a%b,y,x);
            y -= a / b * x; 
            return ret;
        }
        else{
            x = 1, y = 0;
            return a;
        }
    }
    ll mod_inverse(ll a){
        ll x, y;
        extgcd(a, MOD, x, y);
        return (MOD + x % MOD) % MOD;
    }
    ll fact[101];
    void cal_fact(int n){
        fact[0] = 1;
        for (int i = 1; i <= n; i++){
            fact[i] = (fact[i-1] * ll(i)) % MOD;
        }
    }
    ll mod_comb(ll n, ll k){
        if (n < 0 || k < 0 || n < k)
            return 0;
        ll a1 = fact[n], a2 = fact[k], a3 = fact[n-k];
        return a1 * mod_inverse(a2 * a3 % MOD) % MOD;
    }
    ll mod_pow(ll x, ll n){
        ll res = 1;
        while (n > 0){
            if (n & 1) 
                res = res * x % MOD;
                x = x * x % MOD;
            n >>= 1;
        }
        return res;
    }
    int numMusicPlaylists(int N, int L, int K) {
        cal_fact(N);
        ll res = 0;
        for (int i = N; i > K; i--){
            ll now = N - i & 1 ? -1 : 1;
            now = (now * mod_comb(N, i)) % MOD;
            now = (now * mod_pow(i - K, L - K)) % MOD;
            now = (now * fact[i]) % MOD;
            now = (now * mod_inverse(fact[i-K])) % MOD;
            res = (res + now + MOD) % MOD;
        }
        return res;
    }
};

DP

class Solution {
    typedef long long ll;
public:
    static const ll MOD = 1e9+7;
    int numMusicPlaylists(int N, int L, int K){
        vector<ll> dp(N + 1);
        dp[0] = 1;
        for (int i = 1; i <= L; i++){
            for (int j = min(N, i); j > 0; j--){
                dp[j] = (dp[j-1] * (N - j + 1) + dp[j] * max(j - K, 0)) % MOD;
            }
            dp[0] = 0;
        }
        return dp[N];
    }

};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值