组合数学与数论函数专题

组合数学专题

专题简介

本专题包含了一些组合数学中常见的套路和方法,如拉格朗日插值,动态规划,容斥原理,狄利克雷卷积,线性筛,杜教筛 等等.

目录

  1. 2018 四川省赛GRISAIA (数论分块)
  2. HDU 6428 Calculate (狄利克雷卷积,线性筛)
  3. BZOJ4559 成绩比较 (动态规划,拉格朗日插值)
  4. BZOJ 2633 已经没有什么好害怕的了 (容斥森林,动态规划)
  5. BZOJ 3930 选数 (容斥原理,动态规划)
  6. 徐州网络赛 D Easy Math (积性函数,线性筛)
  7. NWERC2015 Debugging (动态规划,数论分块)

四川省赛 GRISAIA

问题描述

∑ i = 1 n ∑ j = 1 i n % ( i ∗ j ) \sum_{i=1}^{n}\sum_{j=1}^{i}{n \% (i * j)} i=1nj=1in%(ij),其中 1 ≤ n ≤ 1 0 11 1 \le n \le 10^{11} 1n1011

引入的一些性质

⌈ n x y ⌉ = ⌈ ⌈ n x ⌉ y ⌉ \lceil \frac{n}{xy} \rceil = \lceil \frac{\lceil \frac{n}{x} \rceil}{y} \rceil xyn=yxn
⌊ n x y ⌋ = ⌊ ⌊ n x ⌋ y ⌋ \lfloor \frac{n}{xy} \rfloor = \lfloor \frac{\lfloor \frac{n}{x} \rfloor}{y} \rfloor xyn=yxn

解法

先对原问题进行变形:

∑ i = 1 n ∑ j = 1 i n % ( i ∗ j ) \sum_{i=1}^{n}\sum_{j=1}^{i}{n \% (i * j)} i=1nj=1in%(ij)

= ∑ i = 1 n ∑ j = 1 i ( n − ⌊ ( n i j ) ⌋ ∗ i j ) = \sum_{i=1}^{n}\sum_{j=1}^{i}{(n - \lfloor (\frac{n}{ij}) \rfloor * ij)} =i=1nj=1i(n(ijn)ij)

只需要求:

∑ i = 1 n ∑ j = 1 i ⌊ ( n i j ) ⌋ ∗ i j \sum_{i=1}^{n}\sum_{j=1}^{i}{\lfloor (\frac{n}{ij}) \rfloor * ij} i=1nj=1i(ijn)ij

= 1 2 ∑ i = 1 n ∑ j = 1 n ( ⌊ ( n i j ) ⌋ ∗ i j ) + 1 2 ∑ i = 1 n ⌊ n i 2 ⌋ ∗ i 2 = \frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}{(\lfloor (\frac{n}{ij}) \rfloor * ij}) + \frac{1}{2}\sum_{i=1}^{n}{\lfloor \frac{n}{i^2} \rfloor * i^2} =21i=1nj=1n((ijn)ij)+21i=1ni2ni2

只需要求:

∑ i = 1 n ∑ j = 1 n ( ⌊ ( n i j ) ⌋ ∗ i j ) \sum_{i=1}^{n}\sum_{j=1}^{n}{(\lfloor (\frac{n}{ij}) \rfloor * ij}) i=1nj=1n((ijn)ij)

这里我们就要使用引入的性质了:

= ∑ i = 1 n ∑ j = 1 n ( i ∗ ⌊ ⌊ n i ⌋ j ⌋ ∗ j ) = \sum_{i = 1}^{n}\sum_{j = 1}^{n}(i * \lfloor \frac{\lfloor \frac{n}{i} \rfloor}{j} \rfloor * j) =i=1nj=1n(ijinj)

= ∑ i = 1 n i ∑ j = 1 n ( ⌊ ⌊ n i ⌋ j ⌋ ∗ j ) = \sum_{i = 1}^{n}{i}\sum_{j = 1}^{n}( \lfloor \frac{\lfloor \frac{n}{i} \rfloor}{j} \rfloor * j) =i=1nij=1n(jinj)

记录

f ( n ) = ∑ i = 1 n ⌊ n i ⌋ ∗ i f(n) = \sum_{i = 1}^{n}{\lfloor \frac{n}{i} \rfloor} * i f(n)=i=1nini

显然

∑ i = 1 n ∑ j = 1 n ( i ∗ ⌊ ⌊ n i ⌋ j ⌋ ∗ j ) = ∑ i = 1 n i ∗ f ( ⌊ n i ⌋ ) \sum_{i = 1}^{n}\sum_{j = 1}^{n}(i * \lfloor \frac{\lfloor \frac{n}{i} \rfloor}{j} \rfloor * j) = \sum_{i = 1}^{n}{i*f(\lfloor \frac{n}{i} \rfloor)} i=1nj=1n(ijinj)=i=1nif(in)

显然 f ( n ) f(n) f(n)的计算可以数论分块,然后上面部分的计算也可以分块.


HDU6428 [Calculate]

知识清单

  1. 欧拉函数
  2. 莫比乌斯函数
  3. 莫比乌斯反演
  4. 狄利克雷卷积
  5. 线性筛

题解

要求计算 ∑ i = 1 A ∑ j = 1 b ∑ k = 1 C ( ϕ ( g c d ( i , j 2 , k 3 ) ) ) \sum_{i = 1}^{A}\sum_{j = 1}^{b}\sum_{k = 1}^{C}(\phi(gcd(i,j^2,k^3))) i=1Aj=1bk=1C(ϕ(gcd(i,j2,k3)))
我们先对式子进行变换,比较套路一步是改为枚举 g c d gcd gcd,然后统计 g c d gcd gcd出现的次数,即:

= ∑ d = 1 ϕ ( d ) ( ∑ i = 1 A ∑ j = 1 b ∑ k = 1 C [ g c d ( i , j 2 , k 3 ) = d ] ) = \sum_{d = 1}{\phi{(d)}}{(\sum_{i = 1}^{A}\sum_{j = 1}^{b}\sum_{k = 1}^{C}[gcd(i,j^2,k^3)=d])} =d=1ϕ(d)(i=1Aj=1bk=1C[gcd(i,j2,k3)=d])

对上述式子中的 ∑ i = 1 A ∑ j = 1 b ∑ k = 1 C [ g c d ( i , j 2 , k 3 ) = d ] \sum_{i = 1}^{A}\sum_{j = 1}^{b}\sum_{k = 1}^{C}[gcd(i,j^2,k^3)=d] i=1Aj=1bk=1C[gcd(i,j2,k3)=d]再进行莫比乌斯反演:

g ( d ) = ∑ i = 1 A ∑ j = 1 b ∑ k = 1 C [ g c d ( i , j 2 , k 3 ) = d ] g(d) = \sum_{i = 1}^{A}\sum_{j = 1}^{b}\sum_{k = 1}^{C}[gcd(i,j^2,k^3)=d] g(d)=i=1Aj=1bk=1C[gcd(i,j2,k3)=d]

并设 G [ n ] = ∑ n ∣ d g ( d ) G[n] = \sum_{n|d}{g(d)} G[n]=ndg(d)

那么显然有 g ( n ) = ∑ n ∣ d μ ( d n ) G ( d ) g(n) = \sum_{n|d}\mu(\frac{d}{n})G(d) g(n)=ndμ(nd)G(d)

那么要求的式子可以写成

= ∑ d = 1 ϕ ( d ) g ( d ) = ∑ d = 1 ϕ ( d ) ∑ d ∣ t μ ( t d ) G ( t ) = \sum_{d = 1}{\phi{(d)}} g(d) = \sum_{d = 1}{\phi{(d)}}\sum_{d|t}\mu(\frac{t}{d})G(t) =d=1ϕ(d)g(d)=d=1ϕ(d)dtμ(dt)G(t)

由于 ϕ ( x ) \phi(x) ϕ(x) μ ( x ) \mu(x) μ(x)都是积性函数,故将 G [ t ] G[t] G[t] ϕ ( d ) \phi(d) ϕ(d)的位置做交换,使之形成狄利克雷卷积.

= ∑ t = 1 G ( t ) ∑ d ∣ t μ ( t d ) ϕ ( d ) = \sum_{t=1}G(t)\sum_{d|t}\mu(\frac{t}{d})\phi(d) =t=1G(t)dtμ(dt)ϕ(d)

在这里,记 F ( x ) = ∑ d ∣ t μ ( t d ) ϕ ( d ) F(x) = \sum_{d|t}\mu(\frac{t}{d})\phi(d) F(x)=dtμ(dt)ϕ(d)

F ( x ) F(x) F(x)是积性函数的狄利克雷卷积,因此 F ( x ) F(x) F(x)也是积性函数,可以用线性筛法求得.

线性筛所能用到的信息:

F ( p ) = p − 2 F(p) = p-2 F(p)=p2

F ( p x ) = F ( p x − 1 ) ∗ p F(p^x) = F(p^{x-1})*p F(px)=F(px1)p

F ( p 2 ) = p 2 − 2 ∗ p + 1 F(p^2) = p^2-2*p+1 F(p2)=p22p+1

有关 G ( n ) G(n) G(n)的计算:

n = ∏ p i a i n = \prod{p_i^{a_i}} n=piai

则定义

g 2 ( n ) = ∏ p i ⌈ a i 2 ⌉ g_2(n) = \prod{p_i^{\lceil \frac{a_i}{2} \rceil}} g2(n)=pi2ai

g 3 ( n ) = ∏ p i ⌈ a i 3 ⌉ g_3(n) = \prod{p_i^{\lceil \frac{a_i}{3} \rceil}} g3(n)=pi3ai

那么 G ( n ) = [ A n ] [ B g 2 ( n ) ] [ C g 3 ( n ) ] G(n) = [\frac{A}{n}][\frac{B}{g_2(n)}][\frac{C}{g_3(n)}] G(n)=[nA][g2(n)B][g3(n)C]

这样的话式子 = ∑ t = 1 G ( t ) F ( t ) = \sum_{t=1}G(t)F(t) =t=1G(t)F(t)中的 G ( t ) G(t) G(t) F ( t ) F(t) F(t)均可在 O ( n ) O(n) O(n)线性时间复杂度内预处理出来.

另外:这道题卡常数,真的恶心.

代码

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const ll MOD = 1<<30;
const int maxn = 10000000;
int zhi[maxn+10],prime[maxn+10],pcnt,F[maxn+10];
int low[maxn+10],lowm[maxn+10],g2[maxn+10],g3[maxn+10];
void sieve(){
    zhi[1] = 1;
    low[1] = F[1] = g3[1] = g2[1] = 1;
    lowm[1] = 0;
    for(int i = 2;i <= maxn;++i){
        if(!zhi[i]) {
            F[i] = i-2,prime[pcnt++] = i;
            low[i] = i;lowm[i] = 1;
            g3[i] = g2[i] = i;
        }
        for(int j = 0;j < pcnt && prime[j]*i <= maxn;++j){
            zhi[i*prime[j]] = 1;
            if(i % prime[j] == 0){
                low[i*prime[j]] = low[i] * prime[j];
                lowm[i*prime[j]] = lowm[i]+1;
                if(i == low[i]){
                    if(i == prime[j]) F[i*prime[j]] = prime[j]*prime[j]-2*prime[j]+1;
                    else F[i*prime[j]] = F[i] * prime[j];
                }
                else
                {
                    F[i*prime[j]] = F[i/low[i]]*F[low[i]*prime[j]];
                }
                g2[i*prime[j]] = g2[i];
                g3[i*prime[j]] = g3[i];
                if(lowm[i*prime[j]] % 2 == 1) g2[i*prime[j]] *= prime[j];
                if(lowm[i*prime[j]] % 3 == 1) g3[i*prime[j]] *= prime[j];
                break;
            }
            else
            {
                F[i*prime[j]] = F[i] * F[prime[j]];
                low[i*prime[j]] = prime[j];
                lowm[i*prime[j]] = 1;
                g2[i*prime[j]] = g2[i] * prime[j];
                g3[i*prime[j]] = g3[i] * prime[j];
            }
        }
    }
}

int T ,A,B,C;
int main(){
    sieve();
    cin>>T;
    while(T--){
        scanf("%d%d%d",&A,&B,&C);
        ll ans = 0;
        for(int n = 1;n <= max(A,max(B,C));++n){
            ans = (ans + (A/n)*(B/g2[n])%MOD*(C/g3[n])%MOD*F[n]%MOD) % MOD;
        }
        cout<<ans<<endl;
    }
    return 0;
}

BZOJ4559 [成绩比较]

知识清单

  1. 动态规划
  2. 拉格朗日插值

题解

分两部分来解决:

1.先不考虑具体分数,只考虑满足相对排名计算得到方案数.

定义 d p [ i ] [ j ] dp[i][j] dp[i][j]表示仅考虑前 i i i节课,B神恰好碾压 j j j个人.
转移方法:
显然 d p [ i ] [ j ] dp[i][j] dp[i][j] d p [ i ] [ j + k ] dp[i][j+k] dp[i][j+k]转移过来.
具体转移方法:
考虑第 i i i门课,从 j + k j+k j+k中选出 k k k个,他们的分数大于B神,剩下 j j j个放在B神的下面,表示继续被碾压.方案总数 C k + j k C_{k+j}^{k} Ck+jk.
剩下的还有 N − 1 − ( j + k ) N-1-(j+k) N1(j+k)名同学,这些同学中取出 R a n k [ i ] − k Rank[i]-k Rank[i]k名同学放在B神上面,填满 R a n k [ i ] Rank[i] Rank[i].方案总数 C N − 1 − j − k R a n k [ i ] − k C_{N-1-j-k}^{Rank[i]-k} CN1jkRank[i]k.
因此转移方程:
d p [ i ] [ j ] = ∑ k = 0 k + j ≤ N − 1 d p [ i ] [ j + k ] C k + j k C N − 1 − j − k R a n k [ i ] − k dp[i][j] = \sum_{k=0}^{k+j≤N-1}{dp[i][j+k]C_{k+j}^{k}}C_{N-1-j-k}^{Rank[i]-k} dp[i][j]=k=0k+jN1dp[i][j+k]Ck+jkCN1jkRank[i]k
dp初始化:
d p [ 0 ] [ N − 1 ] = 1 dp[0][N-1] = 1 dp[0][N1]=1

该部分具体实现代码:

// dp[i][j]表示仅考虑前i门课程,其中有j名同学被B神碾压,的相对排名的方案数.  
dp[0][N-1] = 1;
for(int i = 1;i <= M;++i){
    for(int j = N-1;j >= 0;--j){
        for(int k = 0;k+j<=N-1;++k){
            ll res = C[j+k][k] * C[N-1-(j+k)][R[i]-1-k] % MOD;
            res = res * dp[i-1][j+k] % MOD;
            dp[i][j] = (dp[i][j] + res) % MOD;
        }
    }
}
2.求出满足题目给出的排名条件的分数具体分配方案数.

对于第 i i i门课,设B神的排名为 R a n k [ i ] Rank[i] Rank[i],则该门课产生的方案数就是枚举该门课B神的分数得到的方案数之和:
L R a n k [ i ] [ U x ] = ∑ t = 1 U x t N − R a n k [ i ] ( U x − t ) R a n k [ i ] − 1 L_{Rank[i]}[U_x] = \sum_{t=1}^{Ux}{t^{N-Rank[i]}(U_x-t)^{Rank[i]-1}} LRank[i][Ux]=t=1UxtNRank[i](Uxt)Rank[i]1
这个式子是关于 U x U_x Ux N N N次多项式,而 U x Ux Ux最大则可能达到 1 e 9 1e9 1e9,显然不可能直接来求,我们考虑拉格朗日插值.


L a g r a n g e Lagrange Lagrange插值简介

对于一个 n n n次的多项式,只需要 n + 1 n+1 n+1个点即可唯一确定,而用 n + 1 n+1 n+1个点恢复这个 n n n次多项式的方法就是 L a g r a n g e Lagrange Lagrange插值法.

( x i , y i ) (x_i,y_i) (xi,yi)为我们已经获得的 N + 1 N+1 N+1个点.

定义 f i [ x ] = ∏ i ! = j x − x j x i − x j f_i[x] = \prod_{i!=j}{\frac{x-x_j}{x_i-x_j}} fi[x]=i!=jxixjxxj

那么 L [ x ] = ∑ i = 1 N + 1 y i f i [ x ] L[x] = \sum_{i=1}^{N+1}{y_if_i[x]} L[x]=i=1N+1yifi[x]


如果这道题用 L a g r a n g e Lagrange Lagrange插值求得了 L [ x ] L[x] L[x],那么第 i i i门课的答案即是 L R a n k [ i ] [ U x ] L_{Rank[i]}[U_x] LRank[i][Ux].

//拉格朗日实现具体代码
ll Lagrange(int id){
    //拉格朗日插值
    for(int xi = 1;xi <= N+1;++xi){
        x[xi] = xi;
        y[xi] = 0;
        for(int i = 1;i <= xi;++i){
            y[xi] = (y[xi] + (mod_pow(i,N-R[id])*mod_pow(xi-i,R[id]-1)%MOD)) % MOD;
        }
    }

    ll ans = 0;
    for(int xi = 1;xi <= N+1;++xi){
        ll res = y[xi];
        for(int xj = 1;xj <= N+1;++xj){
            if(xj == xi) continue;
            res = res * (U[id] - xj + MOD) % MOD;
            res = res * div(xi - xj + MOD) % MOD;
        }
        ans = (ans + res) % MOD;
    }
    return ans;
}

综合起来,这道题的答案就是 a n s = d p [ M ] [ K ] ∗ ∏ i = 1 M L R a n k [ i ] [ U x ] ans = dp[M][K]*\prod_{i=1}^{M}{L_{Rank[i]}[U_x]} ans=dp[M][K]i=1MLRank[i][Ux]

###实现代码

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <stack>

using namespace std;

#define pr(x) cout<<#x<<":"<<x<<endl
typedef long long ll;
int N,M,K;
const ll MOD = 1e9+7;
ll mod_pow(ll x,ll n){
    if(n == 0) return 1;
    ll res = 1;
    while(n){
        if(n&1) res = res * x %MOD;
        x = x * x % MOD;
        n >>= 1;
    }
    return res;
}
const int maxn = 107;
ll R[maxn],U[maxn],dp[maxn][maxn];
ll C[maxn][maxn];
ll div(ll x){
    return mod_pow(x,MOD-2);
}
void initC(){
    C[0][0] = 1;
    for(int i = 1;i < maxn;++i){
        C[i][0] = 1;
        for(int j = 1;j <= i;++j){
            C[i][j] = (C[i-1][j] + C[i-1][j-1])%MOD;
        }
    }
}
ll x[maxn],y[maxn];
ll Lagrange(int id){
    //拉格朗日插值
    for(int xi = 1;xi <= N+1;++xi){
        x[xi] = xi;
        y[xi] = 0;
        for(int i = 1;i <= xi;++i){
            y[xi] = (y[xi] + (mod_pow(i,N-R[id])*mod_pow(xi-i,R[id]-1)%MOD)) % MOD;
        }
    }

    ll ans = 0;
    for(int xi = 1;xi <= N+1;++xi){
        ll res = y[xi];
        for(int xj = 1;xj <= N+1;++xj){
            if(xj == xi) continue;
            res = res * (U[id] - xj + MOD) % MOD;
            res = res * div(xi - xj + MOD) % MOD;
        }
        ans = (ans + res) % MOD;
    }
    return ans;
}
int main(){
    initC();
    ios::sync_with_stdio(false);
    cin>>N>>M>>K;
    for(int i = 1;i <= M;++i) cin>>U[i];
    for(int i = 1;i <= M;++i) cin>>R[i];

    // dp[i][j]表示仅考虑前i门课程,其中有j名同学被B神碾压,的相对排名的方案数.
    
    dp[0][N-1] = 1;
    for(int i = 1;i <= M;++i){
        for(int j = N-1;j >= 0;--j){
            for(int k = 0;k+j<=N-1;++k){
                ll res = C[j+k][k] * C[N-1-(j+k)][R[i]-1-k] % MOD;
                res = res * dp[i-1][j+k] % MOD;
                dp[i][j] = (dp[i][j] + res) % MOD;
            }
        }
    }

    ll ans = dp[M][K];

    for(int i = 1;i <= M;++i){
        ans = ans * Lagrange(i) % MOD;
    }
    cout<<ans<<endl;
    return 0;
}

BZOJ2633 [已经没有什么好害怕的了]

知识清单

  1. 容斥原理
  2. 动态规划

题意

给出 n n n个数 a i a_i ai n n n个数 b i b_i bi,将 a i a_i ai b i b_i bi进行匹配,使得匹配中 a i &gt; b i a_i&gt;b_i ai>bi的对数比 a i &lt; b i a_i&lt;b_i ai<bi的匹配数大 k k k组.

题解

首先将 a , b a,b a,b数组排序,然后对于每个 i i i求出满足 b j &lt; a i ( 1 ≤ j ≤ n ) b_j &lt; a_i (1≤j≤n) bj<ai(1jn) j j j的个数,并记为 c n t [ i ] cnt[i] cnt[i].

定义 f [ i ] [ j ] f[i][j] f[i][j]表示仅考虑前 i i i a a a中,满足有 j j j对匹配使得 a u &gt; b v a_u &gt; b_v au>bv成立的方案数.(显然 1 ≤ j ≤ i 1≤j≤i 1ji) (注意这个定义中不要求剩下的 a a a b b b进行匹配.)

那么我们可以得到转移方程:
f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − 1 ] ∗ ( c n t [ i ] − ( j − 1 ) ) f[i][j] = f[i-1][j] + f[i-1][j-1]*(cnt[i]-(j-1)) f[i][j]=f[i1][j]+f[i1][j1](cnt[i](j1))

看起来似乎 f [ n ] [ j ] ∗ ( n − j ) ! f[n][j]*(n-j)! f[n][j](nj)!就是我们要找的答案?

但显然并不是,因为 ( n − j ) ! (n-j)! (nj)!表示的剩下未匹配的元素进行随机搭配的时候,有时候也会搭配出满足 a u &gt; b v a_u &gt; b_v au>bv的匹配,像这种就是不合法的,因此我们还要进行去重操作.

g [ i ] g[i] g[i]表示在 n n n个匹配中,恰好有 i i i个匹配满足 a u &gt; b v a_u &gt; b_v au>bv的方案数.显然这回 g [ ( n + k ) / 2 ] g[(n+k)/2] g[(n+k)/2]就是我们要的答案.

容易想到转移方程:
g [ i ] = f [ n ] [ i ] ∗ ( n − i ) ! − g [ i + 1 ] ∗ C i + 1 i − g [ i + 2 ] ∗ C I + 2 i − . . . − g [ n ] ∗ C n i g[i]=f[n][i]*(n-i)!-g[i+1]*C^{i}_{i+1}-g[i+2]*C^{i}_{I+2}-...-g[n]*C^{i}_{n} g[i]=f[n][i](ni)!g[i+1]Ci+1ig[i+2]CI+2i...g[n]Cni

解释一下为什么要乘以 C j i C^{i}_{j} Cji这个系数:因为 f [ n ] [ i ] ∗ ( n − i ) ! f[n][i]*(n-i)! f[n][i](ni)!产生的满足有 j j j a u &gt; b v a_u &gt; b_v au>bv的匹配数的方案数可以由固定 i i i个匹配数来得到(剩下的 j − i j-i ji个匹配由 ( j − i ) ! (j-i)! (ji)!部分负责).而固定 i i i个匹配数的方案数刚好就是 C j i C^{i}_{j} Cji

得到这个式子倒着转移就可以了.

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <stack>

using namespace std;
const ll MOD = 1e9+9;
int n,k;
const int maxn = 2007;
ll f[maxn][maxn],g[maxn];
ll a[maxn],b[maxn];
ll fac[maxn];
ll C[maxn][maxn];
ll cnt[maxn];
int main(){
    C[0][0] = fac[0] = 1;
    for(int i = 1;i <= 2000;++i) {
        fac[i] = fac[i-1] * i % MOD;
        C[i][0] = 1;
    }
    for(int i = 1;i <= 2000;++i) {
        for(int j = 1;j <= i;++j){
            C[i][j] = (C[i-1][j] + C[i-1][j-1]) % MOD;
        }
    }
    cin>>n>>k;
    if((n+k)%2 != 0) return puts("0");
    for(int i = 1;i <= n;++i) scanf("%lld",&a[i]);
    for(int i = 1;i <= n;++i) scanf("%lld",&b[i]);
    sort(a+1,a+1+n);
    sort(b+1,b+1+n);
    for(int i = 1;i <= n;++i){
        cnt[i] = lower_bound(b+1,b+1+n,a[i])-b-1;
    }
    f[0][0] = 1;
    for(int i = 1;i <= n;++i){
        f[i][0] = 1;
        for(int j = 1;j <= i;++j){
            f[i][j] = f[i-1][j] + (f[i-1][j-1]*max(cnt[i]-j+1,0LL)%MOD);
            f[i][j] %= MOD;
        }
    }
    for(int i = n;i >= (n+k)/2;--i){
        g[i] = f[n][i]*fac[n-i]%MOD;
        for(int j = i+1;j <= n;++j){
            g[i] = (g[i] + MOD - (C[j][i] * g[j] % MOD)) % MOD;
        }
    }
    cout<<g[(n+k)/2]<<endl;
    return 0;
}

BZOJ3930 [选数]

知识清单

  1. 容斥原理 (或莫比乌斯反演)

题解

题目给的一个很关键的信息就是 H − L ≤ 1 e 5 H-L ≤ 1e5 HL1e5.这意味着如果在区间 [ L , H ] [L,H] [L,H]中选出 N N N个不完全相同的整数,那么他们的 G C D ≤ H − L ≤ 1 e 5 GCD≤H-L≤1e5 GCDHL1e5
证明: G C D ( x , y ) = G C D ( x − y , y ) &lt; = x − y GCD(x,y) = GCD(x-y,y) &lt;= x-y GCD(x,y)=GCD(xy,y)<=xy

如果将 [ L , H ] [L,H] [L,H]中的各个元素除以 K K K,就相当于在 ( L − 1 K , H K ] (\frac{L-1}{K},\frac{H}{K}] (KL1,KH]中找 N N N个不完全相同的数使得其 G C D = 1 GCD = 1 GCD=1

F [ x ] F[x] F[x]表示满足在 ( L − 1 K , H K ] (\frac{L-1}{K},\frac{H}{K}] (KL1,KH]中找 N N N个不完全相同的元素,使得其 G C D = 1 GCD=1 GCD=1的方案数.

显然 F [ x ] = ( [ H K x ] − [ L − 1 K x ] ) N − ( [ H K x ] − [ L − 1 K x ] ) − F [ 2 ∗ x ] − F [ 3 ∗ x ] − . . . . F[x] = ([\frac{H}{Kx}]-[\frac{L-1}{Kx}])^N - ([\frac{H}{Kx}]-[\frac{L-1}{Kx}]) - F[2*x] - F[3*x] -.... F[x]=([KxH][KxL1])N([KxH][KxL1])F[2x]F[3x]....

遇见这种递推式子直接倒着进行转移就可以了.
注意,如果 K K K 存在于区间 [ L , H ] [L,H] [L,H]之间,那么 a n s + + ans++ ans++,因为这种属于 N N N个数字全部相等的情况,我们的算法是没有考虑进去的.

另外,
这题实际上可以使用莫比乌斯反演,而且莫比乌斯反演形式特别直接,但是复杂度不对.

重新定义 F [ x ] F[x] F[x]为在 [ L , H ] [L,H] [L,H]中找 N N N个数使得其 G C D = X GCD=X GCD=X的方案数
我们可以得到
∑ n ∣ d F [ d ] = ( [ H n ] − [ L − 1 n ] ) N \sum_{n|d}{F[d]} = ([\frac{H}{n}]-[\frac{L-1}{n}])^N ndF[d]=([nH][nL1])N
反演得到,
F [ n ] = ∑ n ∣ d μ ( d n ) ( [ H d ] − [ L − 1 d ] ) N F[n] = \sum_{n|d}{\mu(\frac{d}{n})([\frac{H}{d}]-[\frac{L-1}{d}])^N} F[n]=ndμ(nd)([dH][dL1])N

我看到其他的博客有讲到分块的方法,但感觉复杂度有点玄学.

const ll MOD = 1e9+7;
ll N,K,L,R;
ll mod_pow(ll x,ll n){
    ll res = 1;
    while(n){
        if(n & 1) res = res * x % MOD;
        x = x * x % MOD;
        n >>= 1;
    }
    return res;
}
ll F[200007];
const int maxn = 200000;
int main(){
    cin>>N>>K>>L>>R;
    ll ans = K >= L && K <= R;
    R /= K;
    L --;
    L /= K;
    ll len = R - L;
    for(ll d = len;d >= 1;--d){
        ll res = R/d - L/d;
        F[d] = (mod_pow(res,N)-res+MOD) % MOD;
        for(ll k = 2*d;k <= len;k += d)
            F[d] = (F[d] - F[k] + MOD) % MOD;
    }
    cout<<ans+F[1]<<endl;
    return 0;
}

徐州网络赛 Easy Math

前置技能

  1. 容斥原理
  2. 线性筛
  3. 杜教筛

题解

∑ i = 1 m μ ( i n ) \sum_{i=1}^{m}\mu(in) i=1mμ(in)

n n n存在平方因子的时候,直接输出 0 0 0作为答案,这是很显然的.

否则, n = p 1 p 2 . . . p k n = p_1p_2...p_k n=p1p2...pk,不含平方因子.

考虑区间 [ 1 , m ] [1,m] [1,m]中的数字 i i i,如果 i i i不含有因子 p 1 p_1 p1,则可以使用积性函数的性质.

即: μ ( i ∗ n ) = μ ( i ∗ n p 1 ∗ p 1 ) = − μ ( i ∗ n p 1 ) \mu(i*n) = \mu(i*\frac{n}{p_1}*p_1) = -\mu(i*\frac{n}{p_1}) μ(in)=μ(ip1np1)=μ(ip1n)

而区间 [ 1 , m ] [1,m] [1,m]中的数并不全都与 p 1 p_1 p1互质,这就会造成 μ ( i ∗ n ) = 0 \mu(i*n) = 0 μ(in)=0 μ ( i ∗ n p 1 ) ≠ 0 \mu(i*\frac{n}{p_1}) \ne 0 μ(ip1n)̸=0的情况,这就要求我们用容斥的思想来去掉这部分.

如果 i i i中恰好有一个因子 p 1 p_1 p1,则 μ ( i ∗ n p 1 ) = μ ( i p 1 ∗ n ) \mu(i*\frac{n}{p_1}) = \mu(\frac{i}{p_1}*n) μ(ip1n)=μ(p1in)

因此要减去 ∑ i = 1 ⌊ n p 1 ⌋ μ ( i n ) \sum_{i=1}^{\lfloor \frac{n}{p_1} \rfloor} \mu(in) i=1p1nμ(in)

而如果 i i i中有多于 1 1 1个的 p i p_i pi因子,那么这部分的莫比乌斯函数值无论是在那部分中都是 0 0 0,因此不会对最终答案造成影响.

这样相当于降低了问题的规模.

我们可以列出递推式子,然后递推解决这个问题.

∑ i = 1 n μ ( i n ) = − ∑ i = 1 n μ ( i ∗ n p 1 ) + ∑ i = 1 ⌊ n p 1 ⌋ μ ( i n ) \sum_{i=1}^{n}{\mu(in)} = -\sum_{i=1}^{n}{\mu(i*\frac{n}{p_1})} + \sum_{i=1}^{\lfloor \frac{n}{p_1} \rfloor} \mu(in) i=1nμ(in)=i=1nμ(ip1n)+i=1p1nμ(in)

递推终止的条件是

  1. n = = 1 n == 1 n==1 时候,使用杜教筛得到莫比乌斯函数的前缀和
  2. ∑ \sum 的上限为 0 0 0时候,直接返回 0 0 0.
typedef long long ll;
const int N = 10000000;;
int mu[N+10],prime[N+10],pcnt,zhi[N+10];
void sieve(){
    pcnt = 0;
    mu[1] = zhi[1] = 1;
    for(int i = 2;i <= N;++i){
        if(!zhi[i]) mu[i] = -1,prime[pcnt++] = i;
        for(int j = 0;j < pcnt && prime[j]*i <= N;++j){
            zhi[i*prime[j]] = 1;
            if(i % prime[j] == 0){
                mu[i*prime[j]] = 0;
                break;
            }
            else{
                mu[i*prime[j]] = -mu[i];
            }
        }
    }
    for(int i = 1;i <= N;++i) mu[i] += mu[i-1];
}
map<ll,int> rec,vis;
int Mu(int x){
    if(x <= N) return mu[x];
    if(vis[x]) return rec[x];
    int res = 1,now = x,nxt;
    while(now >= 2){
        nxt = x/(x/now+1);
        res -= (now - nxt) * Mu(x/now);
        now = nxt;
    }
    vis[x] = 1;
    return rec[x] = res;
}
ll m,n;
ll np[20];int npcnt;
int Ans(ll u,ll d){
    if(d == 1) return Mu(u);
    if(u == 0) return 0;
    for(int i = 0;i < npcnt;++i){
        if(d % np[i] == 0){
            return -Ans(u,d/np[i]) + Ans(u/np[i],d);
        }
    }
}
bool divide(){
    ll x = n;
    for(ll i = 2;i * i <= n;++i){
        int cc = 0;
        if(x % i == 0) np[npcnt++] = i;
        while(x % i == 0) x /= i,cc++;
        if(cc >= 2) return false;
    }
    if(x > 1) np[npcnt++] = x;
    return true;
}
int main(){
    sieve();
    cin>>m>>n;
    if(!divide()) return puts("0");
    cout<<Ans(m,n)<<endl;
    return 0;
}

NWERC2015 Debugging

题目描述

有一份包含一个Bug的 n , ( 1 ≤ n ≤ 1 0 6 ) n,(1 \le n \le 10^6 ) n,(1n106)行代码,运行一次到崩溃的时间为 r , 1 ≤ r ≤ 1 0 9 r,1 \le r \le 10^9 r,1r109.
现在你可以在任意一行花费时间 p , 1 ≤ p ≤ 1 0 9 p,1\le p \le 10^9 p,1p109设置一个 p r i n t f printf printf语句来判断程序是否运行到这里.
请问在最坏情况下,最少需要多少时间可以定位到bug所在行.

题解

因为我们调试的时候往往会运行很多次程序,而每次运行完之后,我们都能定位bug在哪一块代码行中.

因此我们动态规划的思想得到递推公式.
d p [ n ] dp[n] dp[n]表示调试 n n n行代码所需要花的时间.

d p [ n ] = m i n 1 ≤ k ≤ n ( d p [ n k + 1 ] + k ∗ p + r ) dp[n] = min_{1 \le k \le n}(dp[\frac{n}{k+1}]+k*p + r) dp[n]=min1kn(dp[k+1n]+kp+r)

直接递推的时间复杂度是 O ( n 2 ) O(n^2) O(n2)的,而如果我们发现对于 [ n k + 1 ] [\frac{n}{k+1}] [k+1n]相等的 k k k,我们只取 k k k的最小值就可以了.

所以通过底数分块,我们可以求得转移的时间复杂度为 n \sqrt{n} n ,而真正有效的状态数也只有 n \sqrt{n} n ,所以时间复杂度上限不会超过 O ( n ) O(n) O(n).

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值