2024 CCPC 第18届东北四省联赛 The 18th Northeast Collegiate I. Password 【计数DP】

I. Password

传送门:https://codeforces.com/gym/105173

题意

一个长度为 n n n 的序列 a a a,每一项都是 [ 1 , k ] [1, k] [1,k] 的正整数。
如果一个区间 [ i , i + k − 1 ] [i, i + k − 1] [i,i+k1] [ 1 , k ] [1, k] [1,k] 的排列,那么说明这个区间的所有位置都是的。
问有多少种序列满足所有位置都是的。

n ≤ 1 0 5 , k ≤ 1 0 3 n \leq 10^5, k \leq 10^3 n105,k103

思路

我们先定义 d p i dp_i dpi最后一个完整排列的结尾 i i i 的方案数,那么 d p n dp_n dpn 即为答案
列出转移式:
d p i = ∑ j = 1 k d p i − j × g j dp_i = \sum_{j = 1}^{k} dp_{i - j} \times g_j dpi=j=1kdpij×gj
其中 g j g_j gj 即为在一个长度为 k k k 的排列后接上 j j j 个数字,且满足以下两个条件的方案数:

  1. 假设这个长度为 k k k 的排列与新接上的 j j j 个数字形成了序列 b b b,那么 b [ j + 1 , j + k ] b[j + 1, j + k] b[j+1,j+k] 应该为一个长度为 k k k 的排列,这是因为 d p i dp_i dpi 是最后一个完整排列在 i i i 结尾的方案数
  2. 同时, ∀ i ∈ [ 2 , j ] ,      b [ i , i + k − 1 ] \forall i \in [2, j], \;\; b[i, i + k - 1] i[2,j],b[i,i+k1]不能是一个完整的长度为 k k k 的排列,否则不满足 d p i − j dp_{i - j} dpij 是最后一个完整排列的结尾,转移就不符合定义了。

对于上面的条件举例:假如我们现在要在长度为 5 5 5 的排列 { 1 , 2 , 3 , 4 , 5 } \{1, 2, 3, 4, 5\} {1,2,3,4,5} 后接上 j = 3 j = 3 j=3 个数字,那么我们一定要满足 b [ 4 , 8 ] = { 4 , 5 , x , x , x } b[4, 8] = \{4, 5, x, x, x\} b[4,8]={4,5,x,x,x} x x x 为某个数字)一定是一个排列,这样子最后一个完整排列的结尾才能变成 8 8 8
同时, b [ 2 , 6 ] = { 2 , 3 , 4 , 5 , x } b[2, 6] = \{2, 3, 4, 5, x\} b[2,6]={2,3,4,5,x} b [ 3 , 7 ] = { 3 , 4 , 5 , x , x } b[3, 7] = \{3, 4, 5, x, x\} b[3,7]={3,4,5,x,x} 不能是完整的排列,否则前一个完整排列的结尾就不是 5 5 5 而是别的位置了,转移方程就不符合定义

通过这样子定义转移方程和子状态,我们不难发现其满足子集不相交这一特性,我们保证从最后一个完整排列结尾的不同位置转移过来。并且对于每一个长度的序列 a a a,我们只需要考虑其倒数第二个完整排列在哪里,就可以按照 g g g 的算法转移过来

现在问题在于:如何求解 g g g

1 , 2 , 3 , . . . , k 1, 2, 3, ..., k 1,2,3,...,k 来考虑, g j g_j gj 就是要在其之后加上 j j j 个数字,并且要满足前面所提的两个条件。我们先初始化 g j = j ! g_j = j! gj=j!,然后减去不合法的方案数。

考虑枚举最后一个不合法位置,也就是在 b [ k + 1 , k + j ) b[k + 1, k + j) b[k+1,k+j) 中出现了某个完整排列的结尾。
如果最后一个完整排列在 i i i 结尾 ( k + 1 ≤ i < k + j ) (k + 1 \leq i < k + j) (k+1i<k+j),那么我们加上的 j j j 个数字,它的前缀 [ 1 , i ] [1, i] [1,i] 一定是 { 1 , 2 , 3 , . . . , i } \{1, 2, 3, ..., i\} {1,2,3,...,i} 的一个排列,因为 b [ i − k + 1 , i ] b[i - k + 1, i] b[ik+1,i] 是一个完整排列,所以我们必须提供丢失的 [ 1 , i ] [1, i] [1,i] 这些数字来组成完整排列。所以我们加上的 j j j 个数字的这前 i i i 个数字,可以排列:有 i ! i! i! 种方案。对于后面 j − i j - i ji 个数字,它们与前面拼接,不能产生新的完整排列,那么这又回到 g g g 的定义,我们现在最后一个不合法位置在 i i i,后面 [ i + 1 , j − 1 ] [i + 1, j - 1] [i+1,j1] 都没有完整排列,也就是有 g i − j g_{i - j} gij 种方案。

更一般地,如果前面最后一个完整排列不是 { 1 , 2 , 3 , . . . , k } \{1, 2, 3, ..., k\} {1,2,3,...,k} 这种排布,以上思想依然适用。其实 g j g_j gj 就是把前 j j j 个数字取出来排列一下。

因此, g g g 的转移:
g i = i ! − ∑ j = 1 i − 1 j ! × g i − j g_i = i! - \sum_{j = 1}^{i - 1} j! \times g_{i - j} gi=i!j=1i1j!×gij

至此,问题得以在 O ( n k ) O(nk) O(nk) 下解决

#include<bits/stdc++.h>
#define fore(i,l,r)	for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n' 
#define ull unsigned long long

const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;

typedef long long ll;

template<class T>
constexpr T power(T a, ll b){
    T res = 1;
    while(b){
        if(b&1) res = res * a;
        a = a * a;
        b >>= 1;
    }
    return res;
}

constexpr ll mul(ll a,ll b,ll mod){ //快速乘,避免两个long long相乘取模溢出
    ll res = a * b - ll(1.L * a * b / mod) * mod;
    res %= mod;
    if(res < 0) res += mod; //误差
    return res;
}

template<ll P>
struct MLL{
    ll x;
    constexpr MLL() = default;
    constexpr MLL(ll x) : x(norm(x % getMod())) {}

    static ll Mod;
    constexpr static ll getMod(){
       if(P > 0) return P;
       return Mod;
    }

    constexpr static void setMod(int _Mod){
       Mod = _Mod;
    }
    constexpr ll norm(ll x) const{
       if(x < 0){
           x += getMod();
       }
       if(x >= getMod()){
           x -= getMod();
       }
       return x;
    }
    constexpr ll val() const{
       return x;
    }
    explicit constexpr operator ll() const{ 
       return x; //将结构体显示转换为ll类型: ll res = static_cast<ll>(OBJ)
    }
    constexpr MLL operator -() const{ //负号,等价于加上Mod
       MLL res;
       res.x = norm(getMod() - x);
       return res;
    }
    constexpr MLL inv() const{
       assert(x != 0);
       return power(*this, getMod() - 2); //用费马小定理求逆
    }
    constexpr MLL& operator *= (MLL rhs) & { //& 表示“this”指针不能指向一个临时对象或const对象
       x = mul(x, rhs.x, getMod()); //该函数只能被一个左值调用
       return *this;
    }
    constexpr MLL& operator += (MLL rhs) & {
       x = norm(x + rhs.x);
       return *this;
    }
    constexpr MLL& operator -= (MLL rhs) & {
       x = norm(x - rhs.x);
       return *this;
    }
    constexpr MLL& operator /= (MLL rhs) & {
       return *this *= rhs.inv();
    }
    friend constexpr MLL operator * (MLL lhs, MLL rhs){
       MLL res = lhs;
       res *= rhs;
       return res;
    }
    friend constexpr MLL operator + (MLL lhs, MLL rhs){
       MLL res = lhs;
       res += rhs;
       return res;
    }
    friend constexpr MLL operator - (MLL lhs, MLL rhs){
       MLL res = lhs;
       res -= rhs;
       return res;
    }
    friend constexpr MLL operator / (MLL lhs, MLL rhs){
       MLL res = lhs;
       res /= rhs;
       return res;
    }
    friend constexpr std::istream& operator >> (std::istream& is, MLL& a){
       ll v;
       is >> v;
       a = MLL(v);
       return is;
    }
    friend constexpr std::ostream& operator << (std::ostream& os, MLL& a){
       return os << a.val();
    }
    friend constexpr bool operator == (MLL lhs, MLL rhs){
       return lhs.val() == rhs.val();
    }
    friend constexpr bool operator != (MLL lhs, MLL rhs){
       return lhs.val() != rhs.val();
    }
};

const ll mod = 998244353;
using Z = MLL<mod>;

struct Comb {
    int n;
    std::vector<Z> _fac;
    std::vector<Z> _invfac;
    std::vector<Z> _inv;

    Comb() : n{0}, _fac{1}, _invfac{1}, _inv{0} {}
    Comb(int n) : Comb() {
        init(n);
    }

    void init(int m) {
        m = std::min(1ll * m, Z::getMod() - 1);
        if (m <= n) return; //已经处理完了需要的长度
        _fac.resize(m + 1);
        _invfac.resize(m + 1);
        _inv.resize(m + 1);

        for (int i = n + 1; i <= m; i++) {
            _fac[i] = _fac[i - 1] * i;
        }
        _invfac[m] = _fac[m].inv();
        for (int i = m; i > n; i--) { //线性递推逆元和阶乘逆元
            _invfac[i - 1] = _invfac[i] * i;
            _inv[i] = _invfac[i] * _fac[i - 1];
        }
        n = m; //新的长度
    }

    Z fac(int m) {
        if (m > n) init(2 * m);
        return _fac[m];
    }
    Z invfac(int m) {
        if (m > n) init(2 * m);
        return _invfac[m];
    }
    Z inv(int m) {
        if (m > n) init(2 * m);
        return _inv[m];
    }
    Z binom(int n, int m) { //二项式系数
        if (n < m || m < 0) return 0;
        return fac(n) * invfac(m) * invfac(n - m);
    }
} comb;

int main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int n, k;
    std::cin >> n >> k;
    if(n < k){
    	std::cout << "0\n";
    	return 0;
    }
    std::vector<Z> dp(n + 1);
    std::vector<Z> g(k + 1, 0);
	dp[k] = comb.fac(k); //初始化为 k 阶乘
	fore(i, 1, k + 1){
		g[i] = comb.fac(i);
		fore(j, 1, i)	g[i] -= comb.fac(j) * g[i - j];
	}
	
	fore(i, k + 1, n + 1)
		fore(j, 1, k + 1)
			dp[i] += dp[i - j] * g[j];
	
	std::cout << dp[n] << endl;
	
	return 0; 
}
  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值