JZOJ2904 【集训队互测 2012】Calc 用倍增的思路转移dp

本文介绍了一种高效算法,用于解决特定数学问题:给定n、A和mod,求所有不同合法序列的值的和并对mod取余。合法序列需满足特定条件,包括长度、元素范围及唯一性。算法采用动态规划和优化技巧,复杂度为O(n²logA)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目大意

一个序列a1,...,an 是合法的,当且仅当:
1. 长度为给定的n
2. a1,...,an[1,A]
3. a1,...,an

一个序列的值定义为它里面所有数的乘积,即a1a2...an
现在的问题是给定n,A,mod,求所有不同合法序列的值的和(两个序列不同当且仅当他们任意一位不一样)。并且答案对一个数mod 取余的结果。

A109
n500
mod109

解题思路

显然我们可以强制是ai>ai1,然后再把最后的答案乘n!
一个很直接的思路就是设F[i][j]表示用了前i个数中的j个的合法序列的和。那么

F[i][j]=F[i1][j1]j+F[i1][j]

这个的复杂度是O(An)的。我们发现这个算法的瓶颈主要在于A太大,所以我们考虑一种把关于A的复杂度降下来的方法,尝试用F[A]转移到F[2A]

首先考虑只用到F[A]的信息时关于[A,2A]的数求积的问题,我们发现

(ai+A)=s0...n1An|s|isai

意思就是对于每个(ai+A)我们都可以选择用乘ai还是A,然后乘ai的集合设为s就会得到这样的转移。我们发现式子最后的isai是我们已经在F[A]中求过的。那么我们设fi表示在[1,A]中选了i个序列的和,即F[A][i]gi表示在[A+1,2A]中选了i个序列的和。由于fi已经求好了,那么我们只用考虑gi怎么求,根据上面的式子,我们可以得出:
gi=j=0iAijfj(Ajij)

其实很好理解这里的fj其实就等于上面式子的isai,而组合数的意义就是对于上面的(ai+A)除了j个选了ai的数之外,其他数的选择可能。直接求组合数有点慢,那么对组合数化简一下:
(Ajij)=k=1ijAj+1kk=i1k=j(Ak)(ij)!

mi=i1k=0(Ai),rmi=(mi)1,那么(Ajij)=(ij)!1mirmj
最后gi的表达式就是
gi=mij=0iAijfjrmj(ij)!1

fg组合一下,F[2A]的转移就为:
F[2A][i]=j=0ifjgij

边界比较麻烦,要稍微处理一下。然后像快速乘一样,我们就可以做O(log)次得到F[A],而每次的复杂度是n2,总的复杂度就是O(n2logA)

程序

//YxuanwKeith
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;

const int MAXN = 505;

struct Dp {
    int dp[MAXN];
};

int Mo, A, N, M[MAXN], InvM[MAXN], Pow[MAXN], Inv[MAXN], Fac[MAXN];

int Power(int x, int y) {
    int Ans = 1;
    for (; y; y >>= 1, x = 1ll * x * x % Mo)
        if (y & 1) Ans = 1ll * Ans * x % Mo;
    return Ans;
}

Dp Double(Dp Now, int A) {
    Dp Ans;
    memset(Ans.dp, 0, sizeof Ans.dp);
    static int G[MAXN];
    M[0] = Pow[0] = InvM[0] = 1;
    for (int i = 0; i < N; i ++) M[i + 1] = 1ll * M[i] * (A - i) % Mo;
    for (int i = 1; i <= N; i ++) InvM[i] = Power(M[i], Mo - 2);
    for (int i = 1; i <= N; i ++) Pow[i] = 1ll * Pow[i - 1] * A % Mo;
    G[0] = 1;
    for (int i = 1; i <= N; i ++) {
        int Sum = 0;
        for (int j = 0; j <= i; j ++) {
            Sum = (Sum + 1ll * Pow[i - j] * Now.dp[j] % Mo * InvM[j] % Mo * Inv[i - j] % Mo) % Mo;
        }
        G[i] = 1ll * M[i] * Sum % Mo;
    }
    for (int i = 0; i <= N; i ++) {
        Ans.dp[i] = 0;
        for (int j = 0; j <= i; j ++)
            Ans.dp[i] = (Ans.dp[i] + 1ll * Now.dp[j] * G[i - j] % Mo) % Mo;
    }       
    return Ans;
}

Dp Single(Dp Now, int A) {
    Dp Ans;
    Ans.dp[0] = 1;
    for (int j = 1; j <= N; j ++)
        Ans.dp[j] = (1ll *  Now.dp[j - 1] * A % Mo + Now.dp[j]) % Mo;
    return Ans;
}

Dp Solve(int y) {
    Dp Now;
    if (!y) {
        memset(Now.dp, 0, sizeof Now.dp);
        Now.dp[0] = 1;
        return Now;
    }
    Now = Double(Solve(y / 2), y / 2);
    if (y & 1) Now = Single(Now, y);
    return Now;
}

void Prepare() {
    Fac[0] = Inv[0] = 1;
    for (int i = 1; i <= N; i ++) Fac[i] = 1ll * Fac[i - 1] * i % Mo;
    for (int i = 1; i <= N; i ++) Inv[i] = Power(Fac[i], Mo - 2);
}

int main() {
    scanf("%d%d%d", &A, &N, &Mo);
    Prepare();
    printf("%d\n", 1ll * Solve(A).dp[N] * Fac[N] % Mo);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值