CF 439E Devu and Birthday Celebration

题目描述

给你 q q 组询问,对于每组询问给出正整数n,f,求将 n n 拆分成f个正整数相加的形式的方案数,且这 f f 个正整数的最大公约数为1。答案对 109+7 10 9 + 7 取模。

数据范围

1q105,1fn105 1 ≤ q ≤ 10 5 , 1 ≤ f ≤ n ≤ 10 5

分析

PART 1 一个询问

第一眼看这个题目,如果只有1个询问而不是 q q 这个询问,那我们可以用类似D题的做法,我们设sum(x)表示将 n n 拆分成f个正整数相加的形式,且这 f f 个正整数gcd x x 的方案数,那么容易得到sum(x)=(nx1f1)sum(2x)sum(3x)...sum(kx)。至于这个是怎么来的呢?

首先一定有 xn x ∣ n ,那么我们可以把 n n 分为nx块,每块大小为 x x 。(如下图)
pht1
那么问题就相当于是在这nx1个空隙中插入 f1 f − 1 个隔板,这样保证分出来的 f f 个数的gcd一定是至少为 x x ;但是,不一定是所有的的分法得到的gcd都恰好是 x x ,也可能是2x,3x,4x...,kx,因此,我们要除去这些情况,也就是说,减去分出得到的 gcd g c d 2x,3x,4x,...,kx 2 x , 3 x , 4 x , . . . , k x 的情况,这样就得到了上面的式子。

怎么样,这个口糊容斥是不是很完美?好吧我并不会用 f(x) f ( x ) g(x) g ( x ) 的方法去证)

PART 2 Q个询问

然而题目不总是那么友好。题目给出 q q 个询问,更过分的是q居然到了 105 10 5
如果我们每次询问都这样暴力做的话显然会超时,那怎么办呢?考虑到我们计算的时候肯能会有一些冗余的重复计算,这时候我们就会自然而然地想到一个叫做记忆化的东西。首先,我们刚刚的式子中只涉及到了一个状态,那是因为我们在给定 n n f的情况下。现在我们加入 n n f,可以表示出一个三维的状态,设 DP[n][f][g] D P [ n ] [ f ] [ g ] 表示把 n n 分成f份, gcd g c d g g 的方案数,则仍然有DP[n][f][g]=(ng1f1)kgnDP[n][f][kg]。我们要求的就是 DP[n][f][1] D P [ n ] [ f ] [ 1 ] ,则有 DP[n][f][1]=(n1f1)gn,g>1DP[n][f][g] D P [ n ] [ f ] [ 1 ] = ( n − 1 f − 1 ) − ∑ g ∣ n , g > 1 D P [ n ] [ f ] [ g ] 。但是这样的话我们还需要计算这个 DP[n][f][g] D P [ n ] [ f ] [ g ] ,这其实就是前面提到的问题,但是我们又去计算 DP[n][f][g] D P [ n ] [ f ] [ g ] ,这样的话并没有减少冗余的计算。那么我们有什么方法来优化一下呢?我们再看一看上面的图,可以得到这样一个神奇的式子: DP[n][f][g]=DP[n/g][f][1] D P [ n ] [ f ] [ g ] = D P [ n / g ] [ f ] [ 1 ] 。这个可以直接从状态表示的意义上利用最大公约数的一个性质( (a1(a1,a2,..,af),a2(a1,a2,..,af),...,af(a1,a2,..,af))=1 ( a 1 ( a 1 , a 2 , . . , a f ) , a 2 ( a 1 , a 2 , . . , a f ) , . . . , a f ( a 1 , a 2 , . . , a f ) ) = 1 )证明。或者说,(对于上面的图)我们把 n n 分成大小为g ng n g 块之后,为了保证这个 gcd g c d 仍然为 g g ,那么我们就不能让这ng块分出来得到的 gcd g c d 大于 1 1 ,用反证法,若这个分块后gcd大于 1 1 ,那么可知原来的gcd一定大于 g g
这样,我们就可以省去最后一维,直接用DP[n][f]表示把 n n 拆分成f个正整数相加,且 gcd g c d 1 1 的方案数。当然,如果我们预处理出这个DP数组,时间和空间都是不允许的,因此我们只能在线处理。当然了,我们不能直接开这个 DP D P 数组,这时候我们就可以用一个叫 map m a p 的好东西,然后用一个 pair p a i r 表示一下 n n f的状态即可。

参考程序

// Codeforces 439 E
// Round #251 (Div. 2)
#pragma GCC optimize(3)     // 这些优化开关是给map准备的,不知道去了可不可以过,应该去掉也是不会T的
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fwhole-program")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-fstrict-overflow")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-skip-blocks")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("-funsafe-loop-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")
#include <cstdio>
#include <utility>
#include <map>
#define fir first
#define sec second
typedef long long LL;
typedef std::pair<int, int> P;
typedef std::map<P, int> Arr;
const int MAXN = 100005;
const int MOD = 1000000007;

int inv[MAXN], fac[MAXN];
Arr DP;

int pow(int bs, int ex) {   // 快速幂
    int res = 1;
    for (; ex; ex >>= 1, bs = (LL)bs * bs % MOD) if (ex & 1) res = (LL)res * bs % MOD;
    return res;
}
inline void subtrac(int & x, int d) { x = x + MOD - d; while (x >= MOD) x -= MOD; }
inline int C(int n, int r) { return (LL)fac[n] * inv[r] % MOD * inv[n - r] % MOD; }     // 算组合数
void init();
int dp(P);
namespace FastIO {
    template <typename T>
    void read(T & x) {
        x = 0; register char ch = getchar();
        for (; ch < '0' || ch > '9'; ch = getchar());
        for (; ch >= '0' && ch <= '9'; x = (x << 3) + (x << 1) + (ch ^ '0'), ch = getchar());
    }

    template <typename T>
    void write(T x) {
        if (!x) return (void)(putchar('0'));
        register int arr[15], len = 0;
        for (; x; arr[len++] = x % 10, x /= 10);
        while (len) putchar(arr[--len] ^ '0');
    }

    template <typename T>
    inline void writeln(T x) {
        write(x), putchar('\n');
    }
}

int main() {
    init();
    int Q, n, f;
    using FastIO::read;
    read(Q);
    for (int i = 0; i < Q; i++) {
        read(n), read(f);

        FastIO::writeln(dp(P(n, f)));
    }
    return 0;
}

void init() {   //  预处理1e5以内的阶乘极其逆元
    int i, j, k;
    for (fac[0] = i = 1; i <= 100000; i++) fac[i] = (LL)i * fac[i - 1] % MOD;
    inv[100000] = pow(fac[100000], MOD - 2);
    for (i = 99999; i >= 0; --i) inv[i] = (LL)(i + 1) * inv[i + 1] % MOD;
}

// 核心部分就这么一点点
int dp(P now) {
    if (DP.find(now) != DP.end()) return DP[now];
    if (now.sec == 1 && now.fir > 1 || now.sec > now.fir) return 0;     // 这里要特别注意,不合法的状态直接返回0即可,不要再存入map,否则非常耗时间,会TLE,一开始就是因为这个T了半天
    int & res = DP[now];
    res = C(now.fir - 1, now.sec - 1);
    for (int k = 2; (LL)k * k <= now.fir; k++)
        if (!(now.fir % k)) {
            subtrac(res, dp(P(now.fir / k, now.sec)));
            if (k * k != now.fir) subtrac(res, dp(P(k, now.sec)));
        }
    return res;
}

总结

这题如果没有那 q q 个询问,其实并不难。加上询问之后的重点主要在于对冗余计算的处理,再把之前对一个询问的做法变成加上n f f 两维,最后最最重要的地方在于省去g那一维的变形,那个变式若推出来了,整到题也就做完了。

附 更加数学性的做法

其实上述做法为口糊容斥,不过挺好
那么我们应该怎么像之前一样用 f(x) f ( x ) g(x) g ( x ) 的方式去推理呢?

以下内容来自Codeforces
类似于上面的DP方程,令 F(n,f,g) F ( n , f , g ) 表示将正整数 n n 分成f份, gcd g c d g g 的方案数;令P(n,f)为把 n n 分成f个正整数相加的形式的方案数,即 (n1f1) ( n − 1 f − 1 ) 。那么有 F(n,f,1)=P(n,f)gn,g1F(n,f,g) F ( n , f , 1 ) = P ( n , f ) − ∑ g ∣ n , g ≠ 1 F ( n , f , g ) ,又由 F(n,f,g)=F(ng,f,1) F ( n , f , g ) = F ( n g , f , 1 ) ,移项得 P(n,f)=gnF(ng,f,1) P ( n , f ) = ∑ g ∣ n F ( n g , f , 1 ) 。其实这个式子我们通过刚刚对DP转移方程的推理也是可以得到的,不过我们要把这个式子反演过来并不能用我们以前的方法———它不含有那些组合数。事实上呢,这个要用到数论里的一个反演技巧——莫比乌斯反演。
就是这样的

如果两个函数满足:
g(n)=dnf(d),foreveryintegern1 g ( n ) = ∑ d ∣ n f ( d ) , f o r e v e r y i n t e g e r n ≥ 1
那么有 f(n)=dnμ(d)g(nd),foreveryintegern1 f ( n ) = ∑ d ∣ n μ ( d ) g ( n d ) , f o r e v e r y i n t e g e r n ≥ 1

在这里 g(n) g ( n ) P(n,f) P ( n , f ) f(n) f ( n ) F(n,f,1) F ( n , f , 1 )

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值