数学

  • \(avg=\sum\limits_{i=1}^{n}(\dfrac{cnt_i}{cnt}*avg_i)\)
  • \(s^2=\sum\limits_{i=1}^{n}(\dfrac{cnt_i}{cnt}*((avg_i-avg)^2+{s_i}^2))=\dfrac{\sum\limits_{i=1}^{n}{avg_i}^2}{n}-avg^2\)
  • \(l+(l+x)+(l+2x)+...+(l+nx)=(l+(l+nx))*(((l+nx)-l)/x+1)/2\)
  • \(1^2+2^2+3^2+...+n^2=n*(n+1)*(2n+1)/6\)
  • \(a+a^2+a^3+...+a^n=(a^{n+1}-a)/(a-1)\)
  • 调和级数:\(\sum\limits_{i=1}^N\frac{1}{i}=O(\ln n)\)
  • \(n^2\)配对:\(\sum\limits_{i=0}^{n}\sum\limits_{j=0}^{n}(a_j*calc(i,j))=\sum\limits_{j=0}^{n}(a_j*\sum\limits_{i=0}^{n} calc(i,j))\)
  • \(\sum\limits_{i=1}^n calc(\gcd(i,n))\):如果n是1e9级别的,可以枚举n的约数j作为\(gcd_j\),i=1~n中满足\(\gcd(i,n)=gcd_j\)的i一共有\(\varphi (\frac{n}{gcd_j})\)个(证明:\(\gcd(i,n)=gcd_j\Leftrightarrow \gcd(\frac{i}{gcd_j},\frac{n}{gcd_j}=1)\),也就是互质)。\(O(\sqrt N * \log N)\)
for(int i=1;1ll*i*i<=n;i++)
    if(n%i==0)
    {
        sum+=euler(n/i)*calc(i);
        if(i*i!=n) sum+=euler(i)*calc(n/i);
    }
  • 凸函数\(f(x)\)的性质:若p+q=常数,则p与q间的差值越小,f(p)+f(q)越小。→若\(x_1+x_2+x_3+\cdots+x_n=c\),则当\(x_1=x_2=x_3=\cdots=x_n\)时,\(f(x_1)+f(x_2)+f(x_3)+\cdots+f(x_n)\)最小。

注意:能一起到后面除就一起到后面除。

1.数论

1.1.质数

在偶数中2是唯一的质数,除2外其他的偶数都是合数。

伯特兰-切比雪夫定理:若整数n > 3,则至少存在一个质数p,符合n < p < 2n − 2。

削弱版:对于所有大于1的整数n,至少存在一个质数p,符合n < p < 2n。

1~N中质数个数约为\(O(\frac{N}{\log N})\)

\(\sum\limits_{p\in \mathbb{P},p<N}\frac{1}{p}=O(\ln\ln N)\)

1~N每个数的分解质因数个数或者不同质因数个数的总和大约是\(N \ln\ln N\)

质数间隙长度的平均复杂度是\(O(\log n)\),最坏复杂度推测为\(O(\log ^2 n)\)

1.1.1.质数的判定

1.1.1.1.试除法\(O(\sqrt N)\)

bool is_prime(int n)
{
    if(n<2) return false;
    for(int i=1;i<=sqrt(n);i++) if(n%i==0) return false;
    return true;
}

1.1.1.2.\(Miller\ Rabin\)\(O(\log \log N)\)\(Pollard's\ rho\)\(O(N^{\frac{1}{4}})\)

Miller Rabin:快速判断一个数是否是质数。

Pollard's rho:求一个数的一个素因子。在此基础上用分治的思想可求一个数的最大素因子。

const int test[]={2,3,5,7,11,13,17,19,23,29,31,37}; //12个素数测试
int t;
unordered_map<LL,LL> h; //记忆化搜索

//long long范围的随机数
LL llrand(LL l,LL r)
{
    return 1ll*(1.0*rand()/RAND_MAX*(r-l)+l);
}

LL f(LL x,LL c,LL p)
{
    return (__int128(x)*x+c)%p;
}

//判断x是否是质数
bool miller_rabin(LL x)
{
    if(x==2) return true;
    if(x==1 || !(x&1)/*非2的偶数*/) return false;
    LL y=x-1,k=0;
    while(!(y&1)) y>>=1,k++;    //x-1=y*2^(k)
    for(int i=0;i<12;i++)
    {
        if(x==test[i]) return true;
        LL pre=qpow(test[i],y,x);   //pre=test^(y)
        for(int j=1;j<=k;j++)   //pre=test^(y*2^(j-1))同余x,nex=test^(y*2^(j))同余x
        {
            LL nex=__int128(pre)*pre%x;
            if((pre!=1 && pre!=x-1) && nex==1) return false; //违反二次探测定理
            pre=nex;
        }
        if(pre!=1) return false;    //违反费马小定理的逆命题
    }
    return true;
}

//求x的一个素因子
LL pollar_rho(LL x)
{
    if(x==4) return 2;  //特判
    while(true)
    {
        LL c=llrand(1,x-1); //随机常数
        LL pre=0,nex=0,pred=1,nexd;
        do
        {
            for(int i=1;i<=128;i++)
            {
                pre=f(pre,c,x)/*跳一步*/,nex=f(f(nex,c,x),c,x)/*跳两步*/;
                nexd=__int128(pred)*abs(pre-nex)%x; //累积乘积。性质:若gcd(a,b)>1,则gcd(ac,b)>1,减少调用gcd的次数
                if(nexd==0) break;  //1.pre==nex找到环冗余break;2.防止pred=0使下面的gcd(0,)导致返回x本身
                pred=nexd;  //在这里才赋值是为了防止上面的判定条件成立break到下面gcd(0,)而导致RE
            }
            LL d=gcd(pred,x);
            if(d>1) return d;   //求一个因子
        }while(pre!=nex);   //pre==nex找到环冗余break
    }
}

//求最大素因子
LL max_factor(LL x)
{
    if(h.count(x)) return h[x];
    if(miller_rabin(x)) return h[x]=x;
    LL fac=pollar_rho(x);
    return h[x]=max(max_factor(fac),max_factor(x/fac))/*求最大素因子*/;
}

srand(time(NULL));
h[1]=1,h[2]=2;

scanf("%lld",&n);
LL res=max_factor(n);
if(res==n) puts("Prime");
else printf("%lld\n",res);

1.1.2.质数的筛选

1~N中质数个数约为\(O(\frac{N}{\log N})\)

1.1.2.1.埃筛法

优点:可以区间筛。

int primes[N],pidx;
bool vis[N];
void get_primes(int n)
{
    memset(vis,0,sizeof vis);
    for(int i=2;i<=n;i++)
    {
        if(vis[i]) continue;
        primes[++pidx]=i;
        for(int j=i;j<=n/i;j++) vis[i*j]=true;
    }
    return ;
}

get_primes(N-1);//!!!注意是N-1,否则会越界!!!

1.1.2.2.线性筛法

性质:每个合数都只会被它的最小质因子筛掉。

//注意是否要取等
vector<int> prime;
int pmin[N];//pmin[x]:数x的最小质因子

void get_prime()
{
    for(int x=2;x<N;x++)
    {
        if(!pmin[x])
        {
            pmin[x]=x;
            prime.push_back(x);
        }
        for(auto p : prime)
        {
            if(x*p>=N) break;
            pmin[x*p]=p;
            if(x%p==0) break;
        }
    }
    return ;
}

get_prime();

1.1.2.3.区间筛

适用条件:筛[l,r]内的质数。\(r-l≤10^6\)

方法一:\(O(\sqrt r+(r-l)\log \log r)\)

\(r≤10^{12}\)

  1. 先用线性筛筛\([1,\sqrt r]\)内的质数。
  2. 因为[l,r]内的合数一定有质因子\(≤\sqrt r\),所以用\([1,\sqrt r]\)内的质数埃氏筛[l,r]内的质数。
vector<int> prime;

void get_primes2(LL l,LL r)
{
    memset(vis,false,sizeof vis);
    for(auto it : prime) for(LL i=max(2ll,LL(ceil(1.0*l/it)));i*it<=r;i++) vis[i*it-l]=true;
    return ;
}

方法二:\(O((r-l)\log\log r)\)

\(r≤10^{18}\)

用Miller-Rabin依次判断[l,r]内的数是不是质数。

1.1.3.质因数分解

算术基本定理:\(N=p_1^{c_1}*p_2^{c_2}*...*p_m^{c_m}\),(\(p_i\)是质数,\(c_i\)是正整数,\(p_1 < p_2 < ... < p_m\)

1.1.3.1.求N的质因数分解:试除法\(O(\sqrt N)\)

int p[N],c[N],idx;
void divide(int n)
{
    idx=0;
    
    for(int i=1;i<=primes_idx;i++)
    {
        if(n%primes[i]==0)
        {
            p[++idx]=primes[i],c[idx]=0;
            while(n%primes[i]==0)
            {
                n/=primes[i];
                c[idx]++;
            }
        }
    }
    
    if(n>1) p[++idx]=n,c[idx]=1;//注意最后n本身是质数的情况
    
    return ;
}

pre_primes(N-1);
divide(n);
for(int i=1;i<=idx;i++) printf("%d %d\n",p[i],c[i]);

1.1.3.2.求1~N每个数的质因数分解(从小到大排序):线性筛最小质因子转移\(O(N+N\ln\ln N)\)

1~N每个数的分解质因数个数或者不同质因数个数的总和大约是\(N \ln\ln N\)

vector<pii> pf[N];  //x=p1^c1*...*pm^cm:{{p1,c1},...,{pm,cm}}(p1<...<pm)

void get_pf()
{
    for(int x=2;x<N;x++)
    {
        int bx=x/pmin[x],p=pmin[x],c=1;
        while(bx>1)
        {
            if(pmin[bx]==p) c++;
            else
            {
                pf[x].push_back({p,c});
                p=pmin[bx],c=1;
            }
            bx/=pmin[bx];
        }
        pf[x].push_back({p,c});
    }
    return ;
}

get_prime();
get_pf();

1.2.约数

1.2.1.由1.1.3.算术基本定理的推论

\(N=p_1^{c_1}*p_2^{c_2}*...*p_m^{c_m}\),(\(p_i\)是质数,\(c_i\)是正整数,\(p_1 < p_2 < ... < p_m\)

N的正约数集合(组合)={\(p_1^{b_1}*p_2^{b_2}*...*p_m^{b_m}\)},\(0≤b_i≤c_i\)

N的正约数个数(乘法原理)=\((c_1+1)*(c_2+1)*...*(c_m+1)\),注意加1是从\(c_i\)中选0个的情况

N的所有正约数之和(多项式展开)=\((1+p_1+p_1^2+...+p_1^{c_1})*...*(1+p_m+p_m^2+...+p_m^{c_m})\)

1.2.2.求正约数集合

1.2.2.1.求N的正约数集合:试除法\(O(\sqrt N)\)

int factor[N],fidx;
void get_factor(int n)
{
    for(int i=1;i<=n/i;i++)
        if(n%i==0)
        {
            factor[++fidx]=i;
            if(i!=n/i) factor[++fidx]=n/i;  //注意i^2的特判
        }
    return ;
}

get_factor(n);
for(int i=1;i<=fidx;i++) printf("%d \n",factor[i]);

推论:N的约数个数的上界是\(2\sqrt {N}\)

n的约数个数d(n)的复杂度是\(O(2^{\frac{\log n}{\log \log n}})\)

$ n \leq $ $ 10^1 $ $ 10^2 $ $ 10^3 $ $ 10^4 $ $ 10^5 $ $ 10^6 $ $ 10^7 $ $ 10^8 $ $ 10^9 $
\(\max\{d(n)\}\) 4 12 32 64 128 240 448 768 1344
$ n \leq $ $ 10^{10} $ $ 10^{11} $ $ 10^{12} $ $ 10^{13} $ $ 10^{14} $ $ 10^{15} $ $ 10^{16} $ $ 10^{17} $ $ 10^{18} $
\(\max\{d(n)\}\) 2304 4032 6720 10752 17280 26880 41472 64512 103680

1.2.2.2.求1~N每个数的正约数集合(从小到大排序):倍数法\(O(N \ln N)\)

vector<int> factor[N];
void get_factor(int n)
{
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n/i;j++)
            factor[i*j].push_back(i);
    return ;
}

get_factor(N-1);//!!!注意是N-1,否则会越界!!!
for(int i=1;i<=n;i++)
{
    for(int j=0;j<factor[i].size();j++) printf("%d ",factor[i][j]);
    puts("");
}

推论:1~N每个数的约数个数总和大约是\(N \ln N\)

1.2.3.最大公约数\(\gcd\)与最小公倍数\(\operatorname{lcm}\)

1.2.3.1.定理

  • \(\forall a,b \in N,\gcd(a,b)*\operatorname{lcm}(a,b)=a*b \Leftrightarrow lcm(a,b)=a/\gcd(a,b)*b\)

  • \(\gcd(a,b)=d→\gcd(\frac{a}{d},\frac{b}{d})=1\),即\(\frac{a}{d}\)\(\frac{b}{d}\)互质。可以用于简化问题。

    1~n中互质的数对(a,b)((a,b)和(b,a)视为同一个数对)个数就是\(\sum\limits_{i=1}^{n}\varphi(i)\)

  • \(v_p(n)=\max\{k| p^k|n,k\in N\}\)。一个数可以看作以每个质数作为一个维度的高维空间中的一个向量\(\{v_2(n),v_3(n),v_5(n),\cdots\}\)。gcd相当于给多个向量的每个维度取min,而lcm则是取max。

  • 由于最小公倍数越取越大,所以求多个数的最小公倍数不能借助gcd求解,而是对于每个质数维护一个集合\(s_p=\max\{v_p(x)\}\)\(lcm=\prod\limits_{p\in Prime} p^{s_p}\)

1.2.3.2.欧几里德算法

核心:

  1. 当a≥b时,将原问题分解成规模为a%b的子问题2。
  2. 当a<b时,交换a,b地位,进入子问题1的迭代。
  3. 当变量出现0时(到达边界),直接求解。

复杂度一般是\(O(\log N)\)

1.2.3.2.1.欧几里得算法求gcd\(O(Q\log W)\)

\(\forall a,b \in N,\gcd(a,b)=\gcd(b,a\bmod b)\)

int gcd(int a,int b)
{
    return b==0 ? a : gcd(b,a%b);
}

1.2.3.2.2.扩展欧几里得算法

1.2.3.2.3.欧几里得模型

模型一

$f_x
\=\begin{cases}a_ef_{\frac{x}{2}}+b_ef_{\frac{x}{2}-1}+c_e&x\text{\ is\ even}
\a_of_{\lfloor\frac{x}{2}\rfloor}+c_o&x\text{\ is\ odd}
\end{cases}
\=1f_x+0f_{x-1}+0
\:=af_y+bf_{y-1}+c
\=\begin{cases}a(a_ef_{\frac{y}{2}}+b_ef_{\frac{y}{2}-1}+c_e)+b(a_of_{\lfloor\frac{y-1}{2}\rfloor}+c_o)+c&y\text{\ is\ even}
\a(a_of_{\lfloor\frac{y}{2}\rfloor}+c_o)+b(a_ef_{\frac{y-1}{2}}+b_ef_{\frac{y-1}{2}-1}+c_e)+c&y\text{\ is\ odd}
\end{cases}
\=\begin{cases}(aa_e)f_{\frac{y}{2}}+(ab_e+ba_o)f_{\frac{y}{2}-1}+(ac_e+bc_o+c)&y\text{\ is\ even}
\(aa_o+ba_e)f_{\frac{y-1}{2}}+(bb_e)f_{\frac{y-1}{2}-1}+(ac_o+bc_e+c)&y\text{\ is\ odd}
\end{cases} $

1.2.3.2.4.类欧几里得算法

适用条件:求解形如\(\sum\limits_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor\)的问题。

  • 推式子

    \(f(n,a,b,c)=\sum\limits_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor\)

    当a≥c或b≥c时,

    把a,b拆开,转化为对c取模的式子:

    \(=\sum\limits_{i=0}^n\lfloor\frac{(c\lfloor\frac{a}{c}\rfloor+a\%c)i+(b\lfloor\frac{b}{c}\rfloor+b\%c)}{c}\rfloor \\=\sum\limits_{i=0}^n(\lfloor\frac{a}{c}\rfloor i+\lfloor\frac{b}{c}\rfloor)+\sum\limits_{i=0}^n\lfloor\frac{(a\%c)i+b\%c}{c}\rfloor \\=(\lfloor\frac{a}{c}\rfloor \frac{n(n+1)}{2}+\lfloor\frac{b}{c}\rfloor(n+1))+f(n,a\%c,b\%c,c)\)

    当a,b<c时,

    把式子转化为求和符号的形式:

    \(=\sum\limits_{i=0}^n\sum\limits_{j=0}^{\lfloor\frac{ai+b}{c}\rfloor-1}1 \)

    交换求和符号:

    \(=\sum\limits_{i=0}^n\sum\limits_{j=0}^{\lfloor\frac{an+b}{c}\rfloor-1}[j\le \lfloor\frac{ai+b}{c}\rfloor-1] \\=\sum\limits_{j=0}^{\lfloor\frac{an+b}{c}\rfloor-1}\sum\limits_{i=0}^n[j+1\le \lfloor\frac{ai+b}{c}\rfloor] \)

    去掉取整符号:

    \(=\sum\limits_{j=0}^{\lfloor\frac{an+b}{c}\rfloor-1}\sum\limits_{i=0}^n[j+1\le\frac{ai+b}{c}]\)

    独立i,改为计算合法的i的数量,此时c与j在一起,为下面交换a,c地位做铺垫:

    $=\sum\limits_{j=0}{\lfloor\frac{an+b}{c}\rfloor-1}\sum\limits_{i=0}n[cj+c-b\le ai]
    \=\sum\limits_{j=0}{\lfloor\frac{an+b}{c}\rfloor-1}\sum\limits_{i=0}n[cj+c-b-1<ai]
    \=\sum\limits_{j=0}{\lfloor\frac{an+b}{c}\rfloor-1}\sum\limits_{i=0}n[\frac{cj+c-b-1}{a}<i] $

    加上取整符号,以便计算合法的i的数量,式子自然地交换了a,c地位,往下迭代:

    $=\sum\limits_{j=0}{\lfloor\frac{an+b}{c}\rfloor-1}\sum\limits_{i=0}n[\lfloor\frac{cj+c-b-1}{a}\rfloor<i]
    \=\sum\limits_{j=0}^{\lfloor\frac{an+b}{c}\rfloor-1}(n-\lfloor\frac{cj+c-b-1}{a}\rfloor)
    \=\lfloor\frac{an+b}{c}\rfloor n-f(\lfloor\frac{an+b}{c}\rfloor-1,c,c-b-1,a) $

注意运算可以取模,但是递归的参数不可以取模(而且递归的参数只会越来越小)。

LL f(LL n,LL a,LL b,LL c)
{
    if(!a) return (b/c)*(n+1)%MOD;
    if(a>=c || b>=c)
    {
        return ((a/c)*(n*(n+1)/2%MOD)%MOD+(b/c)*(n+1)%MOD+f(n,a%c,b%c,c))%MOD;
    }
    else
    {
        return (((a*n+b)/c)*n%MOD-f((a*n+b)/c-1,c,c-b-1,a)+MOD)%MOD;    //注意(a*n+b)/c-1<n,因为进入这一部分的条件是a<c。
    }
}

1.2.3.2.5.万能欧几里得算法

适用条件:设\(y=\frac{px+r}{q},x\in(0,n]\)(如果题目要求计算x=0的值,则把x=0拿出来单独计算),求\(\sum f(x)a^xg(x)b^y\)。可以把它归结一条直线\(y=\frac{px+r}{q}\)与网格图横线竖线相交的模型。

下面以常见的\(y=\lfloor\frac{px+r}{q}\rfloor\)为例。

原问题可以等价转化为:作出函数\(y=\frac{px+r}{q}\)的图像,之后从横坐标0开始从左往右考虑该线与网格线的交点,维护一个行向量,遇横线则乘矩阵R,遇竖线则乘矩阵C(同时相遇则优先乘R),求刚走过n条竖线后的最终向量。

形如\(\sum f(x)a^xg(x)b^y\)的问题,均可以通过简单构造行向量v和矩阵R,C来转化为上面的矩阵乘积问题。一般遇横线增加y的值,遇竖线增加x的值以及统计答案。

  • 构造\(\sum\limits_x y\)实战

    v:\(\begin{bmatrix}\sum\limits_x y & y &1\end{bmatrix}\)

    R:\(\begin{bmatrix}1&0&0\\0&1&0\\0&1&1\end{bmatrix}\)

    C:\(\begin{bmatrix}1&0&0\\1&1&0\\0&0&1\end{bmatrix}\)

快速计算等价转化后的矩阵乘积问题:万能欧几里得算法\(\gcd(p,q,r,n,R,C)\):一个含有n个C的R、C矩阵序列,序列末尾是C,且第x个C前面恰好有\(y=\lfloor\frac{px+r}{q}\rfloor\)个R,返回该序列的矩阵乘积。

为使整个问题的形式更严整,令开始传入递归时\(r\in[0,q)\)(后文会介绍如何处理r≥q)。只要开始传入递归时\(r\in[0,q)\),万能欧几里得算法就能保证递归中\(r\in[0,q)\)恒成立。

  • 有读者此时有疑惑:为什么要这样定义\(\gcd\)函数呢?
    • 条件“含有n个C”与“第x个C前面恰好有\(y=\lfloor\frac{px+r}{q}\rfloor\)个R”:

      因为这2个条件使得\(\gcd\)函数与原问题等价。

    • 条件“序列末尾是C”与“\(r\in[0,q)\)”:

      下文的“后果”部分会详细讲。

记p,q,r,n,R,C表示当前层的参数,p',q',r',n',R',C'表示下一层或上一层的参数。

建议\(\gcd\)函数背板子。(几乎所有的万能欧几里得的题的\(\gcd\)函数一模一样,而且\(\gcd\)函数代码较短)

\(\gcd\)函数

当p≥q时,

性质1:每个C前至少有\(\lfloor\frac{p}{q}\rfloor\)个连续的R。因为保证\(r\in[0,q)\),所以第一个C也成立。

由矩阵乘法的结合律,可以把\(R^{\lfloor\frac{p}{q}\rfloor}C\)合并成一个矩阵\(C'\)。合并后第x个C前面恰好有\(\lfloor\frac{px+r}{q}\rfloor-x\lfloor\frac{p}{q}\rfloor=\lfloor\frac{px+r}{q}\rfloor-x\frac{p-p\bmod q}{q}=\lfloor\frac{(p\bmod q)x+r}{q}\rfloor\)个R。

模仿欧几里得算法,将原问题分解成规模为p%q的子问题:\(\gcd(p,q,r,n,R,C)=\gcd(p\bmod q,q,r,n,R,R^{\lfloor\frac{p}{q}\rfloor}C)\)


当p<q时,

性质2:第x个C前恰好有\(\lfloor\frac{px+r}{q}\rfloor\)个R。

推论2:考虑“第y个R”以及其前面相邻的“第x个C”:\(C_xR_y\)\(\Leftrightarrow\)\(y>\lfloor\frac{px+r}{q}\rfloor\)(注意一次函数过格点时优先乘R)\(\Leftrightarrow\)在整数上的解集不变的情况下,对不等式的变形:\(x≤\lfloor\frac{qy-r-1}{p}\rfloor\)\(\Leftrightarrow\)第y个R前面恰好有\(\lfloor\frac{qy-r-1}{p}\rfloor\)个C。

模仿欧几里得算法,此时需要通过交换R,C的地位来交换p,q的地位往下递归迭代。根据\(\gcd\)的定义,\(\xRightarrow{?}\)\(\gcd(q,p,-r-1,\lfloor\frac{pn+r}{q}\rfloor,C,R)\)。但是直接这样递归会有2个问题:

  1. 万能欧几里得算法需要保证递归中\(r\in[0,q)\)恒成立,但是-r-1传入下一层显然不成立。

    • 后果

      下一层第一个C'(当前层的R)前面不会有\(\lfloor\frac{p'}{q'}\rfloor\)个连续的R'(当前层的C),不满足性质1。

    解决方法:把递归前第一个R与其前面连续的一段的C(共有\(\lfloor\frac{q-r-1}{p}\rfloor\)个)拿出来单独计算:\(C^{\lfloor\frac{q-r-1}{p}\rfloor}R\),不参与递归。

    此时递归到下一层的第y个C'(当前层的第y+1个R)前面恰好有\(\lfloor\frac{q(y+1)-r-1}{q}\rfloor-\lfloor\frac{q-r-1}{p}\rfloor=\lfloor\frac{q(y+1)-r-1}{q}\rfloor-\frac{(q-r-1)-(q-r-1)\bmod p}{p}=\lfloor\frac{qy+(q-r-1)\bmod p}{p}\rfloor\)个R'(当前层的C)。

    此时递归到下一层的\(r'=(q-r-1)\bmod p\in[0,q')=[0,p)\)

  2. 定义要求下一层的序列以C'(当前层的R)结尾,但是传入下一层的序列却以R'(当前层的C)结尾。

    • 后果

      递归过程中有一个参数n:该矩阵序列含有n个C。如果不保证序列以C结尾,那么递归到下一层计算C'(当前层的R)的数量n'时,只能通过性质2计算最后一个C前面有\(\lfloor\frac{pn+r}{q}\rfloor\)个R,而不能计算最后一个C后面有多少个R,导致n'不能确定而无法算对数值。

    解决方法:把递归前最后一个R后面连续的一段C拿出来单独计算,不参与递归。

    计算最后一个R后面连续的一段C的数量:R一共有\(m=\lfloor\frac{pn+r}{q}\rfloor\)个(注意下一层因为问题1拿出了1个当前层的R,C'一共有m-1个),则最后一个R前面一共有\(\lfloor\frac{qm-r-1}{p}\rfloor\)个C,故最后一个R后面一共有\(n-\lfloor\frac{qm-r-1}{p}\rfloor\)个C。\(C^{n-\lfloor\frac{qm-r-1}{p}\rfloor}\)

\(\gcd(p,q,r,n,R,C)=C^{\lfloor\frac{q-r-1}{p}\rfloor}R*\gcd(q,p,(q-r-1)\bmod p,m-1,C,R)*C^{n-\lfloor\frac{qm-r-1}{p}\rfloor}\)


边界:即将递归时的m=0:即当前层不含R,直接返回\(C^n\)。n=0:当前层为空,直接返回单位矩阵。作为模数的q=0:即上一层的p'=0,又因为r'<q',所以上一层的\(m'=\lfloor\frac{p'n'+r'}{q'}\rfloor=0\),在上一层就会直接返回。


处理开始递归前r≥q的情况:万能欧几里得算法是从横坐标0开始考虑网格线的,当r≥q时,横坐标负数那里会有\(\lfloor\frac{r}{q}\rfloor\)个R没有考虑,故要在递归前先乘上\(R^{\lfloor\frac{r}{q}\rfloor}\)。之后,每个C前面恰有的R的数量都应该减去\(\lfloor\frac{r}{q}\rfloor\),故开始递归时应传入r%q。这样就使得开始递归时r<q。

\(R^{\lfloor\frac{r}{q}\rfloor}*\gcd(p,q,r\%q,n,R,C)\)

矩阵

上文对\(\gcd\)函数的讨论已经足够地标准化了,只要问题满足万能欧几里得算法的适用条件,\(\gcd\)函数一模一样,不同的问题只需要改动矩阵的定义以及矩阵乘法。

矩阵乘法常数大。实际上可以把矩阵改成维护等价信息的结构体,矩阵乘法改成类似于线段树pushup那样合并2个结构体的信息的操作。正确性保证:两个都具有结合律。

注意,遇到竖线才统计答案。

  • 以设计“求\(\sum (y^2)\)”问题中的结构体为例:

    \(sy2=\sum (y^2)\)\(sy=\sum y\),遇到横线的数量y,遇到横线的数量x。

a:                                         b:
sy2_a=y_{a,1}^2,y_{a,2}^2,...,y_{a,n}^2    sy2_b=y_{b,1}^2,y_{b,2}^2,...,y_{b,m}^2
注意,由于遇到竖线才统计答案,所以n=结构体a遇到的竖线的数量$x_a$(而不是横线的数量$y_a$),$m=x_b$。

其中合并后$y_{a,i}^2$不变,而$y_{b,i}^2$应该变成$(y_{b,i}+y_{a,n})^2=y_{b,i}^2+2y_{b,i}y_{a,n}+y_{a,n}^2$:$sy2=sy2_a+(sy2_b+2*sy_b*y_a+y_a*y_a*x_b)$。

因此结构体除了维护sy2外,还需要维护sy、y、x。

其他信息的合并同理。

R和C的设计根据“矩阵乘法”而确定。

模板题。

1.2.3.3.九章算术更相减损术

应用:差分;线段树维护区间\(\gcd\);复杂度证明。

\(\forall a,b \in N,有\gcd(a,b)=\gcd(a,|b-a|)=\gcd(b,|b-a|)\)

\(\forall a,b,c \in N,有\gcd(a,b,c)=\gcd(a,|b-a|,|c-b|)\)

\(\forall 偶数a,b \in N,有\gcd(a,b)=2*\gcd(a/2,b/2)\)

1.2.3.4.高精度\(Stein\)算法求gcd\(O(Q*Calc*\log W)\)

auto gcd(auto a,auto b)
{
    if(a==0) return b;
    if(b==0) return a;
    if(a%2==0 && b%2==0) return 2*gcd(a/2,b/2);
    if(a%2==0 && b%2==1) return gcd(a/2,b);
    if(a%2==1 && b%2==0) return gcd(a,b/2);
    
    if(a>b) swap(a,b);
    return gcd(a,b-a);
}

1.2.3.5.递推求gcd\(O(W^2+Q)-O(W^2)\)

公式:\(\gcd(a,b)=\gcd(b,a)=\gcd(b,a\bmod b)\)

int g[W][W];

for(int i=0;i<W;i++) g[i][0]=g[0][i]=i;
for(int i=1;i<W;i++)
    for(int j=1;j<=i;j++)
        g[i][j]=g[j][i]=g[j][i%j];

cin>>a>>b;
cout<<g[a][b]<<endl;

1.2.3.6.基于值域预处理\(O(1)\)求gcd\(O(W+Q)-O(W)\)

  1. 《1.2.3.5.递推求gcd\(O(W^2+Q)-O(W^2)\)\(O(\sqrt W)*O(\sqrt W)=O(W)\)递推预处理值域\(\sqrt W\)内任意两个数的gcd。

  2. \(O(W)\)内每个x因数分解成合法分解\(\{fac_{x,1},fac_{x,2},fac_{x,3}\}\),满足\(fac_{x,i}≤\sqrt x或\in Prime\),且\(fac_{x,i}\)升序排序。

    定理:对于x≥2,找到x的最小质因子p以及\(\frac{x}{p}\)的合法分解\(\{fac_{\frac{x}{p},0},fac_{\frac{x}{p},1},fac_{\frac{x}{p},2}\}\),则x的一种合法分解为\(\{fac_{\frac{x}{p},0}*p,fac_{\frac{x}{p},1},fac_{\frac{x}{p},2}\}\)的升序排序。

    因此就可以线性筛求解了。

  3. 对于一个询问gcd(a,b),\(\gcd(a,b)=\gcd(fac_{a,0},b)*\gcd(fac_{a,1},b)*\gcd(fac_{a,2},b)\)

    • \(fac_{a,i}<\sqrt W\),则由欧几里德算法:\(\gcd(fac_{a,i},b)=\gcd(fac_{a,i},b\bmod fac_{a,i})\)
    • \(fac_{a,i}\ge\sqrt W\),则\(fac_{a,i}\)一定是一个质数,\(\gcd(fac_{a,i},b)=\begin{cases} fac_{a,i} & fac_{a,i}\mid b \\ 1 & fac_{a,i}\nmid b \end{cases}\)

    注意把\(\gcd(fac_{a,i},b)\)计算贡献后要令\(b=b/\gcd(fac_{a,i},b)\),防止重复计算。

const int W=1e6+10,S=1e3+10;    //W:值域;S:sqrt(W)
int prime[W],pidx;  //线性筛
bool st[W];
int fac[W][3],gcd_pre[S][S];    //fac[x]:x的大小为3的合法因数分解。gcd_pre[a][b]:gcd(a,b)

void pre_gcd()
{
    //通过int gcd(int a,int b) {return b==0 ? a : gcd(b,a%b);}可以递推预处理gcd_pre
    for(int i=0;i<S;i++) gcd_pre[i][0]=gcd_pre[0][i]=i;
    for(int i=1;i<S;i++) for(int j=1;j<=i;j++) gcd_pre[i][j]=gcd_pre[j][i]=gcd_pre[j][i%j];
    
    fac[1][0]=fac[1][1]=fac[1][2]=1;
    for(int i=2;i<W;i++)
    {
        if(!st[i])
        {
            prime[++pidx]=i;
            fac[i][0]=fac[i][1]=1,fac[i][2]=i;
        }
        for(int j=1;j<=pidx && 1ll*i*prime[j]<W;j++)
        {
            int res=i*prime[j];
            st[res]=true;
            
            fac[res][0]=fac[i][0]*prime[j],fac[res][1]=fac[i][1],fac[res][2]=fac[i][2];
            
            //手写sort(fac[res],fac[res]+3);
            if(fac[res][0]>fac[res][1]) swap(fac[res][0],fac[res][1]);
            if(fac[res][1]>fac[res][2]) swap(fac[res][1],fac[res][2]);
            
            if(i%prime[j]==0) break;
        }
    }
    
    return ;
}

int qgcd(int a,int b)
{
    int prod=1;
    for(int i=0;i<3;i++)
    {
        if(fac[a][i]<S)
        {
            //注意先乘再除。因为乘法要用到b的信息
            prod*=gcd_pre[fac[a][i]][b%fac[a][i]];
            b/=gcd_pre[fac[a][i]][b%fac[a][i]];
        }
        else if(b%fac[a][i]==0)
        {
            prod*=fac[a][i];
            b/=fac[a][i];
        }
    }
    return prod;
}

1.2.4.欧拉函数

1.2.4.1.互质

a、b互质:\(\gcd(a,b)=1\)

a、b、c互质:\(\gcd(a,b,c)=1\)

a、b、c两两互质:\(\gcd(a,b)=\gcd(b,c)=\gcd(a,c)=1\)

[x,x+y-1]最多只有1个≥y的倍数。x与x+y-1不同时为≥y的倍数。

相邻的正整数一定互质。

相邻的奇数一定互质。

i与n互质\(\Leftrightarrow\)n-i与n互质\(\Leftrightarrow\)i与i+n互质。证明:更相易减术:\(\gcd(i,n)=\gcd(n-i,n)\)

1.2.4.2.欧拉函数

欧拉函数的定义:phi(N):1~N中与N互质的数的个数。(定义1和所有的数都互质,phi(1)=1)

注意:根据积性函数的定义,\(\varphi(1)=1\)。但是在某些题目背景中,\(\varphi(1)\)会有其他的定义(\(**e.g.**\)\(**\sum\limits_{i=1}^{n}[\gcd(i,n)==1]=\varphi(n)**\)中,\(**\varphi(1)=0**\)。)。\(\varphi(1)\)的值不会影响线性筛,但会影响杜教筛(只需在代码中定义\(**\varphi(1)=1**\),杜教筛筛出的最终答案减去\(**\varphi(1)=1**\)带来的影响即可解决)。

欧拉函数的计算式:\(N=p_1^{c_1}*p_2^{c_2}*...*p_m^{c_m}\),则\(phi(N)=N*\dfrac{p_1-1}{p_1}*\dfrac{p_2-1}{p_2}*...*\dfrac{p_m-1}{p_m}\)

证明:容斥原理:

\(N=p^{c_1}*q^{c_2}\)为例:\(phi(N)=N-\dfrac{N}{p}-\dfrac{N}{q}+\dfrac{N}{p*q}=N*(1-\dfrac{1}{p})*(1-\dfrac{1}{q})\)

欧拉函数的性质:

  1. 1~n与n互质的数的和为\(\begin{cases}1&n==1\\ \frac{n}{2}*\varphi(n)&\text{Otherwise.}\end{cases}\)

    证明:当n>1时,\(\varphi(n)\)总是偶数,这是因为与n互质的数总是成对出现:i与n互质\(\Leftrightarrow\)n-i与n互质,i与n-i的和是n,并且有\(\frac{\varphi(n)}{2}\)对。

  2. 若a、b互质,则phi(ab)=phi(a)phi(b)(同时也可以理解为是下面的公式gcd(a,b)=1时的特殊情况);

否则,\(phi(a*b)=\dfrac{phi(a)*phi(b)*\gcd (a,b)}{phi(\gcd(a,b))}\)

  • 证明

    \(phi(a)=a*\dfrac{d_1-1}{d_1}*\dfrac{d_2-1}{d_2}*...*\dfrac{d_m-1}{d_m}*\dfrac{a_1-1}{a_1}*\dfrac{a_2-1}{a_2}*...*\dfrac{a_{ma}-1}{a_{ma}}\)

    \(phi(b)=b*\dfrac{d_1-1}{d_1}*\dfrac{d_2-1}{d_2}*...*\dfrac{d_m-1}{d_m}*\dfrac{b_1-1}{b_1}*\dfrac{b_2-1}{b_2}*...*\dfrac{b_{mb}-1}{b_{mb}}\)

    \(d=gcd(a,b),phi(d)=d*\dfrac{d_1-1}{d_1}*\dfrac{d_2-1}{d_2}*...*\dfrac{d_m-1}{d_m}\)

    \(phi(a*b)=a*b*\dfrac{d_1-1}{d_1}*\dfrac{d_2-1}{d_2}*...*\dfrac{d_m-1}{d_m}*\dfrac{a_1-1}{a_1}*\dfrac{a_2-1}{a_2}*...*\dfrac{a_{ma}-1}{a_{ma}}*\dfrac{b_1-1}{b_1}*\dfrac{b_2-1}{b_2}*...*\dfrac{b_{mb}-1}{b_{mb}}\)

    剩下的证明很显然……

  1. \(\sum\limits_{n\%d==0}phi(d)=n\)
  2. 如果当a、b互质时,有f(ab)=f(a)f(b),那么称函数f为积性函数;

若f是积性函数,且\(n=\prod\limits_{i=1}^{m}p_i^{c_i}\),则\(phi(n)=\prod\limits_{i=1}^{m}phi(p_i^{c_i})\)

  1. \(\sum\limits_{i=1}^n calc(\gcd(i,n))\):如果n是1e9级别的,可以枚举n的约数j作为\(gcd_j\),i=1~n中满足\(\gcd(i,n)=gcd_j\)的i一共有\(\varphi (\frac{n}{gcd_j})\)个(证明:\(\gcd(i,n)=gcd_j\Leftrightarrow \gcd(\frac{i}{gcd_j},\frac{n}{gcd_j})=1\),也就是互质)。\(O(\sqrt N * \log N)\)
for(int i=1;1ll*i*i<=n;i++)
    if(n%i==0)
    {
        sum+=euler(n/i)*calc(i);
        if(i*i!=n) sum+=euler(i)*calc(n/i);
    }

1.2.4.2.1.求N的欧拉函数:分解质因数

由上面欧拉函数的计算式并结合分解质因数的模板可推出计算式

筛质数只要筛到****\(\sqrt N\)即可。(因为大于\(\sqrt N\)的质因数有且仅有一个)

int euler(int n)
{
    int ans=n;
    for(int i=1;i<=pidx && n>=primes[i];i++)
    {
        if(n%primes[i]==0)
        {
            ans=ans/primes[i]*(primes[i]-1);
            while(n%primes[i]==0) n/=primes[i];
        }
    }
    if(n>1) ans=ans/n*(n-1);//大于sqrt(N)的质因数
    return ans;
}

1.2.4.2.2.求1~N每个数的欧拉函数:质数线性筛法

由上面欧拉函数的性质并结合质数线性筛法的模板可推出计算式:有正整数2≤i≤n和质数2≤j≤n

  1. 由质数的定义:一个质数j和1~j-1的每个数都互质:phi(j)=j-1;
  2. 当i和j互质时:phi(ij)=phi(i)phi(j);
  3. 当i和j不互质时,由于j是质数,故gcd(i,j)=j:\(phi(i*j)=\dfrac{phi(i)*phi(j)*\gcd (i,j)}{phi(\gcd(i,j))}=\dfrac{phi(i)*phi(j)*j}{phi(j)}=phi(i)*j\)
int primes[N],pridx;
int phi[N];
bool st[N];

void euler(int n)//在筛质数的同时求欧拉函数
{
    phi[1]=1,st[1]=true;
    for(int i=2;i<=n;i++)
    {
        if(!st[i])
        {
            primes[++pridx]=i;
            phi[i]=i-1;
        }
        for(int j=1;j<=pridx && i<=n/primes[j];j++)
        {
            st[i*primes[j]]=true;
            if(i%primes[j]==0)
            {
                phi[i*primes[j]]=phi[i]*primes[j];
                break;
            }
            phi[i*primes[j]]=phi[i]*phi[primes[j]];
        }
    }
    return ;
}

euler(N-1);//!!!注意是N-1,否则会越界!!!

1.3.同余

1.3.1.运算性质

a\(\equiv\)a mod n
若a\(\equiv\)b mod n,则b\(\equiv\)a mod n
若a\(\equiv\)b mod n,b\(\equiv\)c mod n,则a\(\equiv\)c mod n
若a\(\equiv\)b mod n,c\(\equiv\)d mod n,则a\(\pm\)c\(\equiv\)b\(\pm\)d mod n
若a\(\equiv\)b mod n,c\(\equiv\)d mod n,则ac\(\equiv\)bd mod n
若a\(\equiv\)b mod n,\(k,c \in Z,k>0\)则a^k c\(\equiv\)b^k c mod n
若ac\(\equiv\)bc mod n,则a\(\equiv\)b mod (n
/gcd(n,c)**);若n、c又互质,则a\(\equiv\)b mod n
若ac\(\equiv\)b mod n,如果c、n互质(即c有在模n意义下的逆元),则a\(\equiv\)b\(c^{-1}\) mod n;如果c、n不互质且c不能整除b,无解。
注意,如果等式两边同时除以一个数,模数n要除以gcd(n,c);如果等式两边同时乘逆元,模数n不变
若a\(\equiv\)b mod n,且n%d==0,则a\(\equiv\)b mod d
若a\(\equiv\)b mod n,且d≠0,则da\(\equiv\)db mod dn
若x\(\equiv\)c mod n,x\(\equiv\)c mod m,n、m互质,则x\(\equiv\)c mod nm
若a+b
c=d,则a\(\equiv\) d mod b。可以用于消灭c。

1.3.2.定理

1.3.2.1.欧拉定理与费马小定理

\(a^{phi(n)} \equiv 1 \pmod n\),其中\(phi(n)\)是欧拉函数。
特别地,当n是质数p时,有费马小定理\(a^{p-1} \equiv 1 \pmod p\)

1.3.2.2.裴蜀定理

ax+by=gcd(a,b)=gcd(b,a%b)=

  1. 保持(x,y)递推)=bx'+(a mod b)y'=bx'+(a-a/bb)y'=ay'+b(x'-a/by'),所以令x=y',y=x'-a/b*y'
  2. 交叉(x,y)递推)=by'+(a mod b)x'=ax'+b(y'-a/bx'),所以令x=x',y=y'-a/bx'

应用。

1.3.2.3.中国剩余定理

适用条件:由\(a+b*c=d\Leftrightarrow a\equiv d \pmod b\)得:形如\(\begin{cases}a_i+b_i*x+c_i*r_i=0\end{cases}\)求出最小的x且\(a_i,b_i,c_i\)已给出,\(r_i\)根据x决定它的值。

\(\Leftrightarrow -b_ix\equiv a_i\pmod {c_i}\)。可以发现未知的\(r_i\)消灭了。将题目转化为了中国剩余定理。

1.3.2.3.1.中国剩余定理

解决问题:形如两两互质的整数\(m_1\)\(m_2\)、...、\(m_n\),对于给出的n个整数\(a_1\)\(a_2\)、...、\(a_n\),有关于x的一元线性同余方程组:

\[\begin{cases}x\equiv a_1(\operatorname{mod} m_1)\\x\equiv a_2(\operatorname{mod} m_2)\\\vdots\\x\equiv a_n(\operatorname{mod} m_n)\end{cases} \]

\(M=\prod\limits_{i=1}^{n}m_i,M_i=\dfrac{M}{m_i},M_i^{-1}\equiv \frac{1}{M_i}\pmod {m_i}\)

有特解\(x_0=\sum\limits_{i=1}^{n}a_i*M_i*M_i^{-1}\)

最小正整数解\(x=(x0 \mod M+M)\mod M\)

  • 详细说明

    有整数解\(x=\sum\limits_{i=1}^{n}a_i*k_i\)

    \(k_i\):设\(M=\prod\limits_{i=1}^{n}m_i,M_i=\dfrac{M}{m_i}\),则\(k_i=M_i*M_i^{-1}\)

找到一个数\(k_i\),使\(k_i \equiv 0 \pmod {M_i},k_i \equiv 1 \pmod {m_i}\),即给x加上\(k_i\)后,只会使x%\(m_i\)的结果加上1,而不影响其他的同余方程。

显然\(M_i*M_i^{-1} \equiv 0 \pmod {M_i}\),要使\(M_i*M_i^{-1} \equiv 1 \pmod {m_i}\),则\(M_i^{-1}\)\(M_i\)在模\(m_i\)意义下的逆元。最小正整数逆元\(M_i^{-1}=(M_i^{-1}\bmod m[i]+m[i])\bmod m[i]\)

综上所述,一个特解\(x_0=\sum\limits_{i=1}^{n}a_i*M_i*M_i^{-1}\),通解\(x=x_0+k*M,(k \in Z)\)(注意这里是M,因为\(k*M \equiv 0 (\mod m_i)\)\(x\equiv a_1(\mod m_1)\)相加不影响\(x\equiv a_1(\mod m_1)\)),最小正整数解\(x=(x0 \mod M+M)\mod M\)

scanf("%d",&n);
for(int i=1;i<=n;i++)
{
    scanf("%lld%lld",&m[i],&a[i]);
    M*=m[i];
}
for(int i=1;i<=n;i++)
{
    LL Mi=M/m[i];
    
    //求Mi在模m[i]意义下的最小正整数逆元
    LL invm,y;    //invm:Mi在模m[i]意义下的逆元
    exgcd(Mi,m[i],invm,y);
    invm=(invm%m[i]+m[i])%m[i]; //注意模数的不同
    
    ans=(ans+(__int128)a[i]*Mi%M*invm%M)%M; //注意模数的不同。在模之前可能会爆long long
}
printf("%lld\n",(ans%M+M)%M);

1.3.2.3.2.扩展中国剩余定理

解决问题:中国剩余定理去掉\(m_i\)两两互质的条件。

1.3.2.3.2.1.两个方程

\(\begin{cases}x\equiv a_1(\operatorname{mod} m_1)\\x\equiv a_2(\operatorname{mod} m_2)\end{cases}\)

  1. \(\Leftrightarrow\)\(\begin{cases}x=m_1*p+a_1\\x=-m_2*q+a_2\end{cases}\)\(\Leftrightarrow\)\(m_1*p+m_2*q=a_2-a_1\)exgcd()解出最小正整数解p''。若无解,则原方程组无解。
  2. 将p的通解\(p'=p_0+r*\dfrac{m_2}{\gcd(m_1,m_2)},r\in Z\)代入第一个方程可得:\(a_x=p''*m_1+a_1\)\(m_x=|\frac{m_0*m_1}{\gcd(m_0,m_1)}|=|lcm(m_1,m_2)|\)注意lcm要取绝对值。
  3. 更新a和m,将两个方程合并为一个方程:\(x \equiv a_x \pmod {m_x}\)。因为\(x\equiv a \pmod m \Leftrightarrow x\equiv (a \bmod m) \pmod m\),所以记得给a模新的模数m。
  4. x的最小正整数解$x=a_x\bmod m_x \(。通解\)x=(a_x \bmod m_x)+k*m_x$。
  • 详细说明

    \(\begin{cases}x\equiv a_1(\operatorname{mod} m_1)\\x\equiv a_2(\operatorname{mod} m_2)\end{cases}\)由1.4.3.线性同余方程的内容\(\Leftrightarrow\)\(\begin{cases}x=m_1*p+a_1\\x=-m_2*q+a_2\end{cases}\)\(\Leftrightarrow\)\(m_1*p+m_2*q=a_2-a_1\)由1.3.4.2.不定方程的内容得:若不定方程有解可解得一组特解\(p_0=p_{gcd}*\frac{(a_2-a_1)}{lca(m1,-m2)}\)\(q_0\);若不定方程无解则\(x\)无解。我们任选p0和q0其中一个继续求解(假设是p0)。

    由1.3.4.2.不定方程的内容得:p的通解:\(p'=p_0+r*\dfrac{m_2}{d}\),p

    的最小正整数解:\(p''=(p_0\bmod|\dfrac{m_2}{d}|+|\dfrac{m_2}{d}|)\bmod|\dfrac{m_2}{d}|\),注意模数要取绝对值。

    将p的通解p'代入上面的方程组,并整理:\(x=(p_0+r*\frac{m_2}{d})*m1+a_1=r*lcm(m_1,m_2)+p_0*m_1+a_1\)。由于\(p_0*m_1+a_1\)可能越界,\(m_1\)\(a_1\)是定值,问题出在p上:我们需要将p的最小正整数解p''代替\(p_0\),r再相应地变化为r'即可:\(x=r'*lcm(m_1,m_2)+p''*m_1+a_1\)

    \(a_x=p''*m_1+a_1\)\(m_x=|\frac{m_0*m_1}{\gcd(m_0,m_1)}|=|lcm(m_1,m_2)|\)注意lcm要取绝对值\(\Leftrightarrow x=r'*m_x+a_x\)得到x的通解。

    \(\Leftrightarrow x \equiv a_x \pmod {m_x}\)

    \(\Rightarrow\)x的最小正整数解$x=a_x\bmod m_x $。

1.3.2.3.2.2.n个方程

\(\begin{cases}x\equiv a_1(\operatorname{mod} m_1)\\x\equiv a_2(\operatorname{mod} m_2)\\\vdots\\x\equiv a_n(\operatorname{mod} m_n)\end{cases}\)任选其中两个方程\(\begin{cases}x\equiv a_1(\operatorname{mod} m_1)\\x\equiv a_2(\operatorname{mod} m_2)\end{cases}\)《数学1.3.2.3.2.1.两个方程》合并为一个方程$x=a_x\bmod m_x \((暂不解出x,因为不一定满足后面的方程),这样\)n\(个方程就减少为\)n-1$个方程。如此循环直至减少为\(1\)个方程解出\(x\)。原方程组有解的条件是操作中选出的任意2个方程有解。

注意求出新的a1后要对新的模数取模防止爆long long,\(x\equiv a \pmod m \Leftrightarrow x\equiv (a \bmod m) \pmod m\)。m1不用取模,因为实际上m1是m1~mi的最小公倍数,题目会保证最小公倍数在long long范围内。

注意乘的过程中防止爆long long。

scanf("%d",&n);
scanf("%lld%lld",&m1,&a1);
for(int i=2;i<=n;i++)
{
    scanf("%lld%lld",&m2,&a2);
    LL d=exgcd(m1,-m2,p,q);//exgcd解转化后的不定方程
    if((a2-a1)%d!=0)//若无解,则原方程组无解
    {
        puts("-1");
        return 0;
    }
    
    //求出不定方程中的p的最小正整数解p''
    __int128 pp=(__int128)p*((a2-a1)/d);//pp在下面取模前可能会爆long long
    pp=(pp%abs(m2/d)+abs(m2/d))%abs(m2/d);
    
    //更新a和m
    //下面顺序不可颠倒!!!
    __int128 aa=(__int128)pp*m1+a1;//注意爆long long
    m1=abs(m1/d*m2);//实际上m1是m1~mi的最小公倍数
    a1=aa%m1;//记得给a模新的模数m
}
printf("%lld\n",(a1%m1+m1)%m1);

1.3.2.3.2.3.有系数的方程组

形如\(\begin{cases}k_2*x\equiv a_2(\operatorname{mod} m_2)\\k_3*x\equiv a_3(\operatorname{mod} m_3)\\\vdots\\k_n*x\equiv a_n(\operatorname{mod} m_n)\end{cases}\)

不能两边同时乘\(k_i^{-1}\),因为逆元不一定存在。

  1. 为了消除系数,方程组增加1个恒成立的方程:\(x\equiv a_1 \pmod {m_1},a_1=0,m_1=1\)
  2. 仿照无系数的方程组解法,从两个方程开始:\(\begin{cases}x\equiv a_1(\operatorname{mod} m_1)\\k_2*x\equiv a_2(\operatorname{mod} m_2)\end{cases}\)\(\Rightarrow\) \(k_2*m_1*p-m_2*q=a_2-a_1\)exgcd()解出最小正整数解p''。因为exgcd()求的是\(\gcd(k_2*m_1,-m_2)\),所以可以给\(k_2*m_1\)\(m_2\)
  3. 将p的通解\(p'=p_0+r*\dfrac{m_2}{d}\)代入第一个方程可得:\(a_x=p''*m_1+a_1\)\(m_x=|\frac{m_1*m_2}{\gcd(k_2*m_1,m_2)}|\)注意lcm要取绝对值且不是\(lcm(m_1,m_2)\)
  4. 后面就和《数学1.3.2.3.2.2.n个方程》一样了。更新a和m,将两个方程合并为一个方程:\(x \equiv a_x \pmod {m_x}\)。x的最小正整数解$x=a_x\bmod m_x \(,通解\)x=(a_x \bmod m_x)+k*m_x$……

1.3.2.4.威尔逊定理

p为质数\(\Leftrightarrow\)\((p-1)!\equiv -1(\operatorname{mod} p)\)

拓展:见《数学3.1.2.求组合数4.》

1.3.2.5.原根

由欧拉定理\(a^{\varphi(m)}\equiv 1\pmod m\),当\(\gcd(a,m)=1\)时,满足\(a^{\delta_m\left(a\right)}\equiv 1\pmod m\)的同余方程的最小正整数\(\delta_m\left(a\right)\)一定存在,则称该最小正整数\(\delta_m\left(a\right)\)为a模m的阶。

性质:

  1. \(a,a^2,\cdots,a^{\delta_m\left(a\right)}\)模m两两不同余且不为0。
    • 证明

      反证法。假设存在\(1\le i,j\le\delta_m\left(a\right),i\ne j\),满足\(a^i\equiv a^j\pmod m\),那么有\(a^{\left|i-j\right|}\equiv1\pmod m\)成立。而\(0<\left|i-j\right|<\delta_m\left(a\right)\),这与阶的最小性矛盾,因此假设不成立。

  2. \(a^n\equiv1\pmod m\),则\(\delta_m\left(a\right)\mid n\)
    • 证明

      不妨设\(n=q\delta_m\left(a\right)+r,q\in\mathbb{N},0\le r<\delta_m\left(a\right)\),则有如下式子成立:

\[a^r\equiv a^r\left(a^{\delta_m\left(a\right)}\right)^q\equiv a^n\equiv1\pmod m \]

  1. \(m\in\mathbb{N}^*,a,b\in\mathbb{Z},\gcd\left(a,m\right)=\gcd\left(b,m\right)=1\),则\(\delta_m\left(ab\right)=\delta_m\left(a\right)\delta_m\left(b\right)\)的充分必要条件是\(\gcd\left(\delta_m\left(a\right),\delta_m\left(b\right)\right)=1\)
  2. \(k\in\mathbb{N},m\in\mathbb{N}^*,a\in\mathbb{Z},\gcd\left(a,m\right)=1\),则:\(\delta_m\left(a^k\right)=\dfrac{\delta_m\left(a\right)}{\gcd\left(\delta_m\left(a\right),k\right)}\)

原根

\(m\in\mathbb{N}^*,g\in\mathbb{Z}\)。若\(\gcd\left(g,m\right)=1\),且\(\delta_m\left(g\right)=\varphi\left(m\right)\),则称\(g\)为模\(m\)的原根。即\(g^{\varphi(m)}\equiv 1\pmod m\)\(\varphi(m)\)是同余方程\(g^{\delta_m\left(g\right)}\equiv 1\pmod m\)的最小正整数解。

其实只需要关心数值范围在[0,m-1]的原根g。因为底数可以模m,所以当一个原根g'不在范围[0,m-1]内,g'和g=(g'%m+m)%m的情况是一样的。

原根存在定理

一个数m存在原根,当且仅当\(m=2,4,p^\alpha,2p^\alpha\),其中p为奇素数,\(\alpha\in\mathbb{N}^*\)

最小原根的数量级:\(O(\sqrt[4]N)\)

原根判定定理

  • 当m=2时,在[0,1]范围内的2的原根为1。

  • 当m≥3时,则g是模m的原根的充要条件是:

    1. \(\gcd(g,m)=1\)
    2. 对于\(\varphi\left(m\right)\)的每个素因数p,都有\(a^{\frac{\varphi\left(m\right)}{p}}\not\equiv1\pmod m\)
  • 证明(必要性)

    \(a\)是模\(m\)的原根,那么有\(\delta_m\left(a\right)=\varphi\left(m\right)\)成立。如果存在一个\(\varphi\left(m\right)\)素因数\(p\)使得\(a^{\frac{\varphi\left(m\right)}{p}}\equiv1\pmod m\),将与阶的最小性矛盾。

    必要性得证。

  • 证明(充分性)

    用反证法证明。

    当对于\(\varphi\left(m\right)\)的每个素因数\(p\),都有$a^{\frac{\varphi\left(m\right)}{p}}\not\equiv1\pmod m \(时,我们假设存在一个\)a\(,它不是模\)m$的原根。

    因为\(a\)不是模\(m\)的原根,则存在一个\(t<\varphi\left(m\right)\)使得\(a^t\equiv1\pmod m\)

    由裴蜀定理得,一定存在一组\(k,x\)满足\(kt=x\varphi\left(m\right)+\gcd\left(t,\varphi\left(m\right)\right)\)

    又由欧拉定理,\(a^{\varphi\left(m\right)}\equiv1\pmod m\),故有:

\[1\equiv a^{kt}\equiv a^{x\varphi\left(m\right)+\gcd\left(t,\varphi\left(m\right)\right)}\equiv a^{\gcd\left(t,\varphi\left(m\right)\right)}\pmod m \]

原根个数

若一个数m有原根,则它的数值范围在[0,m-1]内的原根个数为\(\varphi\left(\varphi\left(m\right)\right)\)

  • 证明及根据最小原根找其他原根的方法

    证明能找到\(\varphi\left(\varphi\left(m\right)\right)\)个数值满足原根的定义:

    \(m\)有原根\(g\),则:

\[\delta_m\left(g^k\right)=\frac{\delta_m\left(g\right)}{\gcd\left(\delta_m\left(g\right),k\right)}=\frac{\varphi\left(m\right)}{\gcd\left(\delta_m\left(g\right),k\right)} \]

求一个数m在[0,m-1]内的所有原根

  1. 预处理质数、欧拉函数以及have_root[m]:m是否存在原根。

  2. 使用have_root\(O(1)\)判断m是否存在原根。特判m=2。

  3. 预处理\(\varphi(m)\)的质因数集合,暴力枚举找到m的最小原根g。把g放入m的原根集合

    也可以不预处理have_root,若枚举到\(\sqrt[4]m+c\)还没有找到原根,则判定m不存在原根。注意不是只枚举到\(\sqrt[4]m\),一个反例是18的最小原根\(=5>\sqrt[4]{18}\)

  4. 枚举次数\(k\in[2,\varphi(m)-1]\),若k与\(\varphi(m)\)互质,则把\(g^k\bmod m\)放入m的原根集合。

int m;
vector<int> prime;
bool vis[N],have_root[N];   //have_root[m]:m是否存在原根
int phi[N];
vector<int> factor,root;    //factor:phi[m]的质因数集合;root:m的原根集合

void get_prime()
{
    for(auto it : prime)
    {
        if(it==2) have_root[2]=have_root[4]=true;   //2和4
        else    //素因数的幂及2倍的素因数的幂
        {
            LL x=it;
            while(x<N)
            {
                have_root[x]=true;
                if(2*x<N) have_root[2*x]=true;
                x*=it;
            }
        }
    }
    return ;
}

//原根判定定理,判断x是不是m的一个原根
bool check_root(int x,int m)
{
    if(gcd(x,m)!=1) return false;
    for(auto it : factor) if(qpow(x,phi[m]/it,m)==1) return false;
    return true;
}

//暴力枚举找到m的最小原根g
int find_smallest_root(int m)
{
    for(int i=1;i<m;i++) if(check_root(i,m)) return i;
}

//第一行输出m在[0,m-1]内的原根个数。第二行从小到大依次输出m在[0,m-1]内的所有原根
get_prime();
int m=read();
if(!have_root[m])
{
    puts("0\n");
    return 0;
}
if(m==2)
{
    puts("1\n1");
    return 0;
}
get_factor(phi[m]);
int g=find_smallest_root(m);
root.push_back(g);
for(int i=2;i<phi[m];i++) if(gcd(i,phi[m])==1) root.push_back(qpow(g,i,m));
sort(root.begin(),root.end());
printf("%d\n",phi[phi[m]]);
for(auto it : root) printf("%d ",it);
puts("");
return 0;

应用

  1. 与数列\(\{a^i\bmod m\}\)的循环节有关的问题。
  2. 离散对数。
  3. NTT、单位根反演。

1.3.3.模p运算

1.3.3.1.直接模p型

\(a+b\)((long long)(a%p)+(b%p))%p;\(a-b\)res=(long long)(a%p)-(b%p),res=((res%p)+p)%p;\(a*b\)((long long)(a%p)*(b%p))%p;

1.3.3.2.乘方\(a^b\)

底数a可以直接模p。

关于指数,当a、p一定时,随着b的增长,\(a^b\bmod p\)\(\rho\)形。

  • 模数p与a互质:欧拉定理

    \(a^b \equiv a^{b \bmod \phi(p)} \pmod p\)pow(a%p,b%phi(p))%p

  • 模数p任意:扩展欧拉定理

    1. \(b<\phi(p)\)时,由于没有进入\(\rho\)的环形,因此\(a^b\equiv a^b\pmod p\)pow(a%p,b)%p
    2. \(b\ge \phi(p)\)时,由于进入了\(\rho\)的环形,因此\(a^b \equiv a^{b \bmod \phi(p) + \phi(p)} \pmod p\)pow(a%p,b%phi(p)+phi(p))%p
//注意指数b的模数与快速幂的模数不一样!!!

LL a,b,m,phi_m;
bool over_phi_m;    //b是否超过phi(m),若超过则b最后还需要加上phi(m)

LL read_b()
{
    LL res=0;
    char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while(isdigit(ch))
    {
        res=res*10+ch-'0';
        if(res>=phi_m)
        {
            over_phi_m=true;
            res%=phi_m;
        }
        ch=getchar();
    }
    return res;
}

scanf("%d%d",&a,&m);
phi_m=get_phi();
b=read_b();
if(over_phi_m) b+=phi_m;
printf("%lld\n",qpow(a,b,m));

结论:

  1. 定义\(f(x,1)=x,f(x,2)=x^x,f(x,3)=x^{x^x}\),则\(f(x,n)\bmod p\)\(n>O(\log p)\)时为定值

    证明:由扩展欧拉定理:假设下面的递归过程满足扩展欧拉定理的条件,\(f(x,n)\bmod p=x^{f(x,n-1)\bmod \phi(p) +\phi(p)}\bmod p=x^{x^{f(x,n-2)\bmod \phi(\phi(p))+\phi(\phi(p))}\bmod \phi(p)+\phi(p)}\bmod p=…\)。因为\(\phi(\phi(…\phi(p)))=1,num\%1=0\),此时不需要往下递归直接返回。

    若p是偶数,那么\(\phi(p)\le \frac{p}{2}\);若p是奇数,则\(\phi(p)\)根据欧拉函数的定义可证是偶数。因此每2步可使得自变量除以2。因此p只需套\(O(\log p)\)\(\phi\)就变成1。

1.3.3.3.除法 \(a/b\)

前提:b、p互质。
找到一个b在模p意义下的逆元\(b^{-1}\),使a/b\(\equiv\)a*\(b^{-1}\)mod p。
a%=p,b%=p,get_b_inv(),res=(a*b_inv)%p;

逆元\(b^{-1}\)\(b*b^{-1}=b*\frac{1}{b}\)在模数p意义下等于1。

求逆元\(b^{-1}\):因为a/b\(\equiv\)a\(b^{-1}\)\(\equiv\)a\(b^{-1}\)/bb\(\equiv\)a/bb\(b^{-1}\)(mod p),所以b\(b^{-1}\)$\equiv$1 mod p:

  • 求一个逆元,且模数p是质数且p>b:快速幂求逆元\(O(\log P)\)

    由费马小定理\(b^{p-1} \equiv 1 (\mod p)\)得:b*\(b^{p-2}\)$\equiv\(1 mod p,故此时\)b{-1}$=$b$。

  • 求一个逆元,且模数p任意exgcd()求逆元\(O(\log P)\)

    通过求解同余方程b*\(b^{-1}\)$\equiv\(1 mod p解得\)b^{-1}$。

    最小正整数逆元\(b^{-1}=(b^{-1}\mod p+p)\mod p\)

int get_inv(int b,int p)
{
    int inv_b,y;
    exgcd(b,p,inv_b,y);
    return (inv_b%p+p)%p;//这里不要忘记!!!
}
  • 求序列\(a_1,a_2,...,a_n\)中所有数在共同模数p意义下的逆元,且模数p任意:线性求逆元\(O(N+\log P)\)

    \((\prod\limits_{i=1}^na_i)^{-1}\equiv\prod\limits_{i=1}^{n}a_i^{-1}\)

    1. 求出前缀积序列:\(\prod\limits_{i=1}^1 a_i,\prod\limits_{i=1}^2 a_i,...,\prod\limits_{i=1}^na_i\)
    2. 求出\(\prod\limits_{i=1}^na_i\)的逆元\((\prod\limits_{i=1}^na_i)^{-1}\)
    3. \(\because\)\((\prod\limits_{i=1}^na_i)^{-1}*a_n=(\prod\limits_{i=1}^n a_i^{-1})*a_n=\prod\limits_{i=1}^{n-1}a_i^{-1}=(\prod\limits_{i=1}^{n-1}a_i)^{-1}\)\(\therefore\)可以O(N)从后往前递推求前缀积序列的逆元。
    4. \(a_k^{-1}=(\prod\limits_{i=1}^{k}a_i)^{-1}*(\prod\limits_{i=1}^{k-1}a_i)\)
prod[0]=1;
for(int i=1;i<=n;i++) prod[i]=prod[i-1]*a[i]%MOD;
inv[n]=get_inv(prod[n],MOD);
for(int i=n-1;i>=1;i--) inv[i]=inv[i+1]*a[i+1]%MOD;
for(int i=1;i<=n;i++) inv[i]=inv[i]*prod[i-1];

1.3.4.同余方程

1.3.4.1.\(exgcd\)算法

求解ax+by=gcd(a,b),通过\(exgcd\)算法可以求出d=gcd(a,b)和方程的一组特解\(x_{gcd}\)\(y_{gcd}\)

t=abs(``b``/d);则x的最小正整数解x=(xgcd%t+t)%t;(没有x和y的最小正整数解这一说法)。

通解\(x=x_{gcd}+k*(\)\(b\)\(/d),y=y_{gcd}-k*(\)\(a\)\(/d).(k \in Z)\)

a和b必须为正数!!!否则取模会有问题。负号可以移到x和y。

//交叉(x,y)递推写法
LL exgcd(LL a,LL b,LL &x,LL &y)
{
    if(b==0)
    {
        x=1;
        y=0;
        return a;
    }
    LL d=exgcd(b,a%b,y,x);  //此时x、y是下一层的,并非当前层

    y-=a/b*x;//a/b一定要下取整

    return d;
}

/*保持(x,y)递推
LL exgcd(LL a,LL b,LL &x,LL &y)
{
    if(b==0)
    {
        x=1;
        y=0;
        return a;
    }
    LL d=exgcd(b,a%b,x,y);  //此时x、y是下一层的,并非当前层

    LL backup=x;
    x=y;
    y=backup-a/b*y;

    return d;
}
*/

LL a,b,x,y,d;
scanf("%lld%lld",&a,&b);
d=exgcd(a,b,x,y);//求出d=gcd(a,b)和方程的一组特解x_{gcd}、y_{gcd}
//后续的求通解

1.3.4.2.一次不定方程

1.3.4.2.1.二元一次不定方程

ax+by=c有解\(\Leftrightarrow\)c%gcd(a,b)==0。

\(a_1*x_1+a_2*x_2+a_3*x_3+\cdots+a_n*x_n=c\)有解\(\Leftrightarrow\)\(c\%\gcd(a_1,a_2,a_3,\cdots,a_n)==0\)

  1. 通过LL d=exgcd(a,b,xgcd,ygcd)求出ax+by=gcd(a,b)方程的一组特解\(x_{gcd}\)\(y_{gcd}\)

  2. 然后令x0=xgcd*(c/d),y0=ygcd*(c/d);得到原方程的一组特解(并非最小正整数解)\(x_0\)\(y_0\)

  3. t=abs(``b``/d);则x的最小正整数解x=(x0%t+t)%t;(没有x和y的最小正整数解这一说法)。

    通解\(x=x_0+k*(\)\(b\)\(/d),y=y_0-k*(\)\(a\)\(/d).(k \in Z)\)

判断是否存在一对非负整数解x,y:这样做可以避免精度。

//x,y是一组特解
if(x>0) y+=a*(x/b),x-=x/b*b;
if(y>0) x+=b*(y/a),y-=y/a*a;
if(x>=0&&y>=0) puts("YES");
else puts("NO");

1.3.4.2.2.多元一次不定方程

《图论4.4.6.1.多元一次不定方程》

1.3.4.3.线性同余方程

\(a*x\equiv c \pmod b \Leftrightarrow a*x-c=b*(-y)\Leftrightarrow a*x+b*y=c\)

此时原同余方程的每一个解x\(\Leftrightarrow\)转化后不定方程的每一个解x。

之后的判断是否有解以及求解见《数学1.3.4.2.不定方程》

1.3.4.4.线性同余方程组

见1.3.2.3.中国剩余定理。

1.3.4.5.高次同余方程

注意在模p意义下,若原数≠0,则\(0^0=1,0^x=0,(x>0)\)

1.3.4.5.1.\(a^x \equiv b \pmod p\),其中a、p互质:\(Baby Step,Giant Step\)北上广深算法

给定整数a、b、p,其中a、p互质,求一个最小非负整数解x,使得\(a^x \equiv b \pmod p\)

由欧拉定理:\(a^x\equiv a^{x\bmod phi(p)} \pmod p\),因此x≤p-1。

设x=i*k-j,其中\(k=\left\lceil\sqrt{p}\right\rceil\),0≤j≤k-1,则原方程变为\((a^k)^i \equiv b*a^j \pmod p\)

  1. 特判a=0或x=0的情况。
  2. \((b*a^j)\%p\)(0≤j≤k-1)插入Hash表。
  3. 枚举1≤i≤k,在Hash表中查找是否存在\((a^k)^i\)对应的j,答案是i*k-j。
int bsgs(int a,int b,int p)
{
    //注意下面三者的顺序!!!
    a%=p,b%=p;//别忘了模p优化
    if(1%p/*防止p=1*/==b%p) return 0;//特判x=0
    if(a==0)
    {
        if(b==0) return 1;
        else return -INF;//注意无解要返回-INF。因为exbsgs每次回溯答案会被+1
    }
    
    int k=sqrt(p)+1;//向上取整
    
    map<int,int> h;
    for(int i=0,j=b%p;i<=k-1;i++)
    {
        h[j]=i;
        j=1ll*j*a%p;
    }
    
    int ak=qpow(a,k,p);
    for(int i=1,j=ak;i<=k;i++)
    {
        if(h.count(j)) return 1ll*i*k-h[j];
        j=1ll*j*ak%p;
    }
    return -INF;//注意如果是配合exbsgs的话,无解要返回-INF。因为exbsgs每次回溯答案会被+1
}

int x=bsgs(a,b,p);
if(x<0) puts("No Solution");
else printf("%d\n",x);

1.3.4.5.2.\(a^x \equiv b \pmod p\),其中a、p任意:拓展\(exBaby Step,Giant Step\)

给定整数a、b、p,其中a、p任意,求一个最小非负整数解x,使得\(a^x \equiv b \pmod p\)

  1. 特判a=0或x=0的情况。

  2. 当x≥1时,

    \(d=\gcd(a,p)\)

    1. 当d=1时,BSGS算法求解x。

    2. 当d>1时,

      \(\Leftrightarrow\)\(a^x+k*p=b\)\(\Leftrightarrow\)( 此处要求d能整除b,否则不存在正整数k使得整数=小数)\(\frac{a}{d}*a^{x-1}+k*\frac{p}{d}=\frac{b}{d}\)\(\Leftrightarrow\)\(a^{x-1}\equiv \frac{b}{d}*(\frac{a}{d})^{-1}\pmod {\frac{p}{d}}\)

      1. 当d不能整除b时,无解。
      2. 当d能整除b时,解\(a^{x-1}\equiv \frac{b}{d}*(\frac{a}{d})^{-1}\pmod {\frac{p}{d}}\)方程,递归继续处理(因为p可能包含很多个d)。
int exbsgs(int a,int b,int p)
{
    //注意下面三者的顺序!!!
    a%=p,b%=p;//别忘了模p优化
    if(1%p/*防止p=1*/==b%p) return 0;//特判x=0
    if(a==0)
    {
        if(b==0) return 1;
        else return -INF;//注意无解要返回-INF。因为exbsgs每次回溯答案会被+1
    }
    
    int x,y;
    int d=exgcd(a,p,x,y);//求gcd
    if(d==1) return bsgs(a,b,p);
    else
    {
        if(b%d!=0) return -INF;
        else
        {
            exgcd(a/d,p/d,x,y);//求逆元
            return exbsgs(a,(1ll*b/d*x%(p/d)+(p/d))%(p/d),p/d)+1;//注意这里是模新的模数
        }
    }
}

if(a==0)    //特判原数=0
{
    if(b!=0) puts("No Solution");
    else puts("1");
}
else
{
    LL x=exbsgs(a,b,p);
    if(x<0) puts("No Solution");
    else printf("%lld\n",x);
}

1.3.4.5.3.\(x^a \equiv b \pmod p\)

1.3.4.6.\(a \equiv b \pmod x\)

\(x|(a-b)\)

证明:\(a \equiv c \pmod x,b \equiv c \pmod x\Rightarrow a-b\equiv 0 \pmod x\)

1.4.数论函数

1.4.1.运算律

推式子的理论基础。

核心:拆开i,j二元项,独立计算一元项以预处理或降低复杂度。

加法交换律和结合律:

  1. \(\sum\limits_i\sum\limits_jf(i,j)[P(i,j)]=\sum\limits_{P(i,j)}f(i,j)=\sum\limits_j\sum\limits_if(i,j)[P(i,j)]\)
    • 当i,j的范围相互无关,即\([i\in I,j\in J]==[i\in I][j\in J]\)时,\(\sum\limits_{i\in I}\sum\limits_{j\in J}f(i,j)=\sum\limits_{i\in I,j\in J}f(i,j)=\sum\limits_{j\in J}\sum\limits_{i\in I}f(i,j)\)

    • 当内和的范围与外和的指标变量有关时,

      \(\sum\limits_{i\in I}\sum\limits_{j\in J(i)}f(i,j)=\sum\limits_{i\in I}\sum\limits_{j\in J'}f(i,j)[P(i,j)]=\sum\limits_{j\in J'}\sum\limits_{i\in I}f(i,j)[P(i,j)]=\sum\limits_{j\in J'}\sum\limits_{i\in I'(j)}f(i,j)\)

      • 深入理解

        \([i\in I][j\in J]==[j\in J'][i\in I']\),有\(\sum\limits_{i\in I}\sum\limits_{j\in J(i)} f(i,j)=\sum\limits_{j\in J'}\sum\limits_{i\in I'(j)}f(i,j)\)

        \(e.g.\)\(\because [1≤i≤n][i≤j≤n]==[1≤i≤j≤n]==[1≤j≤n][1≤i≤j] \therefore \sum\limits_{i=1}^n\sum\limits_{j=i}^nf(i,j)=\sum\limits_{1≤i≤j≤n}f(i,j)=\sum\limits_{j=1}^n\sum\limits_{i=1}^jf(i,j)\)

  2. \(\sum\limits_{i=1}^{n} (f(i)+g(i))=\sum\limits_{i=1}^{n} f(i)+\sum\limits_{i=1}^{n}g(i)\)

乘法分配律:

  1. 移动常数:\(\sum\limits_{i=1}^{n}(a*f(i))=a*\sum\limits_{i=1}^{n}f(i)\)

  2. 把i,j拆开:\(\sum\limits_i\sum\limits_j (f(i)*f(j))=(\sum\limits_i f(i))*(\sum\limits_j f(j))\),选定了i可以自由选定j。

    注意,若选定了i不可以自由选定j而是限定了j,则不能使用乘法分配律,\(e.g.\)\(\sum\limits_i\sum\limits_j(f(i,j)*g(i,j))≠(\sum\limits_i\sum\limits_jf(i,j))*(\sum\limits_i\sum\limits_jg(i,j))\)

指数运算律:\(\prod\limits_i(i^a)=(\prod\limits_i i)^a\)\(a^x*a^y=a^{x+y}\)\((a^x)^y=a^{x*y}\)\((\frac{a}{b})^x=\frac{a^x}{b^x}\)\(a^{-x}=(a^x)^{-1}=(a^{-1})^x\)

对数运算律:\(\log_a(x*y)=\log_ax+\log_ay\)\(\log_a(x^y)=y*\log_ax\)\(\log_ab=\frac{\log_cb}{\log_ca}\)

1.4.2.数论分块(整除分块)

1.4.2.1.向下取整的整除分块

适用条件:求解形如 \(\sum\limits_{i=1}^{n} (f(i)g(\lfloor \frac{n}{i} \rfloor))\)\(,(1≤n≤10^{12})\) 的和式。当可以在\(O(1)\)内计算\(f(r)-f(l)\)或已经预处理\(f\)的前缀和时,数论分块就可以在\(O(\sqrt n)\)的时间内计算上述的和式的值。

引理

\(\forall i \in [l,\lfloor \frac{n}{\lfloor \frac{n}{l} \rfloor} \rfloor]\)\(\lfloor \dfrac{n}{i} \rfloor\)的值均等于\(\lfloor \dfrac{n}{l} \rfloor\)

最多有\(2 \sqrt n\)种不同的\(\lfloor \dfrac{n}{i} \rfloor\)的值。

于是\(\sum\limits_{i=l}^{\lfloor \frac{n}{\lfloor \frac{n}{l} \rfloor} \rfloor} f(i)g(\lfloor \frac{n}{i} \rfloor)=\sum\limits_{i=l}^{\lfloor \frac{n}{\lfloor \frac{n}{l} \rfloor} \rfloor} f(i)g(\lfloor \frac{n}{l} \rfloor)=g(\lfloor \frac{n}{l} \rfloor)*\sum\limits_{i=l}^{\lfloor \frac{n}{\lfloor \frac{n}{l} \rfloor} \rfloor} f(i)\)

变形

  1. \(\sum\limits_{i=1}^{INF} (\lfloor \dfrac{n}{i} \rfloor*f(i))\)\(\sum\limits_{i=1}^{m} (f(i)*g(\lfloor \frac{n}{i} \rfloor))\)也可以用整除分块求解:因为当i>n时\(\lfloor \dfrac{n}{i} \rfloor\)为0。
  2. \(n\bmod i=n-i*\lfloor\frac{n}{i}\rfloor\)进而可以用整除分块求解。

前缀和公式

  1. \(l+(l+x)+(l+2x)+...+(l+nx)=(l+(l+nx))*(((l+nx)-l)/x+1)/2\)
  2. \(1^2+2^2+3^2+...+n^2=n*(n+1)*(2n+1)/6\)
  3. \(1^3+2^3+3^3+...+n^3=(1+2+3+...+n)^2=(1+n)^2*n^2/4\)
  4. \(a+a^2+a^3+...+a^n=(a^{n+1}-a)/(a-1)\)。本质是等比数列求和公式\(S_n=\begin{cases}\frac{a_1(1-q^n)}{1-q}&q\ne 1\\na_1&q=1\end{cases}\)

具体实现

下面以“给定n,m,求\(\sum\limits_{i=1}^{m} (f(i)*g(\lfloor \frac{n}{i} \rfloor))\)”为例。

注意特判除0和开long long!

get_f_sum();//获取f(i)的前缀和,记为s(i);
LL ans=0;
for(LL l=1,r;l<=m;l=r+1)
{
    if(n/l==0)//后面\lfloor \dfrac{n}{i} \rfloor都为0了
    {
        ans+=1ll*g(0)*(s(m)-s(l-1));
        break;
    }
    r=min(m/*小心越界*/,n/(n/l));
    ans+=1ll*g(n/l)*(s(r)-s(l-1));
    //ans=1ll*g(n/l)*(f(l)+f(r))*(r-l+1)/2;//O(1)计算f()+等差数列
}
return ans;

1.4.2.2.向上取整的整除分块

类似于向下取整的整除分块。

适用条件:求解形如\(\sum\limits_{i=1}^{n} (f(i)g(\lceil \frac{n}{i} \rceil))\)的和式。

引理:\(\forall i \in [\lceil \frac{n}{\lceil \frac{n}{l} \rceil} \rceil,r]\)\(\lceil \dfrac{n}{i} \rceil\)的值均等于\(\lceil \dfrac{n}{r} \rceil\)

最多有\(2 \sqrt n\)种不同的\(\lfloor \dfrac{n}{i} \rfloor\)的值。

从大到小枚举r。

get_f_sum();//获取f(i)的前缀和,记为s(i);
LL ans=0;
for(LL l,r=m;r>=1;r=l-1)
{
    l=max(1,ceil(1.0*n/(ceil(1.0*n/r))));
    ans+=1ll*g(ceil(1.0*n/r))*(s(r)-s(l-1));
    //ans=1ll*g(ceil(1.0*n/r))*(f(l)+f(r))*(r-l+1)/2;//O(1)计算f()+等差数列
}
return ans;

1.4.2.3.N维数论分块

求含有$\lfloor \dfrac{a_1}{i} \rfloor \(、\)\lfloor \dfrac{a_2}{i} \rfloor\(、...、\)\lfloor \dfrac{a_n}{i} \rfloor\(的和式时,数论分块右端点的表达式从一维的\)\lfloor \dfrac{n}{i} \rfloor $变为 $\min\limits_{j=1}^{n} \lfloor \dfrac{a_j}{i} \rfloor \(,即对于每一个块的右端点取最小(最接近左端点)的那个作为整体的右端点。时间复杂度:\)O(N\sqrt N)\(。\)e.g.$《1.4.5.1.求满足a≤x≤b,c≤y≤d,gcd(x,y)=k的二元组(x,y)个数》

int n=min(a,b);
for(int l=1,r;l<=n;l=r+1)
{
    r=min({n,a/(a/l),b/(b/l)});
    ans+=1ll*(sum[r]-sum[l-1])*(a/l)*(b/l);
}
  • 例题1:求出$\sum\limits_{i=1}^{n} \lfloor \dfrac

\rfloor $。

scanf("%lld",&n);
LL ans=0;
for(LL l=1,r;l<=n;l=r+1)
{
    if(n/l==0) break;//后面的答案都没有贡献了
    r=min(n,n/(n/l));
    ans+=1ll*(n/l)*(r-l+1);
}
return ans;

例题2:余数之和

1.4.3.莫比乌斯函数

前置知识:《数学3.2.容斥原理》

定义

\(N=p_1^{c_1}*p_2^{c_2}*...*p_m^{c_m}\),定义\(\mu (N)=\begin{cases} 0,(\exists i \in [1,m],使c_i \ge 2) \\ (-1)^m,(\forall i \in [1,m],有c_i = 1) \end{cases}\),其中\((-1)^m=\begin{cases} 1,(m是偶数) \\ -1,(m是奇数) \end{cases}\)

注意:根据积性函数的定义,\(\mu(1)=1\)。但是在某些题目背景中,\(\mu(1)\)会有其他的定义。\(\mu(1)\)的值不会影响线性筛,但会影响杜教筛(只需在代码中定义\(**\mu(1)=1**\),杜教筛筛出的最终答案减去\(**\mu(1)=1**\)带来的影响即可解决)。

意义:1~N中与N互质的数的个数\(=\sum\limits_{i=1}^{N} \mu (i)*\lfloor \dfrac{N}{i} \rfloor\)。证明:容斥原理。

意义:1~N中与N互质的数的个数\(=\sum\limits_{k} \mu (k)*S_k\),(\(k=p_1^{x_1}*p_2^{x_2}*...*p_m^{x_m},(x_!\le c_1,x_2\le c_2,...,x_m\le c_m)\)\(S_k\):1~N中k的倍数的个数)

性质

定义\(S(n)=\sum\limits_{d|n} \mu (d)\),则\(S(n)=\begin{cases} 1,(n=1)\\0,(n≥2) \end{cases}\)

应用:将两数互质的真值表达式转化为数学公式:\(\sum\limits_{d|gcd(i,j)}\mu(d)=[gcd(i,j)==1]\)

1.4.3.1.求N的莫比乌斯函数:分解质因数

\(O(\sqrt N)\)做法

由上面莫比乌斯函数的定义式并结合分解质因数的模板可推出计算式

int mobius(int n)
{
    int res=1;
    for(int i=1;i<=pidx;i++)
    {
        if(n%primes[i]==0)
        {
            n/=primes[i];
            res*=-1;
            if(n%primes[i]==0) return 0;
        }
    }
    if(n>1) res*=-1;
    return res;
}

\(O(\sqrt[3]N)\)做法

  1. 线性筛\(\sqrt[3]N\)以内的质数与莫比乌斯函数。
  2. n剩下的质因子\(>\sqrt[3]N\),故最多只会有2个质因子:
    1. Miller-Rabin判断p:*(-1)。
    2. n%(LL)sqrt(n)==0 && n/(LL)sqrt(n)==(LL)sqrt(n)判断\(p^2\):*0。
    3. 最后只剩一种情况pq:*1。

1.4.3.2.求1~N每个数的莫比乌斯函数:质数线性筛法

由上面莫比乌斯函数的定义式并结合质数线性筛法的模板可推出计算式

\(\mu (1)=(-1)^0=1\)

int primes[N],pridx;
int mu[N];
bool st[N];

void mobius(int n)//在筛质数的同时求莫比乌斯函数
{
    mu[1]=1,st[1]=true;//这里不要忘记!!!
    for(int i=2;i<=n;i++)
    {
        if(!st[i])
        {
            primes[++pridx]=i;
            mu[i]=-1;
        }
        for(int j=1;j<=pridx && i<=n/primes[j];j++)
        {
            st[i*primes[j]]=true;
            if(i%primes[j]==0)
            {
                mu[i*primes[j]]=0;
                break;
            }
            mu[i*primes[j]]=mu[i]*(-1);
        }
    }
    return ;
}

mobius(N-1);//!!!注意是N-1,否则会越界!!!

1.4.4.数论函数

数论函数:定义域是正整数,值域是一个数集的函数。

1.4.4.1.基本函数

  • 指数函数:\(\text{Id}_{k}(n)=n^k\)

    特别地,当k=1时,\(\text{Id}(n)=n\)

    当k=0时,有\(\textbf{1}\)函数:\(\text{Id}_{0}(n)=\textbf{1}(n)=1\)

  • \(\sigma_k(n)=\sum\limits_{d|n}d^k\)

    特别地,当k=1时,有因数之和函数:\(\sigma(n)=\sum\limits_{d|n}d\)

    当k=0时,有因数个数函数:\(\sigma_0(n)=\textbf{d}(n)\)

    \(\sum\limits_{i=1}^n\sigma_k(n)=\sum\limits_{i=1}^n(\lfloor\frac{n}{i}\rfloor*i^k)(枚举因数)\)

  • 欧拉函数:\(\varphi(n)\)

  • 莫比乌斯函数:\(\mu (n)\)

1.4.4.2.运算

加法:逐项相加。(f+g)(n)=f(n)+g(n)。

数乘:(xf)(n)=x·f(n)。

卷积:\(C_n=\bigoplus\limits_{i,j,i\otimes j=n} A_i*B_j\)

1.4.5.积性函数

定义

\(\forall \gcd(a,b)==1\),都有\(f(a*b)==f(a)f(b)\),则称\(f(x)\)为积性函数。

\(e.g.\)莫比乌斯函数\(\mu(x)\)、欧拉函数\(\varphi(x)\)、因数个数函数\(\textbf{d}(n)\)、因数之和函数\(\sigma(n)\)……

\(\forall a,b\),都有\(f(a*b)==f(a)f(b)\),则称\(f(x)\)为完全积性函数。

\(e.g.\)指数函数\(\text{Id}_{k}(n)\)……

性质

  1. 积性函数f(1)=1。

    证明:由积性函数的定义:f(n)f(1)=f(n1)=f(n),所以f(1)=1。

  2. 积性函数均可用质数线性筛法求出1~N每个数的函数值;

  3. 若 f(n) 和 g(n) 都是积性函数,,则以下函数也为积性函数:

    • \(h(x)=f(x^m)\)
    • \(h(x)=f^m(x)\)
    • \(h(x)=f(x)g(x)\)
    • \(h(x)=\sum\limits_{d|x} f(d)g(\frac{x}{d})\)

证明一个函数的积性

  • 方法一:证明\(f(a)*f(b)=f(a*b),a\perp b\)
  • 方法二:构造积性函数\(g,h\)使得\(f*g=h\)\(f=g*h\)

1.4.6.狄利克雷卷积

数论函数之间的运算。

设函数\(h(x)=(f*g)(x)=f(x)g(x)\),则\((f*g)(n)=\sum\limits_{xy=n\text{ and }x,y\in\mathbb{Z}}f(x)g(y)=\sum_{d|n}f(d)g\left(\dfrac{n}{d}\right)\)

1.4.6.1.运算律

  1. 交换律:fg=gf。

  2. 结合律1:(fg)h=f(gh)=fgh。

    结合律2:(x·f)g=x·(fg)。

  3. 对函数加法的分配律:(f+g)h=fh+g*h。

  4. 单位元函数

    \(\varepsilon(n)=\begin{cases}1,&n=1,\\0,&\text{otherwise.}\end{cases}\\\)。对于任意的数论函数f,都有\(\varepsilon * f = f\)

    完全积性函数。

  5. 狄利克雷逆:狄利克雷卷积的逆元

    \(f*f^{-1}=\varepsilon\),则\(f^{-1}\)被称为\(f\)的狄利克雷逆。

    $f^{-1}(n)=\begin{cases} \dfrac{1}{f(1)}&n=1\

-\dfrac{1}{f(1)}\sum\limits_{d|n\text{ and }d\ne n}!f(\dfrac{n}{d})f^{-1}(d)&\text{otherwise.}\end{cases} $

递归求解。

狄利克雷逆\(f^{-1}\)存在的充要条件是\(f(1)\ne0\)

积性函数一定有狄利克雷逆,此时狄利克雷逆也是积性函数。

因此当$f(1)\ne0$并且$g(1)\ne0$时,有$(f*g)^{-1}=f^{-1}*g^{-1}$。

1.4.6.2.性质

  • 若f和g都是积性函数,那么f*g也是积性函数。

  • \(\textbf{1}^{-1}=\mu\)

    \(\sum\limits_{d|n} \mu(d)=\sum\limits_{d|n} \mu(d)*\textbf{1}(\frac{n}{d})=\mu * \textbf{1}=\varepsilon(n) =[n==1]\)

  • \(\text{Id}_{k}*\textbf{1}=\sigma_k\)

  • \(\textbf{1}*\textbf{1}=\textbf{d}\)

  • \(\varphi * \textbf{1}=\text{Id}\)

  • \(\varphi * \textbf{d}=\sigma\)

1.4.6.3.求狄利克雷卷积

1.4.6.3.1.求类狄利克雷卷积

适用条件:类狄利克雷卷积\(h(n)=\bigoplus\limits_{d|n} f(d)\otimes g(\frac{n}{d})\)

1.4.6.3.1.1.求N的函数值h\(O(\sqrt N)\)

根据定义直接枚举约数求解。复杂度是求约数。

1.4.6.3.1.2.求1~N每个数的函数值h\(O(N\ln N)\)

根据定义暴力卷积。复杂度是调和级数。

下面以求狄利克雷卷积为例:

for(int i=1;i<=n;i++)
    for(int j=1;1ll*i*j<=n;j++)
        h[i*j]+=f[i]*g[j];

1.4.6.3.2.狄利克雷前缀和

适用条件:狄利克雷前缀和\(sum_i=\sum\limits_{d|i} a_d\)

1.4.6.3.2.1.求N的狄利克雷前缀和\(O(\sqrt N)\)

根据定义直接枚举约数求解。复杂度是求约数。

1.4.6.3.2.2.求1~N每个数的狄利克雷前缀和\(O(N\log \log N)\)

可以\(O(N\ln N)\)暴力卷积求解,但是有更优的做法。

\(sum_{p^{k+1}}\leftarrow sum_{p^k}\)。实际上狄利克雷前缀和是质数进制下的高维前缀和。复杂度是埃筛。

for(int i=1;i<=n;i++) sum[i]=a[i];
    
//注意下面2个循环的顺序不可交换
for(auto it : prime)    //维度
    for(int i=1;i<=n;i++)
    {
        if(1ll*i*it>n) break;
        sum[i*it]+=sum[i];
    }

1.4.7.数论函数计算与莫比乌斯反演

https://www.luogu.com.cn/blog/An-Amazing-Blog/mu-bi-wu-si-fan-yan-ji-ge-ji-miao-di-dong-xi

适用条件:在涉及到所有约数或倍数,将真值表达式转化为数学公式,从而可以方便调整求和符号等变形,进一步将不好求的原问题转化为数论分块

莫比乌斯反演定理

核心:\(\textbf{1}^{-1}=\mu\)。若\(g=f*\textbf{1}\),则\(f=f*(\textbf{1}*\mu)=g*\mu\)

即若\(g(n)=\sum\limits_{d|n} f(d)\),则\(f(n)=\sum\limits_{d|n} (\mu (d)*g(\frac{n}{d}))\)

  • 代入法证明

    \(\sum\limits_{d|n} (\mu (d)*g(\frac{n}{d}))=\sum\limits_{d|n} \mu (d)*\sum\limits_{i|\frac{n}{d}}f(i)=\sum\limits_{i|n}f(i)*\sum\limits_{d|\frac{n}{i}}\mu(d)=\sum\limits_{i|n}f(i)*S(\frac{n}{i})=0+0+...+0+f(n)*1=f(n)\)

变形1:做题常用

\(g(n)=\sum\limits_{n|d} f(d)\),则\(f(n)=\sum\limits_{n|d} (\mu (\frac{d}{n})*g(n))\)

变形2:模数相关

\(n\bmod i=n-i*\lfloor\frac{n}{i}\rfloor\)

变形3:gcd相关

一般是把左边的表达式转化为右边的数学公式。少数情况下是把右边转化为左边,发现左边的表达式与运算符的结合具有特殊的实际含义。

gcd→真值表达式→数论函数。

由gcd的定义得:\(\gcd(i,j)=\sum\limits_{d=1}[\gcd(i,j)==d]d\)

\(\mu\)的性质\([n==1]=\sum\limits_{d|n}\mu(d)\)得:\([\gcd(i,j)==1]=\sum\limits_{d|\gcd(i,j)}\mu(d)\)

\(\varphi\)的性质\(n=\sum\limits_{d|n}\varphi(d)\)得:\(\gcd(i,j)=\sum\limits_{d|\gcd(i,j)}\varphi(d)\)(欧拉反演)。

别忘了\(\varphi\)的定义:\(\sum\limits_{j=1}^{i}[\gcd(i,j)==1]=\varphi(i)\)。(可以往这个方向转化)

\(\sum\limits_{x|i}\sum\limits_{y|j}[\gcd(x,y)==1]=\textbf{d}(i*j)\)

少数右边转化为左边的例子:

  1. \(\sum\limits_{i=1}^n\sum\limits_{d|\gcd(i,n)}\mu(d)\Rightarrow\sum\limits_{i=1}^n[\gcd(i,n)==1]\)表示[1,n]与n互质的数的个数,它等于\(\varphi(n)\)
  2. \(\sum\limits_{i=1}^{n}i[\gcd(i,n)==1]\)表示[1,n]中与n互质的数的和,它等于\(\begin{cases}1&n==1\\ \frac{n}{2}*\varphi(n)&\text{Otherwise.}\end{cases}\)

例题1

\(\sum\limits_{i=1}^n\sum\limits_{j=1}^n\gcd(i,j)^2\)

  • 推式子

    \(\sum\limits_{i=1}^n\sum\limits_{j=1}^n\gcd(i,j)^2\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^n\sum\limits_{d=1}^nd^2[\gcd(i,j)==d]\\=\sum\limits_{d=1}^nd^2\sum\limits_{i=1}^n\sum\limits_{j=1}^n[\gcd(i,j)==d]\\=\sum\limits_{d=1}^nd^2\sum\limits_{i'=1}^{\lfloor \frac{n}{d}\rfloor}\sum\limits_{j'=1}^{\lfloor \frac{n}{d}\rfloor}[\gcd(i',j')==1]\\=\sum\limits_{d=1}^nd^2((2\sum\limits_{i'=1}^{\lfloor \frac{n}{d}\rfloor}\sum\limits_{j'=1}^{i'}[\gcd(i',j')==1])-1)(减1是为了减去多计算的(1,1))\\=\sum\limits_{d=1}^nd^2((2\sum\limits_{i'=1}^{\lfloor \frac{n}{d}\rfloor}\varphi(i'))-1)\)

    线性筛\(\varphi\)并求其前缀和,\(O(N)\)解决。

技巧

  1. gcd可转化为\(\sum\limits_{d|gcd}\varphi(d)\)\(\sum\limits_{d=1}[\gcd==d]d\)。gcd→真值表达式→数论函数。
  2. 推式子中的“移项”:加法和乘法的运算律
  3. 同时除以k,从约数转化为倍数:\(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}[\gcd(i,j)==k]=\sum\limits_{i'=1}^{\lfloor\frac{n}{k}\rfloor}\sum\limits_{j'=1}^{\lfloor\frac{m}{k}\rfloor}[\gcd(i',j')==1]\)。**注意约数变倍数后,原来意义下的i要变成i'k。*
  4. 对称:算一半:\(\sum\limits_{i=1}^n\sum\limits_{j=1}^n(i,j)=(2*\sum\limits_{i=1}^n\sum\limits_{j=1}^i(i,j))-\sum\limits_{i=1}^n(i,i)\)
  5. 别忘了\(\varphi\)的定义:\(\sum\limits_{j=1}^{i}[\gcd(i,j)==1]=\varphi(i)\)。(可以往这个方向转化)

例题2

\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m[\gcd(i,j)==1]\)

  • 推式子

    不妨设n≤m。

    \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m[\gcd(i,j)==1]\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{d|\gcd(i,j)}\mu(d)\\=\sum\limits_{d=1}^{n}(\mu(d)*(\sum\limits_{i'=1}^{\lfloor\frac{n}{d}\rfloor}1)*(\sum\limits_{j'=1}^{\lfloor\frac{m}{d}\rfloor}1))(枚举d,约数变倍数)\\=\sum\limits_{d=1}^{n}(\mu(d)*\lfloor\frac{n}{d}\rfloor*\lfloor\frac{m}{d}\rfloor)\)

    • 本质过程

      \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m[\gcd(i,j)==1]\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{d|\gcd(i,j)}\mu(d)\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{d=1}^{n}[d|\gcd(i,j)]\mu(d)(枚举d)\\=\sum\limits_{d=1}^{n}\mu(d)\sum\limits_{i=1}^n\sum\limits_{j=1}^m[d|\gcd(i,j)]\\=\sum\limits_{d=1}^{n}\mu(d)\sum\limits_{i=1}^n\sum\limits_{j=1}^m[d|i\land d|j]\\=\sum\limits_{d=1}^{n}\mu(d)\sum\limits_{i=1}^n\sum\limits_{j=1}^m[d|i][d|j]\\=\sum\limits_{d=1}^{n}(\mu(d)*(\sum\limits_{i=1}^n[d|i])*(\sum\limits_{j=1}^m[d|j]))\\=\sum\limits_{d=1}^{n}(\mu(d)*\lfloor\frac{n}{d}\rfloor*\lfloor\frac{m}{d}\rfloor)\)

    线性筛\(\mu\)并求其前缀和,然后数论分块。

技巧:

  1. 一般情况可以设n≤m:若给定的n>m,思考交换n和m对答案是否有影响。

  2. 交换运算符号。(推式子的理论基础)

    核心:拆开i,j二元项,独立计算一元项以预处理或降低复杂度。

    《数学1.4.1.运算律》

  3. 枚举。(调整枚举顺序)

    核心:对于较难处理的部分x,可以在前面枚举它的值。由原式=f(d)*d的个数,把原式转化为枚举x+真值表达式(x的个数),再进一步处理。

    \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{d|\gcd(i,j)}\mu(d)\)通过枚举d可以转化为\(\sum\limits_{d=1}^{n}(\mu(d)*\lfloor\frac{n}{d}\rfloor*\lfloor\frac{m}{d}\rfloor)\)

    既可以通过例题的本质过程理解:\(\sum\limits_{i=1}^n\sum\limits_{d|i}=\sum\limits_{i=1}^n\textcolor{red}{\sum\limits_{d=1}^n[d|i]}=\sum\limits_{d=1}^n\sum\limits_{i=1}^n[d|i]=\sum\limits_{d=1}^n\lfloor\frac{n}{d}\rfloor\)

    也可以理解为:枚举d后,d是i,j的约数→枚举d的倍数i'd,j'd。原式=f(d)*d的个数,于是式子在f(d)后面考虑d的个数\(\sum\limits_{d=1}^{n}(\mu(d)*(\sum\limits_{i'=1}^{\lfloor\frac{n}{d}\rfloor}1)*(\sum\limits_{j'=1}^{\lfloor\frac{m}{d}\rfloor}1))\)

    *注意:1.调整枚举顺序后要把相应的式子提前;2.约数变倍数后,原来意义下的i要变成i'd;3.调整枚举顺序后的上界。

  4. \([d|\gcd(i,j)]=[d|i\land d|j]\)

  5. \([A\land B]=[A][B]\)

    \([A\lor B]=[A]+[B]-[A][B]\)

变式2.1

\(\sum\limits_{i=a}^b\sum\limits_{j=c}^d[\gcd(i,j)==1]\)

技巧:

  1. 有下界(或形成了区间)的问题:简单容斥思想(或前缀和思想)转化为一般问题。

    \(f(n,m)=\sum\limits_{i=1}^n\sum\limits_{j=1}^m[\gcd(i,j)==1]\)

    则原式=f(b,d)-f(a-1,d)-f(b,c-1)+f(a-1,c-1)。

变式2.2

\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m[\gcd(i,j)==k]\)

技巧:

  1. \(\lfloor\frac{\lfloor\frac{n}{x}\rfloor}{y}\rfloor=\lfloor\frac{n}{x*y}\rfloor\)
    • 证明

      \(\lfloor\frac{n}{xy}\rfloor\Leftrightarrow n=a*xy+b,b<xy\therefore \lfloor\frac{n}{xy}\rfloor=a\)

      \(\lfloor\frac{\lfloor\frac{n}{x}\rfloor}{y}\rfloor\Leftrightarrow n=c*x+d,d<x;c=e*y+f,f<y\therefore n=cexy+fx+d,\lfloor\frac{\lfloor\frac{n}{x}\rfloor}{y}\rfloor=ce\)

      \(\therefore fx+d<fx+x=f(x+1)x≤xy\)

      \(\therefore fx+d=b\)

      \(\therefore ce=a\)。证毕。

变式2.3

\(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}ij[\gcd(i,j)==k]\)

*注意:1.约数变倍数后,原来意义下的i要变成i'd;2.调整枚举顺序后的上界。

  • 推式子

    \(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}ij[\gcd(i,j)==k]\\=\sum\limits_{i'=1}^{\lfloor \frac{n}{k}\rfloor}\sum\limits_{j'=1}^{\lfloor \frac{m}{k}\rfloor}\textcolor{red}{i'kj'k} [\gcd(i',j')==1]\\=k^2*\sum\limits_{i'=1}^{\lfloor \frac{n}{k}\rfloor}\sum\limits_{j'=1}^{\lfloor \frac{m}{k}\rfloor}i'j'\sum\limits_{d|\gcd(i',j')}\mu(d)\\=k^2*\sum\limits_{d=1}^{\textcolor{red}{\lfloor\frac{n}{k}\rfloor}}\mu(d)\sum\limits_{i''=1}^{\lfloor \frac{n}{kd}\rfloor}\sum\limits_{j''=1}^{\lfloor \frac{m}{kd}\rfloor}\textcolor{red}{i''dj''d}\\=k^2*\sum\limits_{d=1}^{\lfloor\frac{n}{k}\rfloor}\mu(d)d^2\sum\limits_{i''=1}^{\lfloor \frac{n}{kd}\rfloor}i''\sum\limits_{j''=1}^{\lfloor \frac{m}{kd}\rfloor}j''\\\)

变式2.4

设P表示质数集合。求\(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^m[\gcd(i,j)\in P]\)

  • 推式子

    \(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^m[\gcd(i,j)\in P]\\=\sum\limits_{k=1,k\in P}^n\sum\limits\limits_{i=1}^n\sum\limits_{j=1}^m[\gcd(i,j)==k]\\=\sum\limits_{k=1,k\in P}^n\sum\limits\limits_{i=1}^{\lfloor\frac{n}{k}\rfloor}\sum\limits_{j=1}^{\lfloor\frac{m}{k}\rfloor}\sum\limits_{d|\gcd(i,j)}\mu(d)\\=\sum\limits_{k=1,k\in P}^n\sum\limits\limits_{d=1}^{\lfloor\frac{n}{k}\rfloor}\mu(d)\lfloor\frac{n}{dk}\rfloor\lfloor\frac{m}{dk}\rfloor\)

    此时复杂度是\(O(P*\sqrt N*\sqrt N)=O(PN)\)

    \(=\sum\limits_{k=1,k\in P}^n\sum\limits\limits_{d=1}^{\lfloor\frac{n}{k}\rfloor}\mu(d)\lfloor\frac{n}{T}\rfloor\lfloor\frac{m}{T}\rfloor(设T=dk)\\=\sum\limits_{T=1}^n\lfloor\frac{n}{T}\rfloor\lfloor\frac{m}{T}\rfloor\sum\limits_{k|T,k\in P}\mu(\frac{T}{k})(枚举T)\)

    预处理\(f(T)=\sum\limits_{k|T,k\in P}\mu(\frac{T}{k})\),然后数论分块。

技巧:

  1. 调整枚举顺序:对于较难处理的部分x,可以在前面枚举它的值。由原式=f(d)*d的个数,把原式转化为枚举x+真值表达式(x的个数),再进一步处理。

  2. 对于\(\sum\limits_{i=1}^n\sum\limits_{j=1}^{\lfloor\frac{n}{i}\rfloor}f(\lfloor\frac{n}{ij}\rfloor)g(i)h(j)\)类似的式子,

    \(T=ij≤i*\lfloor\frac{n}{i}\rfloor≤n\)\(=\sum\limits_{i=1}^n\sum\limits_{j=1}^{\lfloor\frac{n}{i}\rfloor}f(\lfloor\frac{n}{T}\rfloor)g(i)h(j)\)

    枚举T:\(=\sum\limits_{T=1}^nf(\lfloor\frac{n}{T}\rfloor)\sum\limits_{i|T}g(i)h(\frac{T}{i})\)。若i,j其中一个有其他限制条件,则第二个求和枚举有限制的。

  3. 不连续的值域无法数论分块,因此需要调整求和符号对连续的值域数论分块。

    \(e.g.\)\(\sum\limits_{n|d}\mu (\frac{d}{n})\):令\(d'=\frac{d}{n}\),则原式\(=\sum\limits_{d'\in \mathbb{N}_+}\mu (d')\),这样就把不连续的\(\mu(x)\)转化为连续的\(\mu(x)\)

  4. 分析复杂度:一次数论分块会把后面第一个求和的上界或式子中的\(\lfloor\frac{n}{d}\rfloor\)确定为定值。有几块区域的\(\lfloor\frac{n}{d}\rfloor\)就需要几次数论分块。

  5. 对于式子中的\(\sum\limits^{up}\)\(\sum\limits_{i|up}\)可以预处理\(f(up)\)

例题3

\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m\gcd(i,j)\)

  • 推式子

    \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m\gcd(i,j)\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{d|gcd(i,j)}\varphi(d)\\=\sum\limits_{d=1}^n\varphi(d)\lfloor\frac{n}{d}\rfloor\lfloor\frac{m}{d}\rfloor\)

\(\sum\limits_{i=1}^nlcm(i,n)\)

  • 推式子

    \(\sum\limits_{i=1}^nlcm(i,n)\\=\sum\limits_{i=1}^n\frac{i*n}{\gcd(i,n)}\\=\sum\limits_{\textcolor{red}{d|n}}\sum\limits_{i=1}^n\frac{i*n}{d}[\gcd(i,n)==d]\\=n*\sum\limits_{d|n}\sum\limits_{i=1}^{\frac{n}{d}}i[\gcd(i,\textcolor{red}{\frac{n}{d}})==1]\\=n*\sum\limits_{d|n}\sum\limits_{i=1}^{d}i[\gcd(i,d)==1]\\=n*\sum\limits_{d|n}\frac{d}{2}\varphi(d)\)

    用求狄利克雷卷积的方法求\(f(n)=\sum\limits_{d|n}\frac{d}{2}\varphi(d)\)

技巧:

  1. 若gcd在一般的位置,把gcd转化为\(\sum\limits_{d|gcd}\varphi(d)\)

    若gcd在特殊的位置,在前面枚举gcd,将原来意义下的gcd变成真值表达式\(\sum\limits_{d=1}[\gcd==d]d\)

  2. \(lcm(i,j)=\frac{i*j}{\gcd(i,j)}\)

  3. \(\sum\limits_{d|n}\frac{n}{d}=\sum\limits_{d|n}d\)

    \(\sum\limits_{d|n} f(d)*\sum\limits_{i|\frac{n}{d}}g(i)=\sum\limits_{i|n}g(i)*\sum\limits_{d|\frac{n}{i}}f(d)\)

  4. 一般情况会把表达式转化为数学公式。但是有些情况下表达式与运算符的结合具有特殊的实际含义。因此不要总是把表达式转化为数学公式而变得复杂,可以考虑实际含义而变得优秀。

变式3.1

\(\sum\limits_{i=1}^n\sum\limits_{j=i+1}^n\gcd(i,j)\)

技巧:

  1. \(\sum\limits_{i=1}^n\sum\limits_{j=i+1}^n\gcd(i,j)=\frac{1}{2}\sum\limits_{i=1}^n\sum\limits_{j=1,j\ne i}^n\gcd(i,j)\)。对于i≠j的限制条件,直接容斥减去i==j的情况即可。

变式3.2

给定一个序列\(\{a_n\},a_i\le W\),求\(\sum\limits_{i=1}^n\sum\limits_{j=1}^n\gcd(a_i,a_j)\)

  • 推式子

    \(\sum\limits_{i=1}^n\sum\limits_{j=1}^n\gcd(a_i,a_j)\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^n\sum\limits_{d|\gcd(a_i,a_j)}\varphi(d)\\=\sum\limits_{d=1}^{W}\varphi(d)\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}[d|\gcd(a_i,a_j)]\\=\sum\limits_{d=1}^{W}\varphi(d)(\sum\limits_{i=1}^{n}[d|a_i])(\sum\limits_{j=1}^{n}[d|a_j])\)

变式3.3

给定求\(\prod\limits_{i=1}^n\prod\limits_{j=1}^mf(\gcd(i,j))\)

  • 推式子

    \(\prod\limits_{i=1}^n\prod\limits_{j=1}^mf(\gcd(i,j))\\=\prod\limits_{d=1}^{n}f(d)^{\sum\limits_{i=1}^n\sum\limits_{j=1}^{m}[\gcd(i,j)==d]}(枚举d)\\=\prod\limits_{d=1}^nf(d)^{\sum\limits_{k=1}^{\lfloor\frac{n}{d}\rfloor}\mu(k)*\lfloor\frac{n}{dk}\rfloor*\lfloor\frac{m}{dk}\rfloor}\\=\prod\limits_{d=1}^nf(d)^{\sum\limits_{k=1}^{\lfloor\frac{n}{d}\rfloor}\mu(k)*\lfloor\frac{n}{T}\rfloor*\lfloor\frac{m}{T}\rfloor}(设T=dk)\\=\prod\limits_{T=1}^n\prod\limits_{d|T}f(d)^{\mu(\frac{T}{d})*\lfloor\frac{n}{T}\rfloor*\lfloor\frac{m}{T}\rfloor}(枚举T)\\=\prod\limits_{T=1}^n(\prod\limits_{d|T}f(d)^{\mu(\frac{T}{d})})^{\lfloor\frac{n}{T}\rfloor*\lfloor\frac{m}{T}\rfloor}\)

    用类狄利克雷卷积预处理\(f(T)=\prod\limits_{d|T}f(d)^{\mu(\frac{T}{d})}\)。然后数论分块。

变式3.4

给定一个序列\(\{a_n\}\),求\(\sum\limits_{i=1}^n\sum\limits_{j=1}^n\gcd(a_i,a_j)\gcd(i,j)\)

  • 推式子

    易得原式\(=\sum\limits_{x=1}^{n}\varphi(x)\color{red}{\sum\limits_{y=1}^{n}\varphi(y)\left(\sum\limits_{i=1}^{n}[x|i][y|a_i]\right)^2}\)

    很难继续化简,但是复杂度依然不对。

    对红色的式子使用增量法:

    首先枚举x,然后枚举i(x∣i),然后枚举\(a_i\)的约数y,使用一个re记录红色式子的值,并使用一个cnt(y)表示y出现了多少次。

    由增量\((x+1)^2-x^2=2*x+1\)得:每枚举到一个y就令res←res+(2×cnt(y)+1)×φ(y),然后令cnt(y)←cnt(y)+1。

    再加上预处理\(a_i\)的约数后,复杂度是\(O(N+N\log N+\sum\limits_{i=1}^N\sigma_0^{\,2}(i))=O(N\log^3N)\)

技巧:

  1. 当很难继续化简式子(\(e.g.\)重要部分套平方),但是复杂度依然不对时,可以采用增量法计算贡献:
    1. 确定枚举顺序。枚举哪个方便计算就枚举哪个。
    2. 考虑如何计算当前枚举的增量。

变式3.5

多组询问,给定n,m,k,求\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m f(\gcd(i,j))[f(\gcd(i,j))≤k]\)

技巧:

  1. 当遇到限制值的范围时,
    1. 先不管值的限制范围,化简式子。

      易得原式\(=\sum\limits_{T=1}^n\lfloor\frac{n}{T}\rfloor\lfloor\frac{m}{T}\rfloor\sum\limits_{d|T}f(d)\mu(\frac{T}{d})\)

      若没有限制,设\(g(T)=\sum\limits_{d|T}f(d)\mu(\frac{T}{d})\),则原式\(=\sum\limits_{T=1}^n\lfloor\frac{n}{T}\rfloor\lfloor\frac{m}{T}\rfloor g(T)\),预处理g(1~N),然后数论分块就做完了。

    2. 找到原式的限制范围在新式的限制范围。

      原式\(=\sum\limits_{T=1}^n\lfloor\frac{n}{T}\rfloor\lfloor\frac{m}{T}\rfloor\sum\limits_{d|T}f(d)\mu(\frac{T}{d})[d\le k]\)

      发现当f(d)≤k时,才会对g(T)产生贡献。

    3. 将询问离线,按k从小到大排序。枚举询问时,k变大会使得更多的f(d)对g(T)产生贡献。当一个f(d)要对g(T)产生贡献时,枚举d的倍数T=d*i,给每个g(T)加上\(f(d)*\mu(i)\)。因为需要单点修改+区间查询,所以使用树状数组维护g。总的时间复杂度是\((Q\sqrt N\log N+N\ln N\log N)\)

      代码链接。

例题4

\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m\textbf{d}(i*j)\)

  • 推式子

    \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m\textbf{d}(i*j)\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{x|i}\sum\limits_{y|j}[\gcd(x,y)==1]\\=\sum\limits_{i=1}^n\sum\limits_{j=1}^m\sum\limits_{x|i}\sum\limits_{y|j}\sum\limits_{d|\gcd(x,y)}\mu(d)\\=\sum\limits_{d=1}^n\mu(d)(\sum\limits_{x=1}^{\lfloor\frac{n}{d}\rfloor}\lfloor\frac{n}{dx}\rfloor)(\sum\limits_{y=1}^{\lfloor\frac{m}{d}\rfloor}\lfloor\frac{m}{dy}\rfloor)\)

    预处理\(f(n)=\sum\limits_{i=1}^n\lfloor\frac{n}{i}\rfloor\),然后数论分块。

应用

适用条件:在涉及到所有约数或倍数,题目的f(n)不好求时,利用莫比乌斯反演构造一个好求函数g,把\([真值表达式]\)转化为\(\sum\limits_{d|n}\),来求解f。

\(e.g.\)\(g(n)=\sum ... [n|\gcd(x,y)]\)限定条件是1~n,好求;\(f(n)=\sum ... [\gcd(x,y)==n]\)限定条件恰好是n,不好求。

  1. \(\sum\limits_{d|n}\)(求倍数和/约数和)要敏感:狄利克雷卷积。

  2. 将原问题转化为\(f(n)=\sum [\ ]\)的表达式。

  3. \(f=\textbf{1}*g\)(或\(f=\mu*g\)),从而构造\(g=\mu*f\)(或\(g=\textbf{1}*f\))。

  4. 思考g(n)如何求解,然后代回去求解f(n),从而把\([真值表达式]\)转化为\(\sum\limits_{d|n}\)。一般把g(n)往数论分块的方向求解。

    注意:如果题目要求\(f(n)\),就只求解把x=n代入g和f的表达式即可,不必求整个函数。

1.4.8.筛法

1.4.8.1.线性筛

适用条件:求1~N每个数的积性函数值。

  • 根据积性函数的定义,令f(1)=1。
  • 考虑对于一个质数x,根据定义怎么直接求f(x)。
  • 则对于一个合数x=i*prime[j],由于我们前面已经筛出了f(i),
    • 若prime[j]整除i,则考虑根据定义怎么求f(i*prime[j])。

      注意线性筛的重要性质:每个合数都只会被它的最小质因子筛掉。筛复杂的积性函数处理这一部分时,需要用到这一性质。

    • 若prime[j]不整除i,则f(iprime[j])=f(i)f(prime[j])。

线性筛较难处理的部分是\(x=i*prime[j],prime[j]|i\),故应先考虑如何处理这一部分,需不需要筛出额外的信息来辅助筛出积性函数值。

  • 例题:筛因数之和函数\(\sigma\)(因数个数函数\(\textbf{d}\)同理)

    因数之和公式:若\(N=p_1^{c_1}*...\),则\(\sigma(N)=(1+p_1+p_1^2+...+p_1^{c_1})*...\)

    先考虑如何处理\(x=i*prime[j],prime[j]|i\):设\(p_1,c_1\)表示x的最小质因子及其对应的指数,额外筛出\(f(x)=1+p_1+p_1^2+...+p_1^{c_1},g(x)=p_1^{c_1}\),即可辅助筛\(\sigma\)

    • \(\sigma(1)=1\)
    • 若x是质数,则根据公式,\(\sigma(x)=1+x,f(x)=1+x,g(x)=x\)
    • 若x是合数,x=i*prime[j],
      • 若prime[j]整除i,则根据公式,\(\sigma(x)=\sigma(i)/f(i)*(f(i)+g(i)*prime[j]),f(x)=f(i)+g(i)*prime[j],g(x)=g(i)*prime[j]\)
      • 若prime[j]不整除i,则\(\sigma(x)=\sigma(i)*\sigma(prime[j]),f(x)=1+prime[j],g(x)=prime[j]\)
struct Sigma
{
    LL s,f,g;
    bool operator < (const Sigma &qw) const
    {
        return s<qw.s;
    }
}sig[N];

void get_sigma()
{
    sig[1].s=1;
    for(int i=2;i<N;i++)
    {
        if(!vis[i])
        {
            sig[i].s=1+i,sig[i].f=1+i,sig[i].g=i;
            prime.push_back(i);
        }
        for(auto it : prime)
        {
            if(1ll*i*it>=N) break;
            vis[i*it]=true;
            if(i%it==0)
            {
                sig[i*it].s=sig[i].s/sig[i].f*(sig[i].f+sig[i].g*it),sig[i*it].f=sig[i].f+sig[i].g*it,sig[i*it].g=sig[i].g*it;
                break;
            }
            sig[i*it].s=sig[i].s*sig[it].s,sig[i*it].f=1+it,sig[i*it].g=it;
        }
    }
    return ;
}

1.4.8.2.杜教筛

适用条件:求解函数\(f\)前缀和\(S(n)=\sum\limits_{i=1}^n f(i)\)。能找到另一函数\(g\)使得能\(O(1)\)计算\((f*g)(i)\)的前缀和与\(g(i)\)的区间和。

\(\sum\limits_{i=1}^n (f*g)(i)=\sum\limits_{i=1}^n \sum\limits_{xy=i} f(x)g(y)=\sum\limits_{y=1}^n g(y)\sum\limits_{xy≤n}f(x)=\sum\limits_{y=1}^n g(y)S(\lfloor \frac{n}{y}\rfloor)=g(1)S(n)+\sum\limits_{y=2}^n g(y)S(\lfloor \frac{n}{y}\rfloor)\)

移项得:\(g(1)S(n)=\sum\limits_{i=1}^n (f*g)(i)-\sum\limits_{y=2}^n g(y)S(\lfloor \frac{n}{y}\rfloor)\)。若g是积性函数,则由g(1)=1得:\(S(n)=\sum\limits_{i=1}^n (f*g)(i)-\sum\limits_{y=2}^n g(y)S(\lfloor \frac{n}{y}\rfloor)\)

  1. 找到\(g\)

    把式子写成狄利克雷卷积的形式\((f_1\cdot f_2)*f_3\),而不是展开式\(\sum\limits_{d|x}f_1(x)f_2(x)f_3(\frac{x}{d})\)。狄利克雷卷积方便变形。

  2. 线性筛预处理\(S(1)\sim S(N^{\frac{2}{3}})\)

  3. 数论分块+递归计算\(S(n)\)。加之第一步预处理以及记忆化优化,时间复杂度为单次计算\(S(N)\)\(O(N^{\frac{2}{3}})\)

  4. 关于记忆化

    • 如果是单次计算\(S(n)\)

      由于递归中的n'一一对应\(\lfloor \frac{n}{n'} \rfloor\),而当\(n'≤N^{\frac{2}{3}}\)又已经被预处理过了,因此只需开大小为\(N^{\frac{1}{3}}\)的数组h记录\(h[\lfloor \frac{n}{n'} \rfloor]=S(n')\)

      • 证明递归中的n'一一对应\(\lfloor \frac{n}{n'} \rfloor\)

        递归中的n'一定满足\(n'=\lfloor\frac{n}{d}\rfloor\)。则\(\lfloor \frac{n}{n'} \rfloor=\lfloor \frac{n}{\lfloor \frac{n}{d} \rfloor} \rfloor\)。根据数论分块的知识:\(\lfloor \frac{n}{d} \rfloor\)\(\lfloor \frac{n}{\lfloor \frac{n}{d} \rfloor} \rfloor\)一一对应。

    • 如果是多次计算\(S(n)\)

      map记录\(h[n]=S(n)\)。注意多组数据不要清空map。复杂度多一个\(O(\log N)\)

注意:

  1. 根据积性函数的定义,\(\varphi(1)=\mu(1)=1\)。但是在某些题目背景中,\(\varphi(1),\mu(1)\)会有其他的定义(\(**e.g.**\)\(**\sum\limits_{i=1}^{n-1}[\gcd(i,n)==1]=\varphi(n)**\)中,\(**\varphi(1)=0**\)。)。必须在代码中定义\(**\varphi(1)=\mu(1)=1**\),杜教筛筛出了最终答案才减去\(**\varphi(1)=\mu(1)=1**\)带来的影响。
  2. 杜教筛筛****\(\sum\limits_{i=1}^n i*f(i)\),应该是杜教筛筛出\(\sum\limits_{i=1}^n g(i),g(i)=i*f(i)\),而不是只筛出\(sum_f(n)=\sum\limits_{i=1}^n f(i)\)然后把\(sum_f(n)\)****与\(**\sum\limits_{i=1}^n i**\)相乘。
const int N=INT_MAX,N1=pow(N,1.0/3)+10,N2=pow(N,2.0/3)+10;
int t,n;

//线性筛预处理S(1)~S(N^{\frac{2}{3}})
LL sum[N2];
vector<int> prime;
bool vis[N2];

//记忆化。注释的部分是单次计算S(n)的记忆化,另一个是多次计算S(n)的记忆化
// struct Memory
// {
//     LL val;
//     bool vis;
// }h[N1];
map<int,LL> h;

LL s(LL nn)
{
    if(nn<N2) return sum[nn];
    
    // if(h[n/nn].vis) return h[n/nn].val;
    // h[n/nn].vis=true;
    // LL &res=h[n/nn].val;
    if(h.count(nn)) return h[nn];
    LL &res=h[nn];
    
    res=sum_fg(nn);
    for(LL l=2,r;l<=nn;l=r+1)
    {
        r=min(nn,nn/(nn/l));
        res-=(sum_g(r)-sum_g(l-1))*s(nn/l);
    }
    res=res*inv(g(1));
    return res;
}

prework();  //线性筛预处理S(1)~S(N^{\frac{2}{3}})
scanf("%d",&t);
while(t--)
{
    //对于不同的n,需要清空Memory的记忆化,不需要清空map的记忆化
    // for(int i=0;i<N1;i++)
    // {
    //     h[i].val=0;
    //     h[i].vis=false;
    // }
    
    scanf("%d",&n);
    printf("%lld\n",s(n));
}

1.5.特殊数

1.5.1.斐波那契数列

\(F_n\)

  • 矩阵快速幂。
  • 通项公式:\(F_n=\frac{(\frac{1+\sqrt 5}{2})^n-(\frac{1-\sqrt5}{2})^n}{\sqrt 5}=\operatorname{round}(\frac{(\frac{1+\sqrt 5}{2})^n}{\sqrt 5})\)。对精度的要求极高,但是可能会结合二次剩余和逆元。
  • 快速倍增法:\(F_{2k}=F_k(2F_{k+1}-F_k),F_{2k+1}=F_{k+1}^2+F_k^2\)

性质

  1. 卡西尼性质:\(F_{n-1}F_{n+1}-F_n^2=(-1)^n\)
  2. 附加性质:\(F_{n+k}=F_kF_{n+1}+F_{k-1}F_n\)
  3. 性质2当k=n时:\(F_{2n}=F_n(F_{n+1}+F_{n-1})\)
  4. 性质3归纳证明:\(\forall k,F_n|F_{nk}\)
  5. 性质4可逆:\(\forall F_a|F_b,a|b\)
  6. GCD性质:\(\gcd(F_m,F_n)=F_{\gcd(m,n)}\)
  7. 皮萨诺周期:模m意义下斐波那契数列的最小正周期不超过6m,当且仅当m满足2*5^k的性质取等。

1.5.2.勾股数组

定义:满足\(a^2+b^2=c^2\)的三元正整数组(a,b,c)。

性质:a,b中必有一个3的倍数;a,b中必有一个4的倍数;a,b,c中必有一个5的倍数;ab是12的倍数;abc是60的倍数。

本原勾股数组

定义:满足gcd(a,b,c)==1的勾股数组。

性质:结构必为\(奇数^2+偶数^2=奇数^2\)

本原勾股数组定理:令本原勾股数组(a,b,c)中a为奇数,b为偶数。每个本原勾股数组(a,b,c)一一对应:

  • s,t,满足s>t且s,t互质且s,t是奇数,使得\(a=st,b=\frac{s^2-t^2}{2},c=\frac{s^2+t^2}{2}\)
  • m,n,满足m>n且m,n互质且m,n奇偶性不同,使得\(a=m^2-n^2,b=2mn,c=m^2+n^2\)

记d=gcd(a,b,c),则每个勾股数组(a,b,c)一一对应本原勾股数组(a/d,b/d,c/d)乘d,进而一一对应s,t和d,一一对应m,n和d。

勾股数组分解

给定正整数c,求所有满足\(c^2=a^2+b^2,a<b\)的正整数组(a,b)。

注:本原勾股数组分解不唯一。(e.g.\(65^2=16^2+63^2=33^2+56^2\)。)自然地,勾股数组分解也不唯一。

  • 求一个数c的本原勾股数组分解:根据勾股数组定理,枚举s,求出t。\(O(\sqrt C\log C)\)

  • 求1~c的本原勾股数组分解:根据勾股数组定理,枚举s,t,求出a,b,c。\(O(C\times\log C)\)

    推论:值域为c内的本原勾股数组的个数的上界为\(\frac{C}{4}\)

  • 求一个数c的勾股数组分解:枚举c的约数,对每个约数求其本原勾股数组分解,转移到c。

  • 求n个数c的勾股数组分解:先求1~c的本原勾股数组分解,再对每个数c借助“求一个数的约数”转移。\(O(C\log C+N\times\max(\sqrt C,勾股数组的个数))\)

  • 求1c的勾股数组分解:先求1c的本原勾股数组分解,再借助“求1~c的约数”转移。\(O(C\log C+\max(C\ln C,勾股数组的个数))\)

vector<pii> ppt[C];//ppt[c]:数值c的本原勾股数组分解{a,b}
vector<pii> pt[N];//pt[i]:第i个数c_i的勾股数组分解{a_i,b_i}

//求1~c的本原勾股数组分解
void get_ppt()
{
    for(int s=1;(s*s+1*1)/2<C;s+=2)
        for(int t=1;t<s && (s*s+t*t)/2<C;t+=2)
            if(gcd(s,t)==1)
                if(s*t<(s*s-t*t)/2) ppt[(s*s+t*t)/2].push_back({s*t,(s*s-t*t)/2});
                else ppt[(s*s+t*t)/2].push_back({(s*s-t*t)/2,s*t});
    return ;
}

get_ppt();

//求n个数c的勾股数组分解
for(int i=1;i<=n;i++)
{
    int c=read();
    for(int d=1;d*d<=c;d++)
        if(c%d==0)
        {
            for(auto it : ppt[d]) pt[i].push_back({it.first*(c/d),it.second*(c/d)});
            if(c/d!=d)
                for(auto it : ppt[c/d])
                    pt[i].push_back({it.first*d,it.second*d});
        }
}

勾股数组分解方案数

补充。

给定一个正整数c,求所有满足\(c^2=a^2+b^2,a<b\)的正整数组(a,b)的个数\(f_1(c)\)

下记p为满足p%41的质数,q为满足q%43的质数(即高斯质数)。

引理:给定一个正整数x,记质因数分解\(x=2^{u}\prod p_i^{v_i}\prod q_j^{w_j}\),则所有满足\(x=a^2+b^2,a>0,b\geqslant0\)的整数组(a,b)的个数\(f_2(x)\)

  • \(f_2(x)=\prod(v_i+1)\prod[w_j\%2==0]\)\(O(\sqrt X)\)

  • 定义积性函数\(\chi(x)=\begin{cases}1&x\equiv1\pmod 4\\-1&x\equiv3\pmod 4\\0&x\equiv0\pmod 2\end{cases}\)

    \(f_2(x)=\sum\limits_{d|x}\chi(d)\)

记质因数分解\(c=2^{u}\prod p_i^{v_i}\prod q_j^{w_j}\),则\(c^2=2^{2u}\prod p_i^{2v_i}\prod q_j^{2w_j}\),则\(f_1(c)=\frac{f_2(c^2)-1}{2}=\frac{(\prod(2v_i+1))-1}{2}\)\(O(\sqrt C)\)

应用:平面上的圆的整点问题

注:在勾股数组的基础上,还需补充\(c^2=0^2+c^2\)

  • 求一个中心为原点半径为正整数r的圆上的整点数:给定一个正整数r,求所有满足\(r^2=a^2+b^2\)的整数组(a,b)的个数\(f_3(r)\)

    引理:给定一个正整数x,所有满足\(x=a^2+b^2\)的整数组(a,b)的个数\(f_4(x)=4f_2(x)\)

    记质因数分解\(r=2^{u}\prod p_i^{v_i}\prod q_j^{w_j}\),则\(r^2=2^{2u}\prod p_i^{2v_i}\prod q_j^{2w_j}\),则\(f_3(r)=f_4(r^2)=4f_2(r^2)=4\prod(2v_i+1)\)\(O(\sqrt R)\)

  • 一个中心为原点半径为正整数r的圆内(含圆上)的整点数\(f_5(r)=\sum\limits_{c=1}^{r^2}f_3(\sqrt c)=\sum\limits_{c=1}^{r^2}f_4(c)=4\sum\limits_{c=1}^{r^2}f_2(c)=4\sum\limits_{c=1}^{r^2}\sum\limits_{d|c}\chi(d)\)

  • 求一个中心为原点半径为有理数r的圆上的有理点:给定一个有理数r,求\(a^2+b^2=r^2\)的有理解(a,b)\(\Rightarrow\)令x=a/r,y=b/r,求\(x^2+y^2=1\)的有理解:\(x=\frac{m^2-1}{m^2+1},y=\frac{-2m}{m^2+1},m\in \mathbb{Q}\)

    • 证明

      显然点(1,0)是一个解,过点(1,0)作直线y=mx-m,与圆x2+y2=1相交,直线的方程和圆的方程联立求解。

所以若\(\gcd\left(\delta_m\left(g\right),k\right)=1\)时,则有\(\delta_m\left(g^k\right)=\varphi\left(m\right)\),即\(g^k\bmod m\)也是模\(m\)的原根。

而满足\(\gcd\left(\varphi\left(m\right),k\right)=1\)\(1\le k\le\varphi\left(m\right)\)\(k\)\(\varphi\left(\varphi\left(m\right)\right)\)个,所以原根就有\(\varphi\left(\varphi\left(m\right)\right)\)个。


证明上面找到的\(\varphi\left(\varphi\left(m\right)\right)\)个原根不重不漏:

设g是m的最小原根。

由阶的性质1:\(g,g^1,g^2,\cdots,g^{\varphi(m)}\)\(m\)两两不同余且不为0,“不重”证毕。同时也证明了\(g,g^1,g^2,\cdots,g^{\varphi(m)}\)模m的值与m互质,而与m互质的数只有\(\varphi(m)\)个,故m所有可能的原根集合等于\(\{g,g^1,g^2,\cdots,g^{\varphi(m)}\}\)。而上面找的过程已经准确判断了\(g^i\)是不是原根,“不漏”证毕。

由于\(\gcd\left(t,\varphi\left(m\right)\right)\mid\varphi\left(m\right)\),且\(\gcd\left(t,\varphi\left(m\right)\right)\le t<\varphi\left(m\right)\),故存在\(\varphi\left(m\right)\)的质因数\(p\)使得\(\gcd\left(t,\varphi\left(m\right)\right)\mid\frac{\varphi\left(m\right)}{p}\)

则有\(a^{\frac{\varphi\left(m\right)}{p}}\equiv a^{\gcd\left(t,\varphi\left(m\right)\right)}\equiv1\pmod m\),矛盾,假设不成立。

得证。

\(r<\delta_m\left(a\right)\),这与阶的最小性矛盾,因此\(r=0\),进一步有\(\delta_m\left(a\right)\mid n\)成立。

推论:若\(a^p\equiv a^q\pmod m\),则有\(p\equiv q\pmod{\delta_m\left(a\right)}\)

2.线性代数

2.1.矩阵

struct Matrix
{
    int mat[N][N];
    Matrix(){memset(a,0,sizeof a);}//别忘记初始化为0!!!
};

2.1.1.矩阵定义及线性运算

  1. 矩阵加减法\(C_{n,m}=A_{n,m} \pm B_{n,m} \Leftrightarrow \forall i \in [1,n],j\in [1,m],有C_{i,j}=A_{i,j}\pm B_{i,j}\)
    矩阵加减法满足交换律和结合律,即:\(A\pm B=B\pm A\\(A\pm B)\pm C=A\pm (B\pm C)\)
  2. 矩阵数乘\(\lambda\begin{bmatrix}a_{1,1}&\cdots&a_{1,c}\\\vdots&\ddots&\vdots\\a_{r,1}&\cdots&a_{r,c}\end{bmatrix}=\begin{bmatrix}\lambda a_{1,1}&\cdots&\lambda a_{1,c}\\\vdots&\ddots&\vdots\\\lambda a_{r,1}&\cdots&\lambda a_{r,c}\end{bmatrix}\)
    运算律:\((\lambda\mu)A=\lambda(\mu A)\\(\lambda+\mu)A=\lambda A+\mu A\\\lambda(A+B)=\lambda A+\lambda B\)
  • 拓展:一种非线性运算:矩阵的转置

    把矩阵A的行和列互相交换所产生的矩阵称为A的转置矩阵,记作\(A^{T}\),这一过程称为矩阵的转置。

    \(e.g.\)\(\begin{bmatrix}a&b&c\\d&e&f\end{bmatrix}^T=\begin{bmatrix}a&d\\b&e\\c&f\end{bmatrix}\)

    运算律:\((A^T)^T=A\\(\lambda A)^T=\lambda(A^T)\\(AB)^T=B^TA^T\)

2.1.2.矩阵乘法

理解

列向量的方式:左边是“原料”,右边是“配比菜谱”:\(\begin{bmatrix} \overrightarrow{u_1} & \overrightarrow{u_2} \end{bmatrix} * \begin{bmatrix} x & a \\ y & b \end{bmatrix}=\begin{bmatrix} x*\overrightarrow{u_1}+y*\overrightarrow{u_2} & a*\overrightarrow{u_1}+b*\overrightarrow{u_2} \end{bmatrix}\)

行向量的方式:左边是“配比菜谱”,右边是“原料”:\(\begin{bmatrix} x & y \end{bmatrix} * \begin{bmatrix} \overrightarrow{u_1} \\ \overrightarrow{u_2} \end{bmatrix}=\begin{bmatrix} x*\overrightarrow{u_1}+y*\overrightarrow{u_2}\end{bmatrix}\)

2.1.2.1.定义、运算及运算律

矩阵乘法\(C_{n,p}=A_{n,m} * B_{m,p} \Leftrightarrow \forall i \in [1,n],j\in [1,p],有C_{i,j}=\sum\limits_{k=1}^{m} A_{i,k} * B_{k,j}\)

\(e.g.\)\(A_{2,3}*B_{3,5}=C_{2,5}\)

\(\begin{bmatrix}a&b&c\\d&e&f\end{bmatrix}*\begin{bmatrix}g&h&i&j&k\\l&m&n&o&p\\q&r&s&t&u\end{bmatrix}=\begin{bmatrix}ag+bl+cq&ah+bm+cr&ai+bn+cs&aj+bo+ct&ak+bp+cu\\dg+el+fq&dh+em+fr&di+en+fs&dj+eo+ft&dk+ep+fu\end{bmatrix}\)

int res[N][P];
void mul(int f[N][M],int a[M][P])
{
    memset(res,0,sizeof res);
    for(int i=0;i<N;i++)
        for(int j=0;j<P;j++)
            for(int k=0;k<M;k++)
                res[i][j]=(res[i][j]+f[i][k]*a[k][j])%MOD;
    return ;
}

矩阵乘法满足结合律(\((A*B)*C=A*(B*C)\))、分配律(\((A+B)*C=A*C+B*C\))。不满足交换律。

2.1.2.2.拓展

2.1.2.2.1.向量递推

特别地,如果F是1n的矩阵,A是nn的矩阵(称为n阶方阵),则F'=FA也是1n矩阵。F和F'可以看作一维数组。

注意,若向量F是nn的矩阵,可以通过状压成一维数组f[NN]:

//压缩
int sets(int x,int y){
    return (x-1)*m+y;
}

//还原
PII get(int state)
{
    int x=(state-1)/m+1,y=(state-1)%m+1;//注意是state-1
    return {x,y};
}

int state=sets(x,y);//压缩

//还原
PII t=get(state);
x=t.first,y=t.second;

\(e.g.\)\(A_{1,3}*B_{3,3}=C_{1,3}\)

\(\begin{bmatrix}a&b&c\end{bmatrix}*\begin{bmatrix}d&e&f\\g&h&i\\j&k&l\end{bmatrix}=\begin{bmatrix}ad+bg+cj&ae+bh+ck&af+bi+cl\end{bmatrix}\)

F和F'看作向量,A看作系数配平,则矩阵乘法运算后,F数组的第k个值会以\(A_{k,j}\)(仅针对\(F_k\))为系数累加到F'数组的第j个值上(当\(A_{k,j}=0\)时相当于\(F_k\)\({F'}_j\)无影响)\(\Leftrightarrow\)在两个向量F和F'之间发生递推:若F的第k个数对下一时间单位的F'的第j个数产生影响,就把A的第k行第j列赋予恰当的系数(系数为0则表示无影响)。

2.1.2.2.2.矩阵快速幂

对于向量*矩阵,复杂度\(O(N^2)\)

对于向量*(矩阵)^k,

  • 当k≤N时,直接执行k次向量*矩阵,复杂度\(O(N^2k)\)
  • 当k>N时,采用下面的矩阵快速幂,复杂度\(O(N^3\log k)\)
void mul1(int f[N][M],int a[M][M])
{
    int res[N][M];
    memset(res,0,sizeof res);//先mem——set!!!
    for(int k=0;k<M;k++)
        for(int i=0;i<N;i++)
            for(int j=0;j<M;j++)
                res[i][j]=(res[i][j]+f[i][k]*a[k][j])%MOD;
    memcpy(f,res,sizeof res);//再mem——cpy!!!注意f是指针,sizeof应参考数组res!!!
    return ;
}

void mul2(int a[M][M])
{
    int res[M][M];
    memset(res,0,sizeof res);
    for(int k=0;k<M;k++)
        for(int i=0;i<M;i++)
            for(int j=0;j<M;j++)
                res[i][j]=(res[i][j]+a[i][k]*a[k][j])%MOD;
    memcpy(a,res,sizeof res);
    return ;
}

void qpow(int b)
{
    while(b)
    {
        if(b&1) mul1(f,a);
        mul2(a);
        b>>=1;
    }
    return ;
}

2.1.2.3.应用:加速递推

由2.1.2.2.《拓展》得:

若一类问题具有以下特点:

  1. 可以抽象为一个长度为n的一维向量,该向量在每个单位时间发生一次变化;
  2. 变化的形式是一个线性递推(只有若干个“加法”和“数乘”运算);
  3. 递推式每个时间可能作用于不同的数据上,但本身保持不变;
  4. 递推的轮数很大,但向量长度n不大。

则可以考虑用矩阵乘法优化:将长度为n的一维向量定义为“状态矩阵”F,把与F相乘而本身固定不变的n阶方阵成为“转移矩阵”A。若F的第x个数对下一时间单位的F'的第y个数产生影响,就把A的第x行第y列赋予恰当的系数(系数为0则表示无影响)。

用矩阵快速幂进行运算。

关键在于定义出“状态矩阵”,并根据递推式构造正确的“转移矩阵”。

注意省略第一维时间t,快速幂t次方即可得到答案。

  • 设计矩阵实战

    设状态转移方程为\(f[i]=f[i-1]+f[i-2]+2*\sum\limits_{i=0}^{n-3}g[i]\),特殊地,\(f[0]=0,f[1]=0,f[2]=0\)。其中\(g[i]=g[i-1]+g[i-2]\)

    f的转移看上去似乎是双重矩阵,不用担心,实际上仍然是一个矩阵,只不过同时更新第一重变量和第二重变量 。设计向量矩阵:

f[5]:{f_n,f_n-1,g_sum_n-2,g_n-1,g_n-2}
f_3={2,0,2,2,1};//初始,当f=3时

设计系数矩阵时,在第一行写上转移后f_n的状态:f_n={f_n,f_n-1,g_sum_n-2,g_n-1,g_n-2},第一列写上转移前f_n-1(令n=n-1)的状态:f_n-1={f_n-1,f_n-2 ,g_sum_n-3,g_n-2,g_n-3 }。

第i列的每一行的状态(转移前f_n-1)乘上系数等于这一列的状态(转移后f_n)。按照这个思路,根据f_n的第i列状态由f_n-1的哪些行状态转移而来,来填写下面的系数矩阵:

f_n=f_n-1+f_n-2+2*g_sum_n-3
f_n-1=f_n-1
g_sum_n-2=g_sum_n-3+g_n-2
g_n-1=g_n-2+g_n-3
g_n-2=g_n-2

a[5][5]:
            f_n   f_n-1   g_sum_n-2   g_n-1   g_n-2
f_n-1       1     1       0           0       0
f_n-2       1     0       0           0       0
g_sum_n-3   2     0       1           0       0
g_n-2       0     0       1           1       1
g_n-3       0     0       0           1       0

2.1.2.3.1.斐波那契

构造方法:在A中用F表示F':

$\begin{aligned}A_{n+1}
&=\begin{bmatrix}Fib_n&Fib_{n+1}\0&0\end{bmatrix}\
&=\begin{bmatrix}Fib_n&Fib_{n-1}+Fib_{n}\0&0\end{bmatrix}\
&=\begin{bmatrix}Fib_{n-1}\times 0+Fib_n\times1&Fib_{n-1}\times 1+Fib_{n}\times 1\0\times0+0\times 1&0\times 1+0\times 1\end{bmatrix}\
&=\begin{bmatrix}Fib_{n-1}&Fib_{n}\0&0\end{bmatrix}\begin{bmatrix}0&1\1&1\end{bmatrix}\&=A_n\begin{bmatrix}0&1\1&1\end{bmatrix}\end{aligned} $

\(\begin{bmatrix}Fib_{n-1}&Fib_{n-2}\end{bmatrix}\begin{bmatrix}1&1\\1&0\end{bmatrix}=\begin{bmatrix}Fib_{n}&Fib_{n-1}\end{bmatrix}\)

void mul1(int f[2],int a[2][2])
{
    int res[2];
    memset(res,0,sizeof res);
    for(int j=0;j<2;j++)
        for(int k=0;k<2;k++)
            res[j]=(res[j]+f[k]*a[k][j])%MOD;
    memcpy(f,res,sizeof res);
    return ;
}

void mul2(int a[2][2])
{
    int res[2][2];
    memset(res,0,sizeof res);
    for(int k=0;k<2;k++)
        for(int i=0;i<2;i++)
            for(int j=0;j<2;j++)
                res[i][j]=(res[i][j]+a[i][k]*a[k][j])%MOD;
    memcpy(a,res,sizeof res);
    return ;
}

void qpow(int b)
{
    int f[2]={0,1};
    int a[2][2]=
    {
        {0,1},
        {1,1}
    };
    while(b)
    {
        if(b&1) mul1(f,a);
        mul2(a);
        b>>=1;
    }
    printf("%d\n",f[0]);
    return ;
}

scanf("%d",&n),qpow(n);

2.1.2.4.广义矩阵乘法

《动态规划8.6.动态dp》

2.1.3.方阵的行列式

注意:行列式是针对于方阵的。

理解

\(\det A =|A|=\begin{vmatrix} a & b \\ c & d \end{vmatrix} = \begin{vmatrix} \overrightarrow{u_1} & \overrightarrow{u_2} \end{vmatrix}=a*d-b*c\)。也就是说,一个n价方阵的行列式相当于n维空间n个列向量的叉积(二维:两个列向量所形成的平行四边形的面积,此内容可以配合下文的性质的理解)。

性质

  1. 单位矩阵\(I=\begin{bmatrix} 1 & 0 & \cdots \\ 0 & 1 & \cdots \\ \cdots & \cdots & \cdots \end{bmatrix}\),|I|=1。

  2. \(|A|=|A^T|\)

    因此下文的行的性质对列也适用。

  3. 若方阵A交换任意2个行得到A',则|A'|=-|A|:\(\begin{vmatrix} a & b \\ c & d \end{vmatrix}=-\begin{vmatrix} c & d \\ a & b \end{vmatrix}\)

  4. 部分线性

    1. 提出一行的系数:\(\begin{vmatrix} t*a & t*b \\ c & d \end{vmatrix}=t*\begin{vmatrix} a & b \\ c & d \end{vmatrix}\)

      \(|t*A|=t^n*|A|\)

    2. 两个行列式只有一行不同时,可相加:\(\begin{vmatrix} a & b \\ c & d \end{vmatrix}+\begin{vmatrix} e & f \\ c & d \end{vmatrix}=\begin{vmatrix} a+e & b+f \\ c & d \end{vmatrix}\)

  5. 一行可以加上另一行的倍数:\(\begin{vmatrix} a & b \\ c & d \end{vmatrix}=\begin{vmatrix} a+k*c & b+k*d \\ c & d \end{vmatrix}\)

  6. \(\begin{vmatrix} a & b \\ 0 & 0 \end{vmatrix}=0\)

  7. 上三角方阵\(U=\begin{bmatrix} d_1 & x_1 & \cdots \\ 0 & d_2 & \cdots \\ \cdots & \cdots & \cdots \end{bmatrix}\)\(|U|=\prod\limits_{i=1}^{n}d_i\)

    • 证明

      \(|U|\xlongequal{性质4.1} (\prod\limits_{i=1}^{n}d_i)\begin{bmatrix} 1 & \frac{x_1}{d_1} & \cdots \\ 0 & 1 & \cdots \\ \cdots & \cdots & \cdots \end{bmatrix}\xlongequal{性质5} (\prod\limits_{i=1}^{n} d_i)\begin{bmatrix} 1 & 0 & \cdots \\ 0 & 1 & \cdots \\ \cdots & \cdots & \cdots \end{bmatrix}\xlongequal{性质1} \prod\limits_{i=1}^{n} d_i\)

  8. 奇异方阵(方阵中至少有两个向量线性相关)\(A=\begin{vmatrix} a & b \\ k*a & k*b \end{vmatrix}\),|A|=0。

    • 证明

      \(|A|\xlongequal{性质5}\begin{vmatrix} a & b \\ 0 & 0 \end{vmatrix}\xlongequal{性质6} 0\)

  9. |AB|=|A||B|。

2.1.4.矩阵的秩

矩阵的秩=行秩=列秩。

2.2.线性方程组

2.2.1.初等矩阵变换

  1. 把某一行乘一个非零的数;
  2. 交换某两行;
  3. 把某行的若干倍加到另一行上去。

2.2.2.高斯消元解m×n线性方程组\(O(mn^2)\)

\(\begin{cases}a_{11}x_1+\cdots+a_{1n}x_n=b_1\\\vdots\\a_{m1}x_1+\cdots+a_{mn}x_n=b_m\end{cases}\Rightarrow [A,b]=A'_{m×(n+1)}\)

高斯消元化\(A'_{m×(n+1)}\)的A为行最简形,得到\(A''_{m×(n+1)}\)

h和l都从1开始。借助循环依次处理第1~n列。对于第l列:
1. 在第h~m行中,找到第l列系数的绝对值最大的一行\(h_{max}\)

    如果该系数不为0,记录$key_h=l$。

    否则,第h~m行的第l列全是0,结束本轮循环,不要执行循环底部的h++,只执行l++处理完第l列。
2. 将第$h_{max}$行与第h行交换。(交换某两行)
3. 将交换后的第h行的第一个有效数字(第l列)变成1。(把某一行乘一个非零的数)
4. 将上下除第h行外的所有行的第l列清成0。(把某行的若干倍加到另一行上去)
5. 执行循环底部的h++。

    执行后如果h>m,跳出循环;否则执行l++处理完第l列。

r(A)=h-1。

判断解的个数:

  • 有解\(\Leftrightarrow\)r(A)==r([A,b])\(\Leftrightarrow\)\(\forall i\in[r(A)+1,m]\cap\mathbb{Z},A''_{i,n+1}==0\)
    • 有唯一解\(\Leftrightarrow\)r(A)r([A,b])n。
    • 否则有无穷多解。
  • 否则无解。

求出一个合法解:

\(x_{key_i}=A''_{i,n+1},i\in[1,r(A)]\cap\mathbb{Z}\),其余的即为自由变量\(x_j=0\)

若要求出其他的合法解,则令自由变量\(x_j\)取任意常数,代入方程求出\(x_{key_i}\)

//i:m;j:n
int m,n;
double a[M][N],x[N];
int ra,key[M];

int gauss()
{
    //高斯消元化A'_{m×(n+1)}的A为行最简形,得到A''_{m×(n+1)}
    int h,l;
    for(h=1,l=1;l<=n;l++)   //h和l都从1开始。借助循环依次处理第1~n列
    {
        //在第h~m行中,找到第l列系数的绝对值最大的一行h_max
        int h_max=h;
        for(int i=h+1;i<=m;i++)
            if(dcmp(fabs(a[i][l]),fabs(a[h_max][l]))>0)
                h_max=i;
        if(dcmp(fabs(a[h_max][l]),0)>0) key[h]=l;   //如果该系数不为0,记录key_h=l
        else continue;  //否则,第h~m行的第l列全是0,结束本轮循环,不要执行循环底部的h++,只执行l++处理完第l列
        
        //将第h_max行与第h行交换。(交换某两行)
        for(int j=l;j<=n+1;j++) swap(a[h][j],a[h_max][j]);
        
        //将交换后的第h行的第一个有效数字(第l列)变成1。(把某一行乘一个非零的数)
        for(int j=n+1;j>=l;j--) a[h][j]/=a[h][l];
        
        //将上下除第h行外的所有行的第l列清成0。(把某行的若干倍加到另一行上去)
        for(int i=1;i<=m;i++)
        {
            if(i==h) continue;
            for(int j=n+1;j>=l;j--) //倒序是因为要用到a[i][l]被清成0之前的值,到l为止是因为a[1~l-1]已经被上面清成0了
                a[i][j]-=a[h][j]*a[i][l];   //把某行的若干倍加到另一行上去,达到下面所有行的第l列清成0的目的
                //思维:此时可以把外层循环i看作定值,且a[h][l]=1
        }
        
        //执行循环底部的h++
        h++;
        if(h>m) break;  //执行后如果h>m,跳出循环;否则执行l++处理完第l列
    }
    
    ra=h-1;
    
    //判断解的个数
    for(int i=ra+1;i<=m;i++)
        if(dcmp(a[i][n+1],0)!=0)
            return 0;
    if(ra!=n) return 2;
    return 1;
}

int cnt=gauss();
if(cnt==0) puts("No solution");
else
{
    if(cnt==1) puts("Unique solution");
    else puts("Infinite group solutions");
    
    //求出一个合法解
    for(int j=1;j<=n;j++) x[j]=0;
    for(int i=1;i<=ra;i++) x[key[i]]=a[i][n+1];
    for(int j=1;j<=n;j++) printf("%.2lf\n",x[j]);
}

2.2.3.高斯消元解m×n线性异或方程组\(O(mn^2)\)

\(\begin{cases}a_{11}x_1\operatorname{xor}\cdots\operatorname{xor}a_{1n}x_n=b_1\\\vdots\\a_{m1}x_1\operatorname{xor}\cdots\operatorname{xor}a_{mn}x_n=b_m\end{cases}\Rightarrow[A,b]=A'_{m×(n+1)}\\a_{ij}\in\{0,1\}\)

特别地,\(\begin{cases}(a_{11}\operatorname{and}x_1)\operatorname{xor}\cdots\operatorname{xor}(a_{1n}\operatorname{and}x_n)=b_1\\\vdots\\(a_{m1}\operatorname{and}x_1)\operatorname{xor}\cdots\operatorname{xor}(a_{mn}\operatorname{and}x_n)=b_m\end{cases}\Rightarrow[A,b]=A'_{m×(n+1)}\\a_{ij},x_i,b_i\in\{0,1\}\)是01线性异或方程组。

异或运算\(\operatorname{xor}\)\(\Leftrightarrow\)不进位加法。

与运算\(\operatorname{and}\)\(\Leftrightarrow\)模2意义下乘法。

高斯消元化\(A'_{m×(n+1)}\)的A为行最简形,得到\(A''_{m×(n+1)}\)

h和l都从1开始。借助循环依次处理第1~n列。对于第l列:
1. 在第h~m行中,找到第l列系数为1的一行h1。

    如果存在该系数为1,记录$key_h=l$。

    否则,第h~m行的第l列全是0,结束本轮循环,不要执行循环底部的h++,只执行l++处理完第l列。
2. 将第h1行与第h行交换。(交换某两行)
3. 将上下除第h行外的所有行的第l列清成0。(把某行异或到另一行上去)
4. 执行循环底部的h++。

    执行后如果h>m,跳出循环;否则执行l++处理完第l列。

r(A)=h-1。

判断解的个数:

  • 有解\(\Leftrightarrow\)r(A)==r([A,b])\(\Leftrightarrow\)\(\forall i\in[r(A)+1,m]\cap\mathbb{Z},A''_{i,n+1}==0\)
    • 线性异或方程组

      • 有唯一解\(\Leftrightarrow\)r(A)r([A,b])n。
      • 否则有无穷多解。
    • 01线性异或方程组

      解的个数为\(2^{n-r(A)}\)

  • 否则无解。

求出一个合法解:

\(x_{key_i}=A''_{i,n+1},i\in[1,r(A)]\cap\mathbb{Z}\),其余的即为自由变量\(x_j=0\)

若要求出其他的合法解,则令自由变量\(x_j\)取任意常数,代入方程求出\(x_{key_i}\)

//i:m;j:n
int m,n;
int a[M][N],x[N];
int ra,key[M];

int gauss()
{
    //高斯消元化A'_{m×(n+1)}的A为行最简形,得到A''_{m×(n+1)}
    int h,l;
    for(h=1,l=1;l<=n;l++)   //h和l都从1开始。借助循环依次处理第1~n列
    {
        //在第h~m行中,找到第l列系数为1的一行h1
        int h1=h;
        for(int i=h+1;i<=m;i++)
            if(a[i][l]==1)
            {
                h1=i;
                break;
            }
        if(a[h1][l]==1) key[h]=l;   //如果存在该系数为1,记录key_h=l
        else continue;  //否则,第h~m行的第l列全是0,结束本轮循环,不要执行循环底部的h++,只执行l++处理完第l列
        
        //将第h1行与第h行交换。(交换某两行)
        for(int j=l;j<=n+1;j++) swap(a[h][j],a[h1][j]);

        //将上下除第h行外的所有行的第l列清成0。(把某行异或到另一行上去)
        for(int i=1;i<=m;i++)
        {
            if(i==h || !a[i][l]) continue;
            for(int j=n+1;j>=l;j--) //这里可倒序可正序
                a[i][j]^=a[h][j];   //把某行异或到另一行上去,达到下面所有行的第l列清成0的目的
                //思维:此时可以把外层循环i看作定值,且a[h][l]=1
        }
        
        //执行循环底部的h++
        h++;
        if(h>m) break;  //执行后如果h>m,跳出循环;否则执行l++处理完第l列
    }
    
    ra=h-1;
    
    //判断解的个数
    for(int i=ra+1;i<=m;i++)
        if(a[i][n+1]!=0)
            return 0;
    return 1<<(n-ra);
}

int cnt=gauss();
printf("%d\n",cnt);

if(cnt)
{
    //求出一个合法解
    for(int j=1;j<=n;j++) x[j]=0;
    for(int i=1;i<=ra;i++) x[key[i]]=a[i][n+1];
    for(int j=1;j<=n;j++) printf("%d\n",x[j]);
}

bitset优化01线性异或方程组\(O(\frac{mn^2}{w})\)

//i:m;j:n
int m,n;
bitset<N> a[M];
bool x[N];
int ra,key[M];

int gauss()
{
    //高斯消元化A'_{m×(n+1)}的A为行最简形,得到A''_{m×(n+1)}
    int h,l;
    for(h=1,l=1;l<=n;l++)   //h和l都从1开始。借助循环依次处理第1~n列
    {
        //在第h~m行中,找到第l列系数为1的一行h1
        int h1=h;
        for(int i=h+1;i<=m;i++)
            if(a[i][l]==1)
            {
                h1=i;
                break;
            }
        if(a[h1][l]==1) key[h]=l;   //如果存在该系数为1,记录key_h=l
        else continue;  //否则,第h~m行的第l列全是0,结束本轮循环,不要执行循环底部的h++,只执行l++处理完第l列
        
        //将第h1行与第h行交换。(交换某两行)
        swap(a[h],a[h1]);
        
        //将上下除第h行外的所有行的第l列清成0。(把某行异或到另一行上去)
        for(int i=1;i<=m;i++)
        {
            if(i==h || !a[i][l]) continue;
            a[i]^=a[h]; //把某行异或到另一行上去,达到下面所有行的第l列清成0的目的
        }
        
        //执行循环底部的h++
        h++;
        if(h>m) break;  //执行后如果h>m,跳出循环;否则执行l++处理完第l列
    }
    
    ra=h-1;
    
    //判断解的个数
    for(int i=ra+1;i<=m;i++)
        if(a[i][n+1]!=0)
            return 0;
    return 1<<(n-ra);
}

int cnt=gauss();
printf("%d\n",cnt);

if(cnt)
{
    //求出一个合法解
    for(int j=1;j<=n;j++) x[j]=0;
    for(int i=1;i<=ra;i++) x[key[i]]=a[i][n+1];
    for(int j=1;j<=n;j++) printf("%d\n",x[j]);
}

建模应用

  1. 开关问题

    特征:有m个节点和n个操作。每个节点拥有2种状态,每个操作可以切换一些节点的状态,最终的局面的状态与操作的顺序无关。(同一个节点在不同的时间被切换2次后的状态和未被切换的状态相同。)求出从这m个节点的起始局面到这m个节点的目标局面的操作方案。

    这个问题符合线性异或方程组。

    第i个节点的起始状态和目标状态:\(b_i=st_i\operatorname{xor}ed_i\)

    第i个节点的状态会被第j个操作切换(第j个操作会切换第i个节点的状态):\(a_{ij}=1\);否则,\(a_{ij}=0\)

    解此线性异或方程组:若\(x_j==1\),则需要执行第j个操作;否则,不执行第j个操作。

  2. 位运算

2.3.线性空间

2.3.1.线性基

定义

给定若干个向量\(a_1,a_2,...,a_k\),若向量b能由向量\(a_1,a_2,...,a_k\)经过向量加法标量乘法运算得出,则称向量b能被向量\(a_1,a_2,...,a_k\)表出。向量\(a_1,a_2,...,a_k\)能表出的所有向量构成一个线性空间,向量\(a_1,a_2,...,a_k\)被称为这个线性空间的生成子集

任意选出线性空间的若干个向量,如果其中任意一个向量都不能被所选的其他向量表出,则称这些向量线性无关。否则线性有关。

一个线性空间的极大线性无关生成子集称为这个线性空间的一个基底,简称。一个线性空间的所有基包含的向量个数都相等,这个数被称为这个线性空间的维数

定理

同一个向量组生成的线性空间的任意两个基都是等价的,且元素数量相同。

由一个线性空间的一个生成子集求出这个线性空间的一个基

对于一个n行m列的矩阵,我们可以把它的每一行看作一个长度为m的向量(“行向量”),矩阵的n个行向量看作一个线性空间的一个生成子集。

把这个矩阵看作“系数矩阵”进行高斯消元(不同于高斯消元解线性方程组,线性空间看作的增广矩阵的最后一列全看作零),实际上,我们只需要其中的有效的n行n列的矩阵就可以求出线性基,得到一个简化阶梯形矩阵。显然这个简化阶梯形矩阵的所有非零行向量线性无关,我们就求出了线性基。

\(e.g.\)

\(\begin{bmatrix} 1&3&0&5\\0&0&4&3 \\ 0&0&0&1 \end{bmatrix}\)\(\Rightarrow\)线性基为\(1*a_1+3*a_2+5*a_4,4*a_3+3*a_4,a_4\)

int n,m;
double a[N][N];

void gauss()
{
    int h,l;
    for(h=1,l=1;l<=n;l++)
    {
        int t=h;
        for(int i=h;i<=n;i++) if(fabs(a[i][l])>fabs(a[t][l])) t=i;
        if(fabs(a[t][l])<EPS) continue;
        for(int j=l;j<=m;j++) swap(a[h][j],a[t][j]);
        for(int j=m;j>=l;j--) a[h][j]/=a[h][l];
        for(int i=1;i<=n;i++)
        {
            if(i==h) continue;
            for(int j=m;j>=l;j--)
                a[i][j]-=a[h][j]*a[i][l];
        }
        h++;
    }
    return ;
}

scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        scanf("%lf",&a[i][j]);
gauss();

2.3.2.异或空间

离线数据结构。但是根据线性基的性质:“只要是两个线性基的元素个数相同,且其中一个线性基的每一个元素都可以被另一个线性基的一个或多个元素表示,那么这两个线性基是等价的”,可以通过等价变形,实现在线操作。

线性基是一种擅长处理异或问题的数据结构,相比于01trie树的优势是空间复杂度是O(log X)。设值域为[1,N],就可以用一个长度为⌈log⁡2N⌉的数组来描述一个线性基。a[i]表示向量\(a_i\)(二进制下的第i位)的系数。

性质

  1. 给定的原序列里面的任意一个数都可以由线性基里面的一些数异或得到;
  2. 线性基里面的任意一些数异或起来都不能得到 0;
  3. 线性基里面的数的个数唯一,并且在保持性质一的前提下,数的个数是最少的;

证明:由\(a \operatorname{xor} b \operatorname{xor} c==0 \Leftrightarrow a \operatorname{xor} b==c\)\(a \operatorname{xor} b==c \Leftrightarrow a \operatorname{xor} c==b\)可推出。

  • 线性基常与bitset结合
#include<bits/stdc++.h>
using namespace std;

typedef bitset<1010> BI;
typedef pair<int,int> PII;
const int N=1010,M=1010,INF=0x3f3f3f3f;

int n,m,q;
int rtot,ridx;
int railway_id[M];
struct Railway
{
    int u,v,tim;
    BI wor;
}rai[M];
bool cancel_op[M];

int h[N],e[M],ne[M],idx;
BI w[M];

BI dis[N];
bool vis[N];

BI a[N];
int pos[N];

BI read()
{
    string s;
    cin>>s;
    BI res(s);
    return res;
}

void add(int u,int v,BI &wor)
{
    e[++idx]=v,w[idx]=wor,ne[idx]=h[u],h[u]=idx;
    return ;
}

void print(BI &x)
{
    bool lead=false;
    for(int i=999;i>=0;i--)
    {
        if(x[i]==1 || lead) putchar('0'+x[i]);
        if(x[i]==1) lead=true;
    }
    if(!lead) putchar('0');
    putchar('\n');
}

void insert(int tim,BI x)
{
    for(int i=1000;i>=0;i--)
        if(x[i]==1)
        {
            if(pos[i]==0)
            {
                a[i]=x;
                pos[i]=tim;
                return ;
            }
            else if(pos[i]<tim)
            {
                swap(a[i],x);
                swap(pos[i],tim);
            }
            x^=a[i];
        }
    return ;
}

void query(int tim)
{
    BI res;
    for(int i=1000;i>=0;i--) if(pos[i]>tim && res[i]==0) res^=a[i];
    print(res);
    return ;
}

void dfs(int u)
{
    for(int i=h[u];i!=0;i=ne[i])
    {
        int v=e[i];
        if(vis[v]) insert(INF,dis[u]^dis[v]^w[i]);
        else
        {
            vis[v]=true;
            dis[v]=dis[u]^w[i];
            dfs(v);
        }
    }
    return ;
}

int main()
{
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1;i<=m;i++)
    {
        int u,v;
        BI wor;
        scanf("%d%d",&u,&v);
        wor=read();
        add(u,v,wor),add(v,u,wor);
    }
    
    dfs(1);
    
    query(0);
    
    for(int i=1;i<=q;i++)
    {
        int x,y,k;
        BI z;
        string op;
        cin>>op;
        if(op[1]=='d')
        {
            scanf("%d%d",&x,&y);
            z=read();
            rai[++ridx]={x,y,0};
            rai[ridx].wor=dis[x]^dis[y]^z;
            rtot++;
            railway_id[rtot]=ridx;
        }
        else if(op[1]=='a')
        {
            scanf("%d",&k);
            int id=railway_id[k];
            rai[id].tim=i;
            cancel_op[i]=true;
        }
        else
        {
            scanf("%d",&k);
            z=read();
            int &id=railway_id[k];
            int u=rai[id].u,v=rai[id].v;
            rai[id].tim=i;
            rai[++ridx]={u,v,0};
            rai[ridx].wor=dis[u]^dis[v]^z;
            id=ridx;
        }
    }
    for(int i=1;i<=ridx;i++) if(rai[i].tim==0) rai[i].tim=INF;
    
    for(int i=1,j=0;i<=q;i++)
    {
        if(!cancel_op[i])
        {
            j++;
            insert(rai[j].tim,rai[j].wor);
        }
        query(i);
    }

    return 0;
}

2.3.2.1.基础离线操作(全局查询)

int t,q,Case;
int cnt;    //线性基个数
LL a[63],tmp[63];//a:线性基;tmp:二进制拆分解决第k小问题
bool zero;  //特判0,线性基不能异或得到0,但有时原序列可以

void init()
{
    memset(a,0,sizeof a);
    memset(tmp,0,sizeof tmp);
    cnt=0;
    zero=false;
    return ;
}

//插入x
void insert(LL x)
{
    for(int i=62;i>=0;i--)
        if(x&(1ll<<i))
            if(a[i]==0)
            {
                a[i]=x;
                return ;    //插入成功,返回
            }
            else x^=a[i];
    zero=true;  //要是走到这一步x还没有return,说明原序列可以异或得到0(而线性基不行,故要特判)
    return ;
}

//查询异或能不能得到x
bool check(LL x)
{
    if(x==0) return zero;
    for(int i=62;i>=0;i--)
        if(x&(1ll<<i))
            if(a[i]==0) return false;
            else x^=a[i];
    return true;
}

//查询异或能得到的最大的结果
LL query_max()
{
    LL res=0;
    for(int i=62;i>=0;i--) res=max(res,res^a[i]);
    return res;
}

//查询异或能得到的最小的结果
LL query_min()
{
    if(zero) return 0;
    for(int i=0;i<=62;i++) if(a[i]!=0) return a[i];
}

//在query_k()前,一定要确保在最新的insert()后至少有一次rebuild()
void rebuild()
{
    cnt=0;
    for(int i=0;i<=62;i++)
    {
        for(int j=i-1;j>=0;j--) if(a[i]&(1ll<<j)) a[i]^=a[j];
        if(a[i]!=0) tmp[cnt++]=a[i];//如果a[i]一开始不为0,则这里一定不可能为0。这里是在特判a[i]一开始就为0的情况
    }
    return ;
}

//查询异或能得到的第k小的结果
//在query_k()前,一定要确保在最新的insert()后至少有一次rebuild()
LL query_k(LL k)
{
    LL res=0;
    k-=zero;
    if(k==0) return 0;
    rebuild();  //在query_k()前,一定要确保在最新的insert()后至少有一次rebuild()
    if(k>=(1ll<<cnt)) return -1;
    for(int i=0;i<cnt;i++) if(k&(1ll<<i)) res^=tmp[i];
    return res;
}

int main()
{
    scanf("%d",&t);
    while(t--)
    {
        init();
        printf("Case #%d:\n",++Case);
        
        scanf("%d",&q);
        while(q--)
        {
            int op;
            scanf("%d",&op);
            if(op==1)
            {
                LL x;
                scanf("%lld",&x);
                insert(x);
            }
            else if(op==2)
            {
                LL x;
                scanf("%lld",&x);
                if(check(x)) puts("ture");
                else puts("false");
            }
            else if(op==3) printf("%lld\n",query_max());
            else if(op==4) printf("%lld\n",query_min());
            else
            {
                LL k;
                scanf("%lld",&k);
                printf("%lld\n",query_k(k));
            }
        }
    }
    return 0;
}

2.3.2.2.区间询问最大值

  • 思路

    我们的前缀线性基需要两个变量:p[i]表示第i位的值,pos[i]表示对第i位造成影响的数的编号。那么很显然,假如我们已经求出了1→r的线性基,其中所有pos≥l的数构成的线性基就是区间[l,r]的数构成的线性基

    根据贪心的思想,我们知道,我们要尽可能地让pos[i]的值最大,这样才能保证我们通过上述方法构造出的线性基是[l,r]的线性基

    那么如何保证pos[i]的值最大呢?

    很简单,如果我们插入一个值x,它的位置为w,那么如果有一位的pos<w,那么我们就把这一位的p取出来换成x,pos换成w,然后把pos,p继续往下插入,这样构造出的线性基pos就是最大的。

离线和在线思路一样。只不过离线将询问按右端点排序,从左到右依次插入,只需要一维空间;在线需要单独给每次插入的数再开一维的空间。

离线

int n,m;
int c[N],ans[N];
PIII q[N];
int a[63];//a:线性基
int pos[63];//pos[i]:第i位线性基造成影响的是第几个插入的数

void insert(int tim,int x)
{
    for(int i=62;i>=0;i--)
        if(x&(1ll<<i))
        {
            if(a[i]==0)
            {
                a[i]=x;
                pos[i]=tim;
                return ;    //插入成功,返回
            }
            else if(pos[i]<tim)
            {
                swap(a[i],x);
                swap(pos[i],tim);
            }
            x^=a[i];//注意这里不可以放在上面的else if
        }
    return ;
}

int query_max(int l,int r)
{
    int res=0;
    for(int i=62;i>=0;i--) if(a[i]!=0 && pos[i]>=l) res=max(res,res^a[i]);
    return res;
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&c[i]);
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        q[i]={{r,l},i};
    }
    sort(q+1,q+m+1);
    int st=1;
    for(int i=1;i<=m;i++)
    {
        int id=q[i].second,l=q[i].first.second,r=q[i].first.first;
        while(st<=r)
        {
            insert(st,c[st]);
            st++;
        }
        ans[id]=query_max(l,r);
    }
    for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
}

在线

在线非常耗空间!!!

int n,q;
int a[N][63];//a[l]:插入第l个数字时的线性基
int pos[N][63];//pos[l][i]:插入第l个数字时,对第i位线性基造成影响的是第几个插入的数

void insert(int tim,int x)
{
    memcpy(a[tim],a[tim-1],sizeof a[tim]);
    memcpy(pos[tim],pos[tim-1],sizeof pos[tim-1]);
    int backup=tim;
    for(int i=62;i>=0;i--)
        if(x&(1ll<<i))
        {
            if(a[backup][i]==0)
            {
                a[backup][i]=x;
                pos[backup][i]=tim;
                return ;    //插入成功,返回
            }
            else if(pos[backup][i]<tim)
            {
                swap(a[backup][i],x);
                swap(pos[backup][i],tim);
            }
            x^=a[backup][i];
        }
    return ;
}

int query_max(int l,int r)
{
    int res=0;
    for(int i=62;i>=0;i--) if(a[r][i]!=0 && pos[r][i]>=l) res=max(res,res^a[r][i]);
    return res;
}

for(int i=1;i<=n;i++)
{
    int x;
    scanf("%d",&x);
    insert(i,x);
}
scanf("%d%d",&l,&r);
printf("%d\n",query_max(l,r));

2.3.2.3.删除和修改操作

离线

  • 思路

    类似《数学2.3.2.2.区间询问最大值》,要写可删除线性基的话,借鉴上面的思想,我们肯定希望在线性基中维护删除时间最晚的元素。

    我们设\(a_i\)表示线性基中第i位的元素,\(pos_i\)表示该元素将在何时被删去(没有被删去的元素\(pos_i=INF\))。

    按照贪心的思想,我们肯定希望越高位的线性基越晚删除——不然如果你到了时间,下面的能选而上面不能选,不是会令答案变小吗?

    因此我们可以从上到下枚举每一位,能换就换,之后继续向下尝试替换。

    查询时,超过删除时间的元素不计入贡献。

修改操作\(\Leftrightarrow\)删除操作(\(pos_i\)记录该元素将在此时被删去)+插入操作。

下面记录了插入、删除第 k个插入的数、修改第 k个插入的数、查询异或res得到的最大值、bitset用法:

void insert(int tim,BI x)
{
    for(int i=1000;i>=0;i--)
        if(x[i]==1)
        {
            if(pos[i]==0/*当前线性基没有数*/)
            {
                a[i]=x;
                pos[i]=tim;
                return ;
            }
            else if(pos[i]<tim)
            {
                swap(a[i],x);
                swap(pos[i],tim);
            }
            x^=a[i];
        }
    return ;
}

void query_max(int tim,BI res)
{
    for(int i=1000;i>=0;i--) if(pos[i]>tim && res[i]==0/*因为bitset没有max,因此只有在当前位为0时异或,贡献一定不会减小*/) res^=a[i];
    print(res);
    return ;
}

for(int i=1;i<=q;i++)
{
    int x,y,k;
    BI z;
    string op;
    cin>>op;
    if(op[1]=='d')//插入
    {
        z=read();
        rai[++ridx].tim=0;
        rai[ridx].wor=z;
        
        //记录第k个插入的数是ridx
        rtot++;
        railway_id[rtot]=ridx;
    }
    else if(op[1]=='a')//删除
    {
        scanf("%d",&k);
        int id=railway_id[k];
        rai[id].tim=i;//删除
        cancel_op[i]=true;
    }
    else//修改
    {
        scanf("%d",&k);
        z=read();
        int &id=railway_id[k];
        rai[id].tim=i;//删除
        
        rai[++ridx].tim=0;
        rai[ridx].wor=z;
        id=ridx;//覆盖第k个插入的数的id
    }
}
for(int i=1;i<=ridx;i++) if(rai[i].tim==0) rai[i].tim=INF;//没有被删去的元素

for(int i=1,j=0;i<=q;i++)
{
    if(!cancel_op[i])
    {
        j++;
        insert(rai[j].tim,rai[j].wor);
    }
    query(i);
}

在线

  • 在线

    重点是删除操作,如果要删除的数 x x x 在线性基外,那么直接删掉即可,问题是假如它在线性基内,把他删掉之后可能序列中其他的数可以填进来。

    现在讨论一下 x x x 在线性基内的做法:

    没有在线性基中的数,一定是因为线性基中的若干个数可以异或得到他,那么可以记录一下不在线性基中的数都是由线性基中的哪些数异或得到的,那么每一个线性基外的数对应一个集合 S S S,这个集合内就是线性基中那些异或起来可以得到他的数。

    假如线性基外的某一个数的 S S S 中包含 x x x,那么就不需要删除 x x x,把这个数删除即可。

    原因是这个数在线性基中是可以代替 x x x的,那么就当这个数代替了 x x x,然后 x x x 被删除了,然后把线性基中的 x x x 当做这个数即可,这样的话线性基不会有变化。(实现起来并不需要维护集合 S S S,而是直接维护有哪些数可以代替线性基中的数就好了)

    假如 x x x 不被线性基外的任何一个数的 S S S 包含,那么就另外造一个集合 P P P,记录线性基中这个数插入进来的时候异或过哪些数。然后找到线性基中最小的并且 P P P 包含 x x x 的数,让他异或线性基中其他包含 x x x 的数即可(包括自己,自己异或完自己后自己这一位就变成了 0 0 0),这样就能消除 x x x 在线性基中的影响(事实上就等价于用最小的数代替了它)。

    总之,由于如果修改了线性基中的某一位会影响到一些比它小的位,所以一般不能修改,要么改最小的并且不会影响到下面的位。

在线写法很恶心。因此考场如果遇到的话——每次在线删除重新建立线性基。

2.3.2.4.线性基合并

将一个线性基内的所有数都插入另一个线性基即可合并这2个线性基。

2.3.2.5.应用

2.3.2.5.1.异或最长路

《图论4.4.8.异或最长路》

图论

3.组合数学

当读入数据爆long long时,注意一下有无模数。有,秦九韶算法。无,高精度。

3.1.加法原理

3.2.乘法原理

3.3.排列数

\(A_n^m=\dfrac{n!}{(n-m)!}=n*(n-1)*...*(n-m+1)\)

3.4.组合数

\(C_n^m=\dfrac{n!}{m!*(n-m)!}=\dfrac{n*(n-1)*...*(n-m+1)}{m*(m-1)*...*2*1}\)

性质

  • \((m!)*C_{n}^{m}=A_{n}^{m}\)

  • \(C_{n}^{m}=C_{n}^{n-m}\)

  • \(C_{n}^{m}=C_{n-1}^{m}+C_{n-1}^{m-1}\)

    理解:从n位同学中选出m位班长的方案数=第n位同学当班长(从n-1位同学中选出m-1位班长的方案数)+第n位同学不当班长(从n-1位同学中选出m位班长的方案数)。

  • 下角标相同:\(\sum\limits_{i=0}^{n}C_n^i=C_{n}^{0}+C_{n}^{1}+C_{n}^{2}+...+C_{n}^{n}=2^n\)

  • 上角标相同:\(\sum\limits_{i=n}^{x} C_i^n = C_n^n + C_{n+1}^n + C_{n+2}^n+\cdots +C_x^n=C_{x+1}^{n+1}\)

    理解:从x+1个数选n+1个数的方案数=以第n+1个数结尾的方案数\(C_n^n\)+以第n+2个数结尾的方案数\(C_{n+1}^n\)+……。

  • 吸收恒等式:\(C_n^m=\frac{n}{m}*C_{n-1}^{m-1}\),其中m≠0。证明:\(C_n^m\)的阶乘形式提出一个\(\frac{n}{m}\)

  • 范德蒙德卷积:\(\sum\limits_{i=0}^{k} \{C_{m}^{i}*C_{n}^{k-i}\}=C_{m+n}^{k}\)

    理解:性质2的推广。从m+n位同学中选出k位班长的方案数=从m位男生中选出i位班长的方案数*从n位女生中选出k-i位班长的方案数。

求组合数\(C_x^y\)

  • 求n个在各自模数p下的y很小的组合数或一个组合数,且答案不爆int128或“有模数p且模数p是质数且p>y(满足在模p意义下y!的逆元存在)”:代入公式直接求解\(O(N*Y)\)
    \(C_x^y=\dfrac{x!}{y!*(x-y)!}\)
LL C(LL down,LL up)
{
    if(down<up) return 0;   //这里不要忘记
    LL res=1;
    for(LL i=1,j=down;i<=up;i++,j--)
    {
        //连续的i个数中有且仅有一个数是i的倍数
        if(j%i==0) res=j/i*res;
        else res=res/i*j;
    }
    return res;
}
LL C(LL down,LL up,LL p)
{
    if(down<up) return 0;   //这里不要忘记
    LL res=1;
    for(LL i=1,j=down;i<=up;i++,j--)
    {
        res=res*j%p;
        
        LL inv_i=get_inv(i,p);
        inv_i=(inv_i%p+p)%p;//这里不要忘记!!!
        res=res*inv_i%p;    //乘i的逆元
    }
    return res;
}
  • 求n个在共同模数q下的x稍小的组合数,且模数q任意:使用公式递推\(O(X^2+N*1)\)

    \(C_{x}^{y}=C_{x-1}^{y}+C_{x-1}^{y-1}\)

int n,p,a,b;
int c[N][N];    //c[a][b]:a是C的下标

void init()
{
    for(int i=0;i<N;i++)//注意这里是大N,而且是从0开始!!!
        for(int j=0;j<=i;j++)
            if(j==0) c[i][j]=1;
            else c[i][j]=(c[i-1][j]+c[i-1][j-1])%p;
    return ;
}

int main()
{
    scanf("%d%d",&n,&p);
    init();
    while(n--)
    {
        scanf("%d%d",&a,&b);
        printf("%d\n",c[a][b]);
    }
    return 0;
}
  • 求n个在共同模数p下的x稍大的组合数,且模数p是质数且p>x(满足在模p意义下x!的逆元存在):递推线性预处理阶乘及其逆元\(O(X+N*1)\)
    1. 递推求出在模p意义下0~x的阶乘。
    2. 快速幂求x!的逆元。
    3. 递推求出在模p意义下0~x-1的阶乘的逆元。
    4. 根据公式
      \(C_x^y=\dfrac{x!}{y!*(x-y)!}\)\(O(1)\)\(C_x^y\)
LL n,p;
LL fact[N],infact[N];//预处理阶乘及其在模P意义下的逆元

void init()
{
    fact[0]=infact[0]=1;
    for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%p;
    
    //线性求逆元,恰好利用了阶乘是前缀积的性质
    infact[N-1]=get_inv(fact[N-1],p);
    for(int i=N-2;i>=1;i--) infact[i]=infact[i+1]*(i+1)%p;
    
    return ;
}

//O(1)求C
inline LL C(LL down,LL up,LL p)
{
    if(down<up) return 0;   //这里不要忘记
    return fact[down]*infact[up]%p*infact[down-up]%p;
}

int main()
{
    scanf("%lld%lld",&n,&p);
    init();
    while(n--)
    {
        LL a,b;
        scanf("%lld%lld",&a,&b);
        printf("%lld\n",C(a,b,p));
    }
    return 0;
}
  • 求n个在共同模数q下的x稍大的组合数,且模数q任意:

    方法一:特殊处理q的因数+递推预处理\(O(\sqrt Q+X\max(\log Q,\log X)+N*\max(\log Q,\log X))\)

    1. 分解质因数\(q=\prod\limits_i p_i^{k_i}\)
    2. 递推求出0~x中每个数w:\(w!=w'\prod\limits_i p_i^{w''_i}\),其中w'不含q的任何因数。求出\(f_w=w'\bmod q,g_{w,i}=w''_i\)
    3. exgcd()\(f_x\)的逆元。递推求出0~x-1中每个数w的\(f_w\)的逆元。
    4. \(C_x^y\equiv f_x*f_y^{-1}*f_{x-y}^{-1}*\prod\limits_ip_i^{g_{x,i}-g_{y,i}-g_{x-y,i}}\pmod q\)。根据组合数的性质,\(g_{x,i}-g_{y,i}-g_{x-y,i}\geqslant0\)

    方法二:(扩展)Lucas定理\(O(N*(\log_P X)*求级别为P的组合数的复杂度)\)

  • 求在共同/各自模数p下的x很大的组合数,且模数p是稍大的质数:Lucas定理\(O((\log_P X)*求级别为P的组合数的复杂度)\)

    p可以小于x。

    Lucas定理:\(C_x^y \equiv C_{x \% p}^{y \% p} * C_{\lfloor x/p\rfloor}^{\lfloor y/p\rfloor} \pmod p\)

    $C_{x % p}^{y % p} \(不能继续递归显然可以直接求解,\) C_{x/p}^{y/p}$继续递归求解。

LL qpow(LL a,LL b,LL p)
{
    LL res=1;
    while(b)
    {
        if(b&1) res=(res*a)%p;
        a=a*a%p;
        b>>=1;
    }
    return res;
}

LL C(LL down,LL up,LL p)
{
    if(down<up) return 0;   //这里不要忘记
    LL res=1;
    for(LL i=1,j=down;i<=up;i++,j--)
    {
        res=res*j%p;
        res=res*qpow(i,p-2,p)%p;    //乘i的逆元
    }
    return res;
}

LL lucas(LL down,LL up,LL p)
{
    if(down<p && up<p) return C(down,up,p);//这里不要忘记
    return C(down%p,up%p,p)*lucas(down/p,up/p,p)%p;
}

int main()
{
    scanf("%d",&n);
    while(n--)
    {
        LL a,b,p;
        scanf("%lld%lld%lld",&a,&b,&p);
        printf("%lld\n",lucas(a,b,p));
    }
    return 0;
}
#### Lucas定理与组合数奇偶性

适用条件:**异或**→等价于某实际意义的方案数的奇偶性→组合数式子的奇偶性。

由Lucas定理:$C_x^y$是奇数$\Leftrightarrow$$C_x^y\bmod 2=1$$\Leftrightarrow$在二进制下的数位,$x_i=0,y_i=0$或$x_i=1,y_i=0$或$x_i=1,y_i=1$$\Leftrightarrow$在二进制下,y是x的子集$\Leftrightarrow$y&(x-y)==0。

推论:$C_n^{a_1}C_{n-a_1}^{a_2}C_{n-a_1-a_2}^{a_3}\cdots$是奇数$\Leftrightarrow$$a_1,a_2,a_3,\cdots$在二进制表示下某一位最多只有一个数为1。
  • 求在各自模数q下的x很大的组合数,且模数q是稍大的数:扩展Lucas定理\(O((\log_Q X)*求级别为Q的组合数的复杂度)\)
    1. 分解\(q=p_1^{k_1}*p_2^{k_2}*\cdots*p_n^{k_n}\)

    2. 一般中国剩余定理\(\begin{cases} x\equiv C_n^m \pmod {p_i^{k_i}} \end{cases}\)解出x即为答案。

      显然在上面的分解中\(p_i^{k_i}\)两两互质,因此使用一般中国剩余定理。

      现在来求出\(C_n^m \bmod p_i^{k_i}\)

      • 若q是sqaure-free数,即\(\forall k_i=1\)

        一般Lucas定理求出\(C_n^m \bmod p_i\)

      • 否则:

        需要背5个结论:

        \(C_{n}^{m} \bmod p^k= \dfrac{\dfrac{n!}{p^{\nu(p,n!)}}}{\dfrac{m!}{p^{\nu(p,m!)}}*\dfrac{(n-m)!}{p^{\nu(p,(n-m)!)}}}*p^{\nu(p,n!)-\nu(p,(m!))-\nu(p,((n-m)!))}\operatorname{mod} p^k\)

        定义1:$\nu(p,x)$:x可以分解出多少个p,即$x=p^{\nu(p,x)}*c,c\bot p$。
        
        结论1:威尔逊定理拓展2:$\nu(p,x)=\dfrac{x-sum_p(x)}{p-1}$。
        
          定义2:$sum_p(x)$:x在p进制下的数位之和。
        

        定义3:看到式子中有3个形式一样的结构,设\(f(x)\equiv\dfrac{x!}{p^{\nu(p,x!)}}(\operatorname{mod}p^k)\)

        \(原式=\dfrac{f(n)}{f(m)*f(n-m)}*p^{\nu(p,n!)-\nu(p,(m!))-\nu(p,((n-m)!))}\operatorname{mod} p^k\)

        结论3:威尔逊定理变形:\(f(x)=(n\bmod p^k)_p^!*[(p^k)_p^!]^{\lfloor\frac{x}{p^k}\rfloor}*f(\lfloor\dfrac{x}{p}\rfloor)\bmod p^k\)

        定义4:$(x)_p^!=\prod\limits_{1\le i\le x \land i\bot p}i$。
        
        对于第一项:因为对$p^k$取模且$p^k\le q\le 10^5$,所以暴力求解。
        
        对于第二项:结论5:威尔逊定理拓展1:$(p^k)_p^!\bmod p^k =\begin{cases}1&p=2\land k\geqslant 3,\\-1&\texttt{otherwise.}\end{cases} $。如果$(p^k)_p^!=-1$则再看指数的奇偶性,-1的奇数次方等于-1,偶数次方等于1。
        
        对于第三项:递归处理。
        
        边界:$f(0)=1$。
        

        注意:除了1.判断\(i\bot p\),因为p是质数,所以可以用i%p!=0判互质;2.判断-1的指数的基偶性x/pk%2==1。其余全是模\(p^k\)

#define a first
#define m second

LL inv(LL b,int p){}    //求b在模p意义下的逆元

//求x在p进制下的数位之和
int sp(LL x,int p)
{
    int res=0;
    while(x)
    {
        res+=x%p;
        x/=p;
    }
    return res;
}

LL f(LL x,int p,int k,int pk)
{
    if(x==0) return 1;  //边界
    LL res=1;
    
    //依次求三项
    for(int i=1;i<=x%pk;i++) if(i%p!=0/*因为p是质数,所以可以这样判互质,因此模的是p*/) res=res*i%pk;    //暴力求解
    if(!(p==2 && k>=3)/*威尔逊定理拓展2*/ && x/pk%2==1/*-1的奇数次方等于-1,偶数次方等于1*/) res=(res*-1%pk+pk)%pk;
    res=res*f(x/p,p,k,pk)%pk;   //递归求解
    
    return res;
}

LL subLucas(LL n,LL m,int p,int k,int pk)
{
    int spn=sp(n,p),spm=sp(m,p),spd=sp(n-m,p);
    int vpn=(n-spn)/(p-1),vpm=(m-spm)/(p-1),vpd=((n-m)-spd)/(p-1);
    return f(n,p,k,pk)*inv(f(m,p,k,pk)*f(n-m,p,k,pk)%pk,pk)%pk*qpow(p,vpn-vpm-vpd,pk)%pk;
}

LL CRT(vector<pair<LL,int> > v,int M)
{
    LL res=0;
    for(auto it : v)
    {
        int Mi=M/it.m;
        int invm=inv(Mi,it.m);
        res=(res+__int128(it.a)*Mi%M*invm%M)%M;
    }
    return (res+M)%M;
}

LL exLucas(LL n,LL m,int q)
{
    if(n<m && q==1) return 0;  //不要忘记特殊情况!
    vector<pair<LL,int> > v;
    int backup=q;   //记得备份,最后要用
    for(int p=2;1ll*p*p<=q;p++)
        if(q%p==0)
        {
            int k=0,pk=1;
            while(q%p==0)
            {
                k++;
                pk*=p;
                q/=p;
            }
            v.push_back({subLucas(n,m,p,k,pk),pk});
        }
    if(q>1) v.push_back({subLucas(n,m,q,1,q),q});   //别忘了大于根号的质因数!
    return CRT(v,backup);
}

scanf("%lld%lld%d",&n,&m,&q);
printf("%lld\n",exLucas(n,m,q));
  • 高精度求组合数

    • 前置知识:阶乘分解

      N!中质因子p的个数\(=\sum\limits_{k=1}^{p^k<N} \lfloor \dfrac{N}{p^k} \rfloor\)

int n;
int primes[N],pidx;
bool st[N];

void pre_primes()
{
    for(int i=2;i<=n;i++)
    {
        if(!st[i]) primes[++pidx]=i;
        for(int j=1;j<=pidx && primes[j]<=n/i;j++)
        {
            st[primes[j]*i]=true;
            if(i%primes[j]==0) break;
        }
    }
    return ;
}

int main()
{
    scanf("%d",&n);

    pre_primes();

    for(int i=1;i<=pidx;i++)
    {
        int p=primes[i],cnt=0;
        for(int j=p;j<=n;j*=p)
        {
            cnt+=n/j;
            if(j>n/p/*j*p>n*/) break;   //注意这个不能放在上面的判定条件,否则会少统计
        }
        printf("%d %d\n",p,cnt);
    }

    return 0;
}
公式$C_n^m=\dfrac{n!}{m!*(n-m)!}$,把阶乘分解,在数组中保存各项质因子的指数。然后约分。最后把剩余质因子乘起来。
const int N = 5010;//N=max(down,up)

int primes[N], cnt;
int sum[N];
bool st[N];


void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}


int get(int n, int p)
{
    int res = 0;
    while (n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}


vector<int> mul(vector<int> a, int b)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}


int main()
{
    int a, b;
    cin >> a >> b;

    get_primes(a);

    for (int i = 0; i < cnt; i ++ )
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(a - b, p) - get(b, p);
    }

    vector<int> res;
    res.push_back(1);

    for (int i = 0; i < cnt; i ++ )
        for (int j = 0; j < sum[i]; j ++ )
            res = mul(res, primes[i]);

    for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
    puts("");

    return 0;
}

应用

  • 组合计数一般与dp、递推结合。

  • 隔板法

    • \(a_1+a_2+...+a_k=n\)的正整数解的数量\(=C_{n-1}^{k-1}\)

    • 非负整数解的数量:令\(b_i=a_i+1\),则\(b_i>0\)\(b_1+b_2+...+b_k=n+k\),答案\(=C_{(n+k)-1}^{k-1}\)

    • 《数学3.2.容斥原理》:求\(a_1+a_2+...+a_k=n,(a_1 \le x)\)的正整数解的数量。

      全集:\(a_1+a_2+...+a_k=n\)的正整数解的数量\(=C_{n-1}^{k-1}\)

      补集:\(a_1+a_2+...+a_k=n,(a_1 > x)\)的正整数解:先把x个小球给1,再对剩下的小球隔板法:\(C_{(n-x)-1}^{k-1}\)

      答案\(=C_{n-1}^{k-1}-C_{n-x-1}^{k-1}\)

    • 《数学3.2.容斥原理》:求\(a_1+a_2+...+a_k=n,(a_1 \le x)\)的非负整数解的数量。

      \(b_i=a_i+1\),则\(b_i>0\)\(b_1+b_2+...+b_k=n+k\)\(,(b_1≤\)\(x+1\)\()\)

      全集:\(b_1+b_2+...+b_k=n+k\)的正整数解的数量\(=C_{(n+k)-1}^{k-1}\)

      补集:\(b_1+b_2+...+b_k=n,(b_1 >\)$ x+1$$)\(的正整数解:先把x+1个小球给1号,再对剩下的小球隔板法:\)C_{((n+k)-(x+1))-1}^{k-1}$;

      答案\(=C_{(n+k)-1}^{k-1}-C_{((n+k)-(x+1))-1}^{k-1}\)

    • 例题:给定无穷个1*x(x为任意正整数)的矩形,求使用 a 个矩形,铺满 1×b 的方格的方案数。

      有a个放矩形的区域,有b个格子分配给每个区域,一个区域被分配x个格子就代表该区域放入一个1*x的矩形,且每个区域至少分配1个格子,那么答案就是\(C_{b-1}^{a-1}\)

  • 补集法:给定n*m的网格,求三点都在格点上的三角形数量。

    全集:从网格中任选3个点的方案数\(=C_{n*m}^3\)

    补集:选出的3个点在同一直线上的方案数(即不合法方案数)

    1. 直线斜率=INF:\(m*C_n^3\)
    2. 直线斜率=0:\(n*C_m^3\)
    3. 0<直线斜率<INF:首先n--,m--;还原为长度,\(\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{m}(n-i+1)*(m-j+1)*(\gcd (i,j)-1)\)

3.5.二项式定理

\((x+y)^n=\sum\limits_{k=0}^{n} \{ C_{n}^{k}*x^{n-k}*y^k \}=\sum\limits_{k=0}^{n} \{ C_{n}^{k}*x^k*y^{n-k} \}\)

3.6.多重集

设S={\(n_1*a_1,n_2*a_2,...,n_k*a_k\)}:S是由\(n_1\)\(a_1\)\(n_2\)\(a_2\),...,\(n_k\)\(a_k\)组成的多重集。

  • \(n=n_1+n_2+...+n_k\),则S的全排列个数,即排列数\(=\dfrac{n!}{n_1!*n_2!*...*n_k!}\)

  • 设整数\(r≤n_i(\forall i \in [1,k])\),则从S中取出r个元素组成一个多重集(不考虑元素的顺序),产生的不同多重集的数量,即组合数\(=C_{k+r-1}^{k-1}\)

  • 《数学3.1.3.应用》:设更一般的整数r≤n,则从S中取出r个元素组成一个多重集(不考虑元素的顺序),产生的不同多重集的数量,即组合数\(=C_{k+r-1}^{k-1}-\sum\limits_{i=1}^{k}C_{k+r-(n_i+1)-1}^{k-1}+\sum\limits_{1\le i<j\le k}C_{k+r-(n_i+1)-(n_j+1)-1}^{k-1}-...+(-1)^k*C_{k+r-1-(\sum_{i=1}^{k}(n_i+1))}^{k-1}\)
    证明:将问题转化为求\(x_1+x_2+...+x_k=n\)\(,(x_1≤n_1,x_2≤n_2,...,x_k≤n_k)\)的非负整数解的数量。

    前置知识:

    《数学3.1.3.容斥原理》:求$a_1+a_2+...+a_k=n,(a_1 \le x)$的非负整数解的数量。
    
    令$b_i=a_i+1$,则$b_i>0$,$b_1+b_2+...+b_k=n+k$$,(b_1≤$$x+1$$)$。
    
    全集:$b_1+b_2+...+b_k=n+k$的正整数解的数量$=C_{(n+k)-1}^{k-1}$;
    
    补集:$b_1+b_2+...+b_k=n,(b_1 >$$ x+1$$)$的正整数解:先把x+1个小球给1号,再对剩下的小球隔板法:$C_{((n+k)-(x+1))-1}^{k-1}$;
    
    答案$=C_{(n+k)-1}^{k-1}-C_{((n+k)-(x+1))-1}^{k-1}$。
    
const int N=20,MOD=1e9+7;
LL up,down=1,ans;
LL n[N];

LL qpow(LL a,LL b)
{
    LL res=1;
    while(b)
    {
        if(b&1) res=res*a%MOD;
        a=a*a%MOD;
        b>>=1;
    }
    return res;
}

LL C(LL a,LL b)
{
    if(a<b) return 0;
    up=1;
    for(LL i=a;i>a-b;i--) up=i%MOD*up%MOD;
    return up*down%MOD;
}

int main()
{
    LL k,r;
    scanf("%lld%lld",&k,&r);
    for(int i=0;i<k;i++) scanf("%lld",&n[i]);
    
    for(int j=1;j<=k-1;j++) down=down*j%MOD;
    down=qpow(down,MOD-2);  //逆元
    
    for(int i=0;i<=(1<<k)-1;i++)
    {
        LL a=r+k-1,b=k-1;
        int sign=1;
        for(int j=0;j<k;j++)
            if((i>>j)&1)
            {
                sign*=-1;
                a-=n[j]+1;
            }
        ans=(ans+C(a,b)*sign)%MOD;
    }
    
    printf("%lld\n",(ans%MOD+MOD)%MOD);
    
    return 0;
}

3.7.Catalan数

格路计数

在平面直角坐标系上,每一步只能向上或向右走,从(0,0)走到(n,m)的方案数是\(C_{n+m}^n\):一共要走n+m步,先把n+m步分配给n步向右走。

Catalan数

\(Cat_n=\dfrac{C_{2*n}^{n}}{n+1}=C_{2n}^n-C_{2n}^{n-1}\)(需要模p时采用最后一种表达式)。

方案数是Catalan数的事件的特征:该事件满足可以通过枚举分割点,分治成本质仍相同的两个子事件。

  • 在平面直角坐标系上,每一步只能向上或向右走,从(0,0)走到(n,n)并且不接触直线y=x+1的路线的数量。

    • 证明

      去除“不接触直线”的条件,总方案数是\(C_{2n}^n\)

      设一条与y=x+1接触的不合法路径的第一个交点是(a,a+1),然后把(a,a+1)之后的路径全部按照y=x+1这条线对称过去。这样,最后的终点就会变成(n−1,n+1)。可通过对称证明所有不合法路径与一条到(n−1,n+1)的路径一一对应。故不合法路径总数是\(C_{2*n}^{n-1}\)

      故合法路径总数是\(C_{2n}^n-C_{2*n}^{n-1}\)

  • 给定n个0和n个1,将他们排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数,的序列的数量。

  • n个左括号和n个右括号组成的合法括号序列的数量。

  • 1,2,...,n经过一个栈,形成的合法出栈序列的数量。

  • 具有n+1个叶子节点的不同二叉树的数量。

  • 连接凸n-2边形的n-5条互不相交的对角线(划分成n-4个三角形)的方案数。

3.8.Stirling数

3.8.1.无符号第一类Stirling数

适用条件:将 n 个两两不同的元素,划分为 k 个集合。其中对于一个大小为m的集合,其内部的排列数为(m-1)!。

定义

将 n 个两两不同的元素,划分为 k 个非空圆排列的方案数,记作\(s(n,k)\)或$\begin{bmatrix}n\k\end{bmatrix} $。

性质

除最后两个性质外,均可通过组合计数的方法求解。

  • s(0,0)=1

  • s(n,0)=0

  • s(n,n)=1

  • 非常重要的性质:\(s(n,1)=\frac{n!}{n}=(n-1)!\),即一个大小为n的集合的圆排列数是全排列个数除掉圆旋转得到的相同方案数。

  • \(s(n,n-1)=C_n^2\)

  • \(s(n,2)=\frac{1}{2}\sum\limits_{i=1}^{n-1}\text{C}_n^i\left(i-1\right)!\left(n-i-1\right)!=\left(n-1\right)!\sum\limits_{i=1}^{n-1}\frac{1}{i}\)

  • \(s(n,n−2)=2*C_n^3+\frac{C_n^2C_{n-2}^2}{2}=2*C_n^3+3*C_n^4\),n-1个长度为1的圆和1个长度为3的圆/n-2个长度为1的圆和2个长度为2的圆。

  • 设无符号第一类Stirling数的生成函数\(f(x)=\sum\limits_{k=0}^{n}\begin{bmatrix}n \\ k\end{bmatrix} x^k\)。则\(f(x)=x^{n \uparrow}=x(x+1)(x+2)...(x+n-1)\)

    证明:第k次项系数:第n项选x:\(\begin{bmatrix}n-1 \\ k-1\end{bmatrix}\);第n项选(n-1):\((n-1)*\begin{bmatrix}n-1 \\ k\end{bmatrix}\)。所以第k次项系数=\(\begin{bmatrix}n \\ k\end{bmatrix}\)

  • \(\sum\limits_{k=1}^ns\left(n,k\right)=n!\)。也就是上面的生成函数f(1)的情况。

递推求N以下的 无符号第一类Stirling数\(O(N^2)\)

\(s1[n][k]\)\(s(n,k)\)

状态转移:\(s1[i][j]=s1[i-1][j-1]+(i-1)*s1[i-1][j]\):前一项表示将第i个数放入新的圆排列;后一项表示将第i个数放入原来的圆排列,一共有i-1种放法。

边界:\(s1[0][0]=1\)\(\forall i≥1,s1[i][0]=0\)

3.8.2.第二类Stirling数

适用条件:将 n 个两两不同的元素,划分为 k 个集合。其中对于一个大小为m的集合,其内部的排列数为1。

定义

将 n 个两两不同的元素,划分为 k 个非空子集的方案数,记作\(S(n,k)\)\(\left\{\begin{matrix}n\\k\end{matrix} \right\}\)

性质

除最后两个性质外,均可通过组合计数的方法求解。

  • s(0,0)=1
  • s(n,0)=0
  • s(n,n)=1
  • s(n,1)=1
  • \(s(n,n-1)=C_n^2\)
  • \(s(n,2)=\frac{2^n-2}{2}=2^{n-1}-1\)
  • \(s(n,n−2)=C_n^3+\frac{C_n^2C_{n-2}^2}{2}=C_n^3+3*C_n^4\)
  • \(\sum\limits_{k=1}^ns\left(n,k\right)=B_n\)\(B_n\)是贝尔数。

通项公式求一个第二类Stirling数\(O(N\log N)\)

\(S\left(n,k\right)=\frac{1}{k!}\sum\limits_{i=0}^k\left(-1\right)^i\text{C}_k^i\left(k-i\right)^n=\sum\limits_{i=0}^k\frac{(-1)^{k-i}i^n}{i!(k-i)!}\)

递推求N以下的第二类Stirling数\(O(N^2)\)

\(s2[n][k]\)\(S(n,k)\)

状态转移:\(s2[i][j]=s2[i-1][j-1]+j*s2[i-1][j]\):前一项表示将第i个数放入新的子集;后一项表示将第i个数放入原来的子集,一共有j种放法。

边界:\(s2[0][0]=1\)\(\forall i≥1,s2[i][0]=0\)

3.8.3应用

  • 上升幂(\(x^{\overline{n}}=\prod\limits_{k=0}^{n-1}(x+k)=\frac{(x+n-1)!}{(x-1)!}\))与普通幂的转化:

    \(x^{\overline{n}} = \sum\limits_{k=0}^{n} \begin{bmatrix} n \\ k \end{bmatrix} x^k\)\(x^n = \sum\limits_{k=0}^{n} (-1)^{n-k} \begin{Bmatrix} n \\ k \end{Bmatrix} x^{\overline{k}}\)

  • 下降幂(\(x^{\underline{n}}=\prod\limits_{k=0}^{n-1}(x-k)=\frac{x!}{(x-n)!}\))与普通幂的转化:

    \(x^{\underline{n}} = \sum\limits_{k=0}^{n} (-1)^{n-k} \begin{bmatrix} n \\ k \end{bmatrix} x^k\)\(x^n = \sum\limits_{k=0}^{n} \begin{Bmatrix} n \\ k \end{Bmatrix} x^{\underline{k}}\)

3.9.三角形数

定义

边长为n个点的等边三角形点阵的点数,也即前n个正整数的和,记作第n个三角形数。

第n个三角形数的计算公式是\(\frac{n(n+1)}{2}=C_{n+1}^2= \frac{(2n+1)^2-1}{8}\)

性质

  • 前n个立方数是第n个三角形数的平方。
  • 第n个三角形数加第n+1个三角形数的和是第n+1个平方数。
  • 任何正整数是最多三个三角形数的和。

3.10.容斥原理

适用条件:求满足多个条件的方案数。

“至少/多”和“恰好”之间的转换。

\(|S_1 \cup S_2 \cup ... \cup S_n|=\sum\limits_{i=1}^{n}|S_i|-\sum\limits_{1\le i<j\le n}|S_i \cap S_j|+...+(-1)^{n+1}*|S_1 \cap S_2 \cap ...\cap S_n|\)

\(|S_1 \cap S_2 \cap ... \cap S_n|=|U|-|\=S_1 \cup \=S_2 \cup ...\cup \=S_n|\)

如果每个条件\(S_i\)本质相同,则用组合数直接计算方案而不需要枚举状态,复杂度是\(O(N)\)

否则,需要枚举状态,复杂度是\(O(2^N)\),见下。

一般容斥原理的题目给出N不会很大。

代码实现时,我们可以枚举x=\(0\)~\(2^N-1\),若x在二进制表示下共有p位为1,分别是第\(i_1,i_2,...,i_p\)位,则这个x代表上式的一项:\((-1)^p*|S_{i_1} \cap S_{i_2} \cap ... \cap S_{i_p}|\)\(O(2^N)\)

for(int i=0;i<=(1<<n)-1;i++)
{
    int sign=1,res=0;
    for(int j=0;j<n;j++)
        if((i>>j)&1)
        {
            sign*=-1;
            res=calc(res,j);
        }
    ans+=res*sign;
}

应用

  1. \(a_1+a_2+...+a_k=n,(a_1 \le x)\)的正整数解的数量。见《数学3.1.3.应用》隔板法。
  2. \(a_1+a_2+...+a_k=n,(a_1 \le x)\)的非负整数解的数量。见《数学3.1.3.应用》隔板法。
  3. 求多重集组合数。见《数学3.1.1.6.多重集》。

3.11.十二重计数法

  • 盒子可以为空

    球不同,盒子不同:\(m^n\)

    球相同,盒子不相同:组合数隔板法。

    球不相同,盒子相同:斯特林数。

    • 球相同,盒子相同:划分数。

      \(p_{n,m}\):将 n 划分成 m 个自然数的可重集的方案数。答案就是\(p_{n,m}\)

      \(p_{i,j}=p_{i-j,j}+p_{i,j-1}\):将 j 个 自然数同时 +1/加入一个 0 到可重集中。

4.0/1分数规划

定义

分数规划用来求一个分式的极值。

形如,给出一组\(a_i\)\(b_i\),求一组\(w_i\in \{ 0,1 \}\),使\(\frac{\sum\limits_{i=1}^{n} (a_i*w_i)}{\sum\limits_{i=1}^{n} (b_i*w_i)}\)最另外一种描述:每种物品有两个权值 a和b,选出若干个物品使得\(\frac{\sum a}{\sum b}\)最大或最小。

一般分数规划问题还会有一些奇怪的限制,比如“至少选k个物品”,“分母至少为W”。

二分求解

假设我们要求最大值。二分一个答案mid(实数域),然后推式子(为了方便少写了上下界):\(\frac{\sum(a_i*w_i)}{\sum(b_i*w_i)}> mid \Rightarrow \sum (a_i*w_i)-mid*\sum(b_i*w_i)>0\)$\Rightarrow $$\sum (w_i(a_i-midb_i))> 0$

那么只要求出不等号左边的式子的最大值就行了。如果最大值比0 要大,说明mid是可行的,mid=l;否则不可行,mid=r

求出不等号左边的式子的最大值:可以直接计算\(a_i-mid*b_i\),把i=1~n中所有的正数相加即可。

int n;
int a[N],b[N];
double c[N];

bool check(double x)
{
    double res=0;
    for(int i=1;i<=n;i++) c[i]=a[i]-x*b[i];
    for(int i=1;i<=n;i++) if(c[i]>0) res+=c[i];
    return res>0;
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);
    
    double l=0,r=1e9;
    while(r-l>EPS)
    {
        double mid=(l+r)/2;
        if(check(mid)) l=mid;
        else r=mid;
    }
    printf("%.2lf\n",round(r));//四舍五入
    
    return 0;
}

4.1.至少选k个物品

check函数里把第i个物品的权值设为\(a_i-mid*b_i\),然后选最大的k个即可得到最大值。

bool check(double x)
{
    double res=0;
    for(int i=1;i<=n;i++) c[i]=a[i]-x*b[i];
    sort(c+1,c+n+1);
    reverse(c+1,c+n+1);
    for(int i=1;i<=k;i++) res+=c[i];
    return res>0;
}

4.2.分母至少为W

check函数里可以考虑 01 背包。把\(b_i\)作为第i个物品的重量,\(a_i-mid*b_i\)作为第i个物品的价值,然后问题就转化为背包了。此时f[n][W]就是\(\sum (w_i*(a_i-mid*b_i))\)的最大值 。

\(\sum w_i*b_i\)可能超过W,此时直接视为W。

double f[N];

bool check(double x)
{
    for(int i=1;i<=W;i++) f[i]=-1e9;
    for(int i=1;i<=n;i++)
        for(int j=W;j>=0;j--)
        {
            int k=min(W,j+b[i]);
            f[k]=max(f[k],f[j]+a[i]-mid*b[i]);
        }
    return f[W]>0;
}

4.3.最优比率生成树

最优比率生成树

4.4.环上01分数规划

给定一张有向图,有点权a[]和边权b[],求图上的一个环,使“环上各点权值之和”除以“环上各边权值之和”最大。

二分答案mid,每轮二分建立一张新图,结构与原图相同,但是没有点权,有向边e=(x,y)边权为a[x]-mid*b[e]。

check函数判定条件:某个环上\(\sum (w_i*(a_i-mid*b_i))>0\)\(\Leftrightarrow\)SPFA求最长路判定正环。

bool check(double limit)
{
    memset(dis,0,sizeof dis);
    memset(cnt,0,sizeof cnt);
    memset(vis,false,sizeof vis);
    
    for(int i=1;i<=n;i++)
    {
        q.push(i);
        vis[i]=true;
    }
    
    while(!q.empty())
    {
        int t=q.front();
        q.pop();
        vis[t]=false;
        for(int i=h[t];i!=0;i=ne[i])
        {
            int v=e[i];
            if(dis[v]<dis[t]+w_v[t]-limit*w_e[i])
            {
                dis[v]=dis[t]+w_v[t]-limit*w_e[i];//在这里加上边权
                cnt[v]=cnt[t]+1;
                if(cnt[v]>=n) return true;
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=true;
                }
            }
        }
    }
    return false;
}
  1. 有时推出的式子是\(f[i]=a+b*f[i]\),看起来像自己调用自己,计算机会无限递归。但是我们人可以给它移项\(\Rightarrow (1-b)*f[i]=a \Rightarrow f[i]=\frac{a}{1-b}\)

  2. \(f_{i,j}←f_{i-1,?}\Rightarrow f_{i,j}←f_{i,j-1}\),有时会有意想不到的优化。

    \(e.g.\)一个\(O(N)\)转移的式子\(f[i][j]=\sum\limits_{k=0}^j (f[i-1][k]*a^{j-k}*b)\)

    把j-1代入得:\(f[i][j-1]=\sum\limits_{k=0}^{j-1} (f[i-1][k]*a^{j-1-k}*b)\)

    所以\(f[i][j]=f[i][j-1]*a+f[i-1][j]*b\)。由此做到\(O(1)\)转移。

  3. 期望*总方案数=权值和。

    知二求一。

    有的题目通过实际意义求出期望,再乘上总方案数得到权值和。例题。

  4. 只有古典概型(各种情况的概率相等)的概率才可用“符合条件的方案数/总方案数”计算。

  5. \((\sum\limits_i i*P(x==i))==(\sum\limits_i P(x≥i))\)

    证明。

5.概率论

5.1.概率

设样本空间为\(\Omega\),则对于\(\Omega\)中每一个随机事件A,都存在实值函数P(A),满足:

  1. 0≤P(A)≤1;
  2. P(\(\Omega\))=1;
  3. 对于两两互斥事件\(A_1,A_2,...,A_n\)\(\sum\limits_{i=1}^{n} P(A_i)=P(A_1 \cup A_2 \cup ... \cup A_n)\)

5.1.1.借助有向无环图的反图做到O(朴素dp)预处理O(1)查询

5.2.数学期望

一个格子上有一个非负权值,取走它该格子的权值就会等概率随机变成不比当前的权值大的一个非负权值。设\(f[x]\)表示该格子上的权值为x时能取走的总权值的最大期望,则\(f[x]=x+\sum\limits_{i=0}^{x} \frac{f[i]}{x+1}=2*x\)

定义

若随机变量X取值有\(x_1,x_2,...,x_n\),一个随机事件可表示为\(X==x_i\),其概率为\(P(X==x_i)=p_i\),则称\(E(X)=\sum\limits_{i=1}^n (p_i*x_i)\)为随机变量X的数学期望,即随机变量取值与概率的乘积之和。

\(e.g.\)掷两枚骰子,掷出的点数的数学期望\(E(X)=\dfrac{1}{36}*2+\dfrac{1}{18}*3+\dfrac{1}{12}*4+\dfrac{1}{9}*5+\dfrac{5}{36}*6+\dfrac{1}{6}*7+\dfrac{5}{36}*8+\dfrac{1}{9}*9+\dfrac{1}{12}*10+\dfrac{1}{18}*11+\dfrac{1}{36}*12=7\)

性质(可用dp递推计算数学期望的基础)

数学期望是线性函数,满足E(ax+by)=aE(x)+bE(y)。

\(e.g.\)掷两枚骰子,掷出的点数的数学期望可以很简单的求解:
设随机变量X表示掷一枚骰子的点数的数学期望\(E(X)=\dfrac{1+2+3+4+5+6}{6}=3.5\),则掷两枚骰子的点数可表示为随机变量2X,掷一枚骰子的点数的数学期望\(E(2X)=2*E(X)=2*3.5=7\)

5.2.1.有向无环图上数学期望的计算

数学期望的计算一般可看作有向无环图,可用记忆化搜索dp递归反图上拓扑排序来计算。

注意:在数学期望递推过程中,我们通常把终止状态作为初值,起始状态作为目标,倒着计算(用递归或反图来实现倒着计算)。这是因为起始状态是唯一的,终止状态很多。若正着计算,还要计算起始状态到终止状态的概率,与F值相乘才能得到答案;若倒着计算,起始状态由于唯一,它的概率自然为1,直接输出F即可。

记忆化搜索dp写法:

const double INF=1e18;
double ans;
double f[N];

double dp(int u)
{
    if(f[u]>=0) return f[u];//记忆化搜索
    f[u]=0;//或根据题意f[u]=1,清零操作
    //abaabaaba
    if(/*aba*/)
    {
        f[u]=INF;
        return f[u];
    }
    return f[u];
}

int main()
{
    memset(f,-1,sizeof f);//!!!记忆化搜索别忘记初始化!!!
    double ans=dp(1);//从起点开始递归
    if(ans>INF/2) puts("-1");//注意如果计算时有负数,判定条件要INF/2。
    else printf("%.2lf\n",ans);
    return 0;
}

5.2.1.1.1→N有向无环图路径长度的数学期望

f(i):从i到N的路径长度的数学期望。

边界:f(N)=0。

答案:f(1)。

计算:设i有k条出边,则\(f(i)=E(i)=\sum\limits_{i=1}^{k}E(\dfrac{1}{k}\)\(*(w_i+x_i))=\sum\limits_{i=1}^{k}(\dfrac{1}{k}E(w_i+x_i))=\sum\limits_{i=1}^{k}(\dfrac{1}{k}*\)\((w_i+E(x_i)))=\sum\limits_{i=1}^{k}(\dfrac{1}{k}(w_i+E(v_i)))=\sum\limits_{i=1}^{k}(\dfrac{1}{k}*(w_i+f(v_i)))\)

记忆化搜索dp写法:

int dout[N];
double f[N];

double dp(int u)
{
    if(f[u]>=0) return f[u];
    f[u]=0;
    for(int i=h[u];i!=0;i=ne[i])
    {
        int v=e[i];
        f[u]+=(w[i]+dp(v))/dout[u]; //不可以放在外面统一除,因为叶节点dout为0会浮点数错误
    }
    return f[u];
}

int main()
{
    memset(f,-1,sizeof f);//!!!记忆化搜索别忘记初始化!!!
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v,wor;
        scanf("%d%d%d",&u,&v,&wor);
        add(u,v,wor);
        dout[u]++;
    }
    printf("%.2lf\n",dp(1));
    return 0;
}

拓扑排序写法:

终点出发,在反图上执行拓扑排序,排序中求解f(i)。

int dout[N],deg[N];
double f[N];
queue<int> q;

void topsort()
{
    q.push(n);
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i=h[u];i!=0;i=ne[i])
        {
            int v=e[i];
            f[v]+=(f[u]+w[i])/deg[v];
            dout[v]--;
            if(dout[v]==0) q.push(v);
        }
    }
    return ;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v,wor;
        scanf("%d%d%d",&u,&v,&wor);
        add(v,u,wor);
        dout[u]++,deg[u]++;
    }
    topsort();
    printf("%.2lf\n",f[1]);
    return 0;
}

5.3.有后效性的概率/数学期望的计算

有时概率/数学期望列出的式子在同一维度上具有后效性,此时应用高斯消元优化dp。

最常见的题目类型是“随机游走”类。

首先列出等式(或称方程逆推式,而不是递推式,这样可以避免“后效性”误导理解)。n个未知数(即\(f_i\))应该列出n个式子。

5.3.1.高斯消元\(O(N^3)\)

  1. 对式子移项变形,使得其满足高斯消元的矩阵形式。
  2. \(f_i\)看作未知量,根据列出的n个等式(或称方程),高斯消元解出未知量\(f_i\)
  • 例题:游走

    首先考虑如何给边编号:贪心:一条边被走过的期望次数越大,编号应该越小。现在需要对于每一条无向边i求出其被走过的期望次数\(f_i\)

    但是如果直接求解\(f_i\),又难又慢。数据范围N=500启发考虑求出每个点u被走过的期望次数\(g_u\)。边i(u,v)满足\(f_i=\frac{1}{deg_u}g_u+\frac{1}{deg_v}g_v\)

    列出等式:$\begin{cases}
    g_1=1+\sum\limits_{(u,1)}\frac{1}{deg_u}g_u
    \g_v=\sum\limits_{(u,v)}\frac{1}{deg_u}g_u&1<v<n
    \g_n=0
    \end{cases} \(。\)g_n=0\(是因为根据边i(n,v)\)f_i=\frac{1}{deg_n}g_n+\frac{1}{deg_v}g_v\(,由于走到了点n就结束,因此\)g_n$必须为0。

    对式子移项变形,使得其满足高斯消元的矩阵形式:$\begin{cases}
    -g_1+\sum\limits_{(u,1)}\frac{1}{deg_u}g_u=-1
    \-g_v+\sum\limits_{(u,v)}\frac{1}{deg_u}g_u=0&1<v<n
    \g_n=0
    \end{cases} $。

    高斯消元求出\(g_u\),进而求出\(f_i\),然后按照\(f_i\)从大到小的顺序给边i从小到大编号,最后可以直接求得\(ans=\sum\limits_i^m i*f_i\)

int n,m;
double ans;
struct Edge
{
    int u,v;
    double f;   //无向边i被走过的期望次数
    bool operator < (const Edge &qw) const
    {
        return f>qw.f;
    }
}e[M];
int deg[N];
double g[N][N]; //高斯消元矩阵,点u被走过的期望次数

scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
    int u,v;
    scanf("%d%d",&u,&v);
    deg[u]++,deg[v]++;
    e[i]={u,v};
}

//根据列出的式子给高斯消元矩阵填入系数
for(int i=1;i<n;i++) g[i][i]=-1;
g[1][n+1]=-1,g[n][n]=1,g[n][n+1]=0;
for(int i=1;i<=m;i++)
{
    int u=e[i].u,v=e[i].v;
    if(u!=n) g[u][v]=1.0/deg[v];
    if(v!=n) g[v][u]=1.0/deg[u];
}

//高斯消元求出g_u
gauss();

//f_i=\frac{1}{deg_u}g_u+\frac{1}{deg_v}g_v求出f_i
for(int i=1;i<=m;i++)
{
    int u=e[i].u,v=e[i].v;
    e[i].f=1.0/deg[u]*g[u][n+1]+1.0/deg[v]*g[v][n+1];
}

//贪心:按照f_i从大到小的顺序给边i从小到大编号
sort(e+1,e+m+1);
for(int i=1;i<=m;i++) ans+=i*e[i].f;
printf("%.3lf\n",ans);

5.3.2.手动高斯消元\(O(N)\)

  1. 若已知量\(f_x=y\)在前面,设\(f_i=k_i*f_{i+1}+b_i\)(这样\(k_i\)就是从前面的已知量递推到后面了);若已知量在后面,设\(f_i=k_i*f_{i-1}+b_i\)。下面以已知量在前面为例。
  2. \(f_{i-1}=k_i*f_i+b_i\)代入等式,消除\(f_{i-1}\)
  3. 边界:\(k_x=0,b_x=y\)。从前往后递推求出\(k_i,b_i\)
  4. 边界:\(f_n=b_n\)(因为不存在\(f_{n+1}\),所以\(k_n=0\))。从后往前递推求出\(f_i\)

注意

  1. 一般需要特判概率=0或1时的情况。
  2. 注意原式的合法性。
  3. 若已知量在前面,设\(f_i=k_i*f_{i-1}+b_i\)也可以做。
  4. 树上高斯消元也是类似的,只不过一定设的是\(f_u=k_u*f_{fa_u}+b_u\)
  • 另一种方法

    1. 对式子移项变形,使得其无后效性,变成我们最熟悉的递推式。

      e.g.\(f_i=f_{i-1}+i*f_{i+1}\Leftrightarrow f_{i-1}=f_i-i*f_{i+1}\Leftrightarrow f_i=f_{i+1}-(i+1)*f_{i+2}\)

      一般n个原式中只有n-1个式子可以转化为递推式,剩下的1个式子因为边界原因无法转化,称它为边界式。

      因此,使用递推式时,要先注意原式的合法性。

    2. 列出3种式子:已知量、递推式、原式的边界式。

    3. 设x。利用已知量、x和递推式递推求出\(f_i=k_i*x+b_i\)

    4. \(f_i\)代入原式的边界式解出x,进而求出\(f_i\)的具体值。

    注意:

    1. 一般需要特判概率=0或1时的情况。
    2. 使用递推式时,要先注意原式的合法性。
    • 例题

      给定n和p。点i(i<n)有p的概率走到i+1,有1-p的概率走到i-1,一旦走到点0就失败,点n就成功。求出每个点i(1≤i≤n-1)成功走到点n的概率。

      \(f_i\):初始在点i时成功走到点n的概率。

      列出等式:\(\begin{cases} f_n=1 \\f_i=p*f_{i+1}+(1-p)*f_{i-1}&n>i>1 \\f_1=p*f_2 \end{cases}\)

      对式子移项变形,使得其无后效性,变成我们最熟悉的递推式:$\begin{cases}
      f_n=1
      \f_{i-1}=\frac{f_i}{1-p}-\frac{pf_{i+1}}{1-p}\Leftrightarrow f_i=\frac{f_{i+1}}{1-p}-\frac{pf_{i+2}}{1-p}&n-2\ge i\ge 1
      \f_1=p*f_2
      \end{cases} $。

      发现如果还知道\(f_{n-1}\)的值,就可以通过\(f_{n-1},f_n=1\)和第二个式子求出\(f_{n-2}\),进而通过第二个式子递推求出\(f_i\)。现在有n-1个未知数\(f_{1\sim n-1}\),但是第二个式子只有n-2个,还差第三个式子\(f_1=p*f_2\)没有被利用。因此设\(f_{n-1}=x\),通过第二个式子递推求出\(f_i=k_i*x+b_i\)

      然后把\(f_1,f_2\)代入到第三个式子\(f_1=p*f_2\)解出x,进而求出\(f_i\)的具体值。

      需要特判p=0或1时的情况。

5.4.min-max容斥

\(\max\limits_{i\in S}x_i=\sum\limits_{T\subset S}(-1)^{|T|-1}\min\limits_{j\in T}x_j\)

\(\min\limits_{i\in S}x_i=\sum\limits_{T\subset S}(-1)^{|T|-1}\max\limits_{j\in T}x_j\)

\(E(\max\limits_{i\in S}x_i)=\sum\limits_{T\subset S}(-1)^{|T|-1}E(\min\limits_{j\in T}x_j)\)

\(E(\min\limits_{i\in S}x_i)=\sum\limits_{T\subset S}(-1)^{|T|-1}E(\max\limits_{j\in T}x_j)\)

\(\max\limits_{i\in S}x_i\):“点亮”S中的最后一个点的……(由于集合之间相互影响,因此max较难求)

\(\min\limits_{i\in S}x_i\):“点亮”S中的第一个点的……

多组询问\(\max\limits_{i\in S}x_i\):预处理所有的\(\min\limits_{j\in T}x_j\),然后乘上各自的容斥系数,之后做一遍高维前缀和即可得到所有的\(\max\limits_{i\in S}x_i\)

  • 第 k 大的形式

6.博弈论

常考设问:“先手是否必胜”、“双方采用最优策略的情况下的最优得分”。

必胜态指的是当前处于这个状态时,先手必胜。

做题方向:

  1. sg函数

    适用条件:由多个互不影响的平行游戏组成。

  2. dp:(一般采用逆推\(f_{state,0/1}\):先手0/后手1从当前状态state走到终点的……

    若转移有环,则设\(f_{i,state,0/1}\):第i轮先手0/后手1从当前状态state走到终点的……

  3. 分类讨论

  4. 人类智慧,打表找规律,实际意义

    公平组合游戏适合打表。

证明局面必胜必败条件:

  1. 必胜必败条件满足终点。
  2. 证明满足必胜条件的局面走一步可以使得局面满足必败条件,必败条件的局面走一步一定使得局面满足必胜条件。

6.1.SG函数

适用条件:由多个互不影响的并行游戏组成。

核心:找到并行游戏,抓住必败态。

公平组合游戏ICG

若一个游戏满足:

  1. 由两名玩家交替行动;
  2. 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
  3. 不能行动的玩家判负;

则称该游戏为一个公平组合游戏。

NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

适合打表。

有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。

任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面有向边

mex运算

设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:

mex(S) = min{x}, x属于自然数,且x不属于S

求mex

  1. 二分,check(mid)函数判断≤mid的元素中的“最极端”的元素是否在集合内(即判断是否存在一个≤mid的元素不在集合内)/判断≤mid的元素是否都在集合内。

SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, ..., yk,定义SG(x)为x的后继节点y1, y2, ..., yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:

SG(x) = mex({SG(y1), SG(y2), ..., SG(yk)})

特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。

终止状态的SG函数值为0。

有向图游戏的和

设G1, G2, ..., Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, ..., Gm的和。

有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:

SG(G) = SG(G1) ^ SG(G2) ^ ... ^ SG(Gm)

定理

有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。

有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。

int n,m,k,ans;
int h[N],e[M],ne[M],idx;
int f[N];   //记忆化搜索,存u的sg函数

void add(int u,int v)
{
    e[++idx]=v;
    ne[idx]=h[u];
    h[u]=idx;
    return ;
}

int sg(int u)
{
    if(f[u]!=-1) return f[u];
    
    //预处理mex运算
    unordered_set<int> s;
    for(int i=h[u];i!=0;i=ne[i])
    {
        int v=e[i];
        s.insert(sg(v));
    }
    
    //mex运算
    for(int i=0;;i++)
        if(s.count(i)==0)
        {
            f[u]=i;
            break;
        }
    
    return f[u];
}

int main()
{
    memset(f,-1,sizeof f);  //!!!记忆化搜索别忘记初始化!!!
    
    scanf("%d%d%d",&n,&m,&k);
    
    //有向图游戏
    for(int i=0;i<m;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
    }
    
    //有向图游戏的和:异或和
    while(k--)
    {
        int u;
        scanf("%d",&u);
        ans^=sg(u);
    }
    
    if(ans==0) puts("lose");
    else puts("win");
    
    return 0;
}

有向图游戏不一定要建边,比如说《AcWing 893. 集合-Nim游戏》for(int i=1;i<=k;i++) if(x>=s[i]) S.insert(sg(x-s[i]));

6.2.Nim游戏

6.2.1.Nim游戏

给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为Nim博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。

所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。

Nim博弈不存在平局,只有先手必胜和先手必败两种情况。

定理: Nim博弈先手必胜,当且仅当 A1 ^ A2 ^ ... ^ An != 0。

方案:

  • \(A_1\operatorname{xor} A_2 \operatorname{xor} \cdots \operatorname{xor} A_n=0\),无论怎么取石子,得到的局面各堆石子异或起来都不等于0。

  • \(A_1\operatorname{xor} A_2 \operatorname{xor} \cdots \operatorname{xor} A_n=x≠0\),设x在二进制下最高位的1在第k位,显然至少存在一堆石子\(A_i\),它的第k位是1,因此\(A_i \operatorname{xor} x < A_i\)。所以从\(A_i\)堆中取出\(A_i-(A_i\operatorname{xor} x)\)个石子,使其变为\(A_i\operatorname{xor} x\),就得到了一个各堆石子异或起来等于0的局面。

    \(A_i \operatorname{xor} x\leq A_{i}\)\(\Leftrightarrow\)对于x里最高的二进制位1,A_i这一位也是1。

6.2.2.台阶-Nim游戏

有一个n级台阶的楼梯,每级台阶上都有若干个石子,其中第i级台阶上有ai个石子。两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

定理:台阶-Nim博弈先手必胜,当且仅当奇数层台阶上石子的异或和不为0。

6.3.其他博弈论模型

6.3.1.巴什博弈

特征:给定一堆n个石子,每轮可以从中拿走1~m个石子,不能操作的人判负。

先手必胜条件:n%(m+1)!=0。

应用:给定多种物品,每种物品有若干个,每轮的取物品规则看似复杂:或许可以将每种物品转化为系数乘第一种物品,且满足“可以等价为每轮从中拿走1~m个第一种物品”,则可转化为巴什博弈。

6.4.不平等博弈

一般情况某人必胜,特殊情况另一人必胜,基本没有平局。

6.5.容斥

6.5.1.min-max

6.5.2.\(\alpha-\beta\)剪枝

适用条件:状态表示是从当前局面到终点的最优……

7.信息论

核心:信息量之差,抽屉原理。

题型一:传递信息

适用条件:形如“二人暗示地、绝对聪明地传递信息”。

不必得知二人信息间的映射关系是怎样的。可以理解为二人心有灵犀,映射关系乱七八糟都可以,我说123,对方就知道123唯一映射888。

核心:传递信息的情况数大于等于需要传递的信息量。

证明正确性:证明每种实际传递的情况一一对应需要传递的信息。

题型二:查找次品

适用条件:正品中有一个(或一些)次品,有若干个具有特定的识别次品的模式的机器,求出查找次品所需的最少机器使用数。

核心:机器表达的信息的情况数大于等于次品的情况数。

证明正确性:构造方案使得每条机器表达的信息一一对应每种次品的情况。

题型三:证明复杂度

核心:抽屉原理。

抽屉原理,证明出n大于某个值时一定有解,由此缩小n的范围。

.计算几何

.1.基础知识

.1.1.浮点数的比较

double的精度是小数点后15位。

设置精度一般为min(1e-8,题目要求精度的\(10^{-4}\))。

const double eps = 1e-8;//设置精度
int sign(double x)  // 符号函数
{
    if (fabs(x) < eps) return 0;
    if (x < 0) return -1;
    return 1;
}
int dcmp(double x, double y)  // 比较函数
{
    if (fabs(x - y) < eps) return 0;
    if (x < y) return -1;
    return 1;
}

.1.2.两点距离

设两点\(A(x_1,y_1),B(x_2,y_2)\)

欧氏距离:\(\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}\)

曼哈顿距离:\(|x_2-x_1|+|y_2-y_1|\)

\(|x_2-x_1|+|y_2-y_1|=\max\{x_2-x_1+y_2-y_1,x_2-x_1+y_1-y_2,x_1-x_2+y_2-y_1,x_1-x_2+y_1-y_2\}\)

\(\max\limits_i\{|x_i-x|+|y_i-y|\}=\max\{\max\limits_i\{x_i+y_i\}-x-y,\max\limits_i\{x_i-y_i\}-x+y,\max\limits_i\{-x_i+y_i\}+x-y,\max\limits_i\{-x_i-y_i\}+x+y\}\)

切比雪夫距离:\(\max(|x_2-x_1|,|y_2-y_1|)\)

曼哈顿距离与切比雪夫距离的转化:两点\(A(x_1,y_1),B(x_2,y_2)\)的曼哈顿距离=两点\(A'(x_1+y_1,x_1-y_1),B'(x_2+y_2,x_2-y_2)\)的切比雪夫距离。

  • 证明

    方法一:

    \(|x_2-x_1|+|y_2-y_1|\\=\max\{x_2-x_1+y_2-y_1,x_2-x_1+y_1-y_2,x_1-x_2+y_2-y_1,x_1-x_2+y_1-y_2\}\\=\max(|(x_2+y_2)-(x_1+y_1)|,|(x_2-y_2)-(x_1-y_1)|)\)

    方法二:相似变换。

应用:去绝对值,使x,y两维独立。

.1.3.向量

\(\bm{a}=\left(x_1,y_1\right),\bm{b}=\left(x_2,y_2\right),\lambda\in R\)

  • 向量的加减法和数乘运算

    代数意义

    \(\bm{a}+\bm{b}=\left(x_1+x_2,y_1+y_2\right)\)

    \(\bm{a}-\bm{b}=\left(x_1-x_2,y_1-y_2\right)\)

    \(\lambda\bm{a}=\left(\lambda x_1,\lambda y_1\right)\)

    几何意义:平行四边形法则、三角形法则。

  • 向量的模

    代数意义

    \(\left|\bm{a}\right|=\sqrt{x_1^2+y_1^2}\)

    几何意义: 向量的长度。

  • 单位向量

    \(\vec{a_0}=\frac{\vec{a}}{|\vec{a}|}\)

  • 向量的内积(点乘)

    代数意义

    \(\bm{a}\cdot\bm{b}=\left|\bm{a}\right|\left|\bm{b}\right|\cos\theta=x_1x_2+y_1y_2\)

    几何意义:一个向量在另一个向量上的投影长度和另一个向量长度的乘积。可类比力的做功。

  • 向量的外积(叉乘)

    叉乘没有交换律!!!

    代数意义

    \(\bm{a}\times\bm{b}=\left|\bm{a}\right|\left|\bm{b}\right|\sin\theta=x_1y_2-x_2y_1\)

    几何意义:两个向量所围成的平行四边形的有向面积。注意有向仅体现于面积的正负。可类比力矩。

    注:其实这里的叉积的理解是狭隘的:

    \(\bm{a}\times\bm{b}\)事实上是一个垂直\(\bm{a}\)\(\bm{b}\)所成平面向量,而非一个标量

    这个向量满足右手定则:当右手的四指\(\bm{a}\)不超过\(180\)度的转角转向\(\bm{b}\)时,竖起的大拇指指向是\(\bm{a}\times\bm{b}\)的方向。垂直纸面向外有向面积为正,向内为负

  • 两个向量的夹角

    \(\theta=\arccos\frac{\bm{a}\cdot\bm{b}}{\left|\bm{a}\right|\left|\bm{b}\right|}\)

  • 向量的投影和投影数量

    三点A、B、C,求\(\overrightarrow{AC}\)\(\overrightarrow{AB}\)方向上的投影和投影数量。

    投影数量\(=\overrightarrow{AC}*\dfrac{\overrightarrow{AB}}{|\overrightarrow{AB}|}\)

    投影\(=\overrightarrow{AC}*\dfrac{\overrightarrow{AB}}{|\overrightarrow{AB}|}*\overrightarrow{AB_0}=\overrightarrow{AC}*\dfrac{\overrightarrow{AB}}{|\overrightarrow{AB}|}*\dfrac{\overrightarrow{AB}}{|\overrightarrow{AB}|}\)

  • 向量和点的旋转

    将向量\(\bm{a}\)顺时针旋转\(\theta\)角(逆时针\(\theta\)取负):\(\begin{bmatrix}x & y\end{bmatrix}\times\begin{bmatrix}\cos\theta & -\sin\theta \\\sin\theta & \cos\theta\end{bmatrix}=\left(x\cos\theta+y\sin\theta,-x\sin\theta+y\cos\theta\right)\)

    点的旋转相当于原点与该点形成的向量的旋转。

  • 两个向量是否平行

    • \(\bm{a}\times\bm{b}=0\)
    • \(\bm{a}=\lambda\bm{b}\Leftrightarrow y_1x_2=y_2x_1\)
  • 两个向量是否垂直

    \(\bm{a}\cdot\bm{b}=0\)

常用函数参考代码

//向量储存
struct Point//既可以是结构体在里面定义内置函数(适用于题目需要用到较多的向量运算)
{
    int x,y;
    bool operator + (Point const &poi) const
};
pair<int,int> PII;//也可以使用pair类型,向量的加减法手动运算(适用于题目只用到一两个向量运算)

double dot(Point a, Point b) // 向量点乘
{
    return a.x * b.x + a.y * b.y;
}
double cross(Point a, Point b) // 向量叉乘
{
    return a.x * b.y - b.x * a.y;
}
double length(Point a) // 向量取模
{
    return sqrt(dot(a, a));
}
Point unit(Point a)//单位向量
{
    return a/length(a);
}
double project(Point a,Point b,Point c) //投影数量
{
    return (dot(b-a,c-a))/length(b-a);
}
Point v=project(a,b,c)*unit(c-a);//投影
double get_angle(Point a, Point b)//计算向量夹角(点乘的变形),范围是[0,Π]
{
    return acos(max(-1.0, min(1.0, dot(a, b) / get_length(a) / get_length(b))));
}
double area(Point a, Point b, Point c) // 计算由 a, b, c 三点确定下的平行四边形的面积
{
    return cross(b - a, c - a);
}
Point rotate(Point a, double angle) // 向量的顺时针旋转,返回旋转后的向量
{
    return Point(a.x * cos(angle) + a.y * sin(angle), -a.x * sin(angle) + a.y * cos(angle));
}

.1.4.直线

  • 直线的表示

    • 一般式:\(ax+by+c=0\)

    • 斜截式:\(y=kx+b\)

      斜截式转两点式:\(st=(0,b),ed=(1,k+b)\)

    • 点向式(常用):给定直线上一个点\(P_0\)和与这条直线平行的一个非零向量\(\bm{v}\),则该直线上的所有点都可以表示成\(P_0+t\bm{v},t\in R\)

      点向式表示射线:规定\(t\)的符号。

      点向式表示线段:规定\(t\)的值域。

      两点式转点向式:\(P_0=st,\bm{v}=ed-st\)

  • 三点是否共线

    \(\overrightarrow{P_1P_2}\parallel\overrightarrow{P_1P_3}\)

  • 点和直线的关系

    \(P\left(x,y\right)\)在直线上\(\Leftrightarrow\)求出一条直线的点向式\(P_0+t\bm{v}\)\(,则\)P在直线上当且仅当\(\bm{v}\times\overrightarrow{PP_0}=0\)

    \(P\)在该直线的方向向量\(\bm{v}\)的左侧\(\Leftrightarrow\)\(\bm{v}\times\overrightarrow{PP_0}>0\)。否则,在另一侧。

    n个直线把平面划分成若干个区域,判断一点在哪个区域:根据该点在这n个直线的哪一侧情况判断。

  • 直线(或线段)与直线的关系并返回两条相交直线的交点,两条直线以点向式给出。

    设两条直线为\(P+t\bm{v},Q+t\bm{w}\)。记向量\(\overset{-\!-\!\rightarrow}{QP}\)\(\bm{u}\)

    如图,\(\triangle PAO\sim\triangle CBQ\),因此有\(\dfrac{PO}{QC}=\dfrac{PA}{CB}=\dfrac{S_{\triangle PQD}}{S_{\triangle CQD}}=\dfrac{\bm{u}\times\bm{w}}{\bm{w}\times\bm{v}}\)

    • 参考代码.
Point get_line_intersection(Point p, Vector v, Point q, vector w)
{
    if(sign(v*w)==0) return {INF,INF};//两条平行线
    vector u = p - q;
    double t = cross(w, u) / cross(v, w);
    Point o=p+v*t;
    // if(!on_segment(o,p,p+v) || !on_segment(o,q,q+w)) return {INF,INF};//两条线段不相交
    return o;
}
  • 点到直线的距离

    求点\(C\)到直线\(AB\)的距离。

    设向量\(\bm{v_1}=\overset{-\!-\!\rightarrow}{AB},\bm{v_2}=\overset{-\!-\!\rightarrow}{AC}\),则距离为\(\dfrac{\bm{v_1}\times\bm{v_2}}{\left|\bm{v_1}\right|}\)(从平行四边形的有向面积考虑)

    • 参考代码
double distance_to_line(Point p, Point a, Point b)
{
    vector v1 = b - a, v2 = p - a;
    return fabs(cross(v1, v2) / get_length(v1));
}
  • 点到线段的距离

    求点\(C\)到线段\(AB\)的距离。

    \(\bm{v_1}=\overset{-\!-\!\rightarrow}{AB},\bm{v_2}=\overset{-\!-\!\rightarrow}{AC},\bm{v_3}=\overset{-\!-\!\rightarrow}{BC}\),分以下几种情况:

    1. \(A\)\(B\)重合,答案为\(\left|\bm{v_2}\right|\)
    2. \(\bm{v_1}\cdot\bm{v_2}<0\),此时\(C\)在直线\(AB\)上的垂足落在了\(BA\)的延长线上,因此距离为\(\left|\bm{v_2}\right|\)
    3. \(\bm{v_1}\cdot\bm{v_3}>0\),此时\(C\)在直线\(AB\)上的垂足落在了\(AB\)的延长线上,因此距离为\(\left|\bm{v_3}\right|\)
    4. 否则,距离为\(C\)到直线\(AB\)的距离。
    • 参考代码
double distance_to_segment(Point p, Point a, Point b)
{
    if (a == b) return get_length(p - a);
    Vector v1 = b - a, v2 = p - a, v3 = p - b;
    if (sign(dot(v1, v2)) < 0) return get_length(v2);
    if (sign(dot(v1, v3)) > 0) return get_length(v3);
    return distance_to_line(p, a, b);
}
  • 点在直线上的投影

    求点\(C\)在直线\(AB\)上的投影点。

    \(\overset{-\!-\!\rightarrow}{AC}\)\(\overset{-\!-\!\rightarrow}{AB}\)上的投影数量为\(\left|\overrightarrow{AC}\right|\cos\theta=\dfrac{\overrightarrow{AC}\cdot\overrightarrow{AB}}{\left|\overrightarrow{AB}\right|}\)。再累加上起点坐标\(A\)即可。

    • 参考代码
Point get_line_projection(Point p, Point a, Point b)
{
    Vector v = b - a;
    return a + v * (dot(v, p - a) / dot(v, v));
}
  • 点是否在线段上

    判断点\(C\)是否在线段\(AB\)上。

    共线:叉积为零;不在延长线上:点积非正。

    • 参考代码
bool on_segment(Point p, Point a, Point b)
{
    return sign(cross(p - a, p - b)) == 0 && sign(dot(p - a, p - b)) <= 0;
}
  • 两线段是否相交

    判断线段\(AB\)与线段\(CD\)是否相交。

    两线段相交,等价于\(A,B\)位于\(CD\)的两侧且\(C,D\)位于\(AB\)的两侧。判断点位于直线两侧用外积的正负来判断。我们称这个做法叫跨立实验

    • 参考代码

      注:如果端点相交不算的话,判定条件从\(\le\)改成\(<\)即可。

bool segment_intersection(Point a1, Point a2, Point b1, Point b2)
{
    double c1 = cross(a2 - a1, b1 - a1), c2 = cross(a2 - a1, b2 - a1);
    double c3 = cross(b2 - b1, a2 - b1), c4 = cross(b2 - b1, a1 - b1);
    return sign(c1) * sign(c2) <= 0 && sign(c3) * sign(c4) <= 0;
}

  • 求线段的中垂线

    中垂线用点向式表示。则点是线段的中点,向量是线段所在直线顺时针旋转\(\frac{\pi}{2}\)的向量。

Line get_mid(Point a,Point b)
{
    return {(a+b)/2,rotate(b-a,PI/2)};
}

.1.5.角

  • 三角函数

    C++三角函数库是弧度制

反三角函数 定义域 值域
asin \([-1,1]\) \([-\frac{\pi}{2},\frac{\pi}{2}]\)
acos \([-1,1]\) \([0,\pi]\)
atan \((-\infty,\infty)\) \((-\frac{\pi}{2},\frac{\pi}{2})\)
注意:`asin`和`acos`的参数θ不在定义域内是未定义行为,**精度问题可能导致θ不在定义域内,因此需要手写****`asin`****和****`acos`****。**
double arcsin(double a)
{
    return asin(max(-1.0,min(1.0,a)));
}

double arccos(double a)
{
    return acos(max(-1.0,min(1.0,a)));
}
  • 圆周率\(\pi\)acos(-1)

  • 余弦定理:\(c^2=a^2+b^2-2ab\cos C\)

  • 正弦定理:\(\dfrac{a}{\sin A}=\dfrac{b}{\sin B}=\dfrac{c}{\sin C}\)

  • atan2(y,x):方位角,极角,广义\(\arctan(\frac{y}{x})\):从(0,0)指向(1,0)的向量与从(0,0)指向(x,y)的向量的有向夹角。值域\((-\pi,\pi]\)。除原点和负半横轴外,逆时针方向递增。

    \(\operatorname{atan2}(y,x)=\begin{cases} \arctan(\frac{y}{x})-\pi&x<0,y<0\\ -\frac{\pi}{2}&x=0,y<0\\ \arctan(\frac{y}{x})&x>0\\ \frac{\pi}{2}&x=0,y>0\\ \arctan(\frac{y}{x})+\pi&x<0,y\geqslant0\\ \operatorname{undefined}&x=0,y=0 \end{cases}\)

    极角排序

    • 直接计算极角:

      优点:常数小,简单直观。

int acmp(Point u,Point v)//返回true表示向量u<v
{
    return dcmp(atan2(u.y,u.x),atan2(v.y,v.x));
}
- 间接利用叉积:

    优点:稳健性强,精度高,可实现全整数运算。
bool zero(Point u)
{
    return u.x==0 && u.y==0;
}

//模拟atan2
bool half(Point u)
{
    return u.y>0 || (u.y==0 && u.x<0);
}
/*令起始方向为向量st的方向
Point st;
bool half(Point u)
{
    return cross(st,u)<0 || (cross(st,u)==0 && dot(st,u)<0);
}
*/

bool acmp(Point u,Point v)//返回true表示向量u<v
{
    if(zero(u) || zero(v)) return zero(u)>zero(v);
    else if(half(u)!=half(v)) return half(u)<half(v);
    else if(cross(u,v)!=0) return cross(u,v)>0;
    else return length(u)<length(v);
}

.1.6.多边形

  • 三角形

    • 面积
      • 叉积(用途广泛)
      • 海伦公式:设\(p\)为三角形周长的一半,三角形三边长\(a,b,c\),则面积\(S=\sqrt{p\left(p-a\right)\left(p-b\right)\left(p-c\right)}\)
    • 三角形四心
      • 外心,外接圆圆心

        三边中垂线交点。到三角形三个顶点的距离相等

      • 内心,内切圆圆心

        角平分线交点,到三边距离相等

      • 垂心

        三条垂线交点

      • 重心

        三条中线交点(到三角形三顶点距离的平方和最小的点,三角形内到三边距离之积最大的点)

        \((\frac{x_1+x_2+x_3}{3},\frac{y_1+y_2+y_3}{3})\)

  • 普通多边形

    通常按逆时针存储所有点。

    • 定义
      • 多边形

        由在同一平面且不在同一直线上的多条线段首尾顺次连接且不相交所组成的图形叫多边形。

        n个边能形成多边形充要条件是任何一条边的长度小于其他边的长度的总和。

      • 简单多边形

        简单多边形是除相邻边外其它边不相交的多边形。

      • 凸多边形

        过多边形的任意一边做一条直线,如果其他各个顶点都在这条直线的同侧,则把这个多边形叫做凸多边形。

        任意凸多边形外角和均为\(360\degree\)

        任意凸多边形内角和为\(\left(n-2\right)360\degree\)

    • 常用函数
      • 求多边形面积(不一定是凸多边形)

        三角剖分。

        我们可以从第一个顶点出发把凸多边形分成\(n-2\)个三角形,然后把面积加起来。

        求一个三角形的面积可以利用向量叉积实现。叉积求得面积的有向性使得我们求凹多边形也是对的。

double polygon_area(Point p[], int n)
{
    double s = 0;
    for (int i = 1; i + 1 < n; i ++ )
        s += cross(p[i] - p[0], p[i + 1] - p[i]);
    return s / 2;
}
  - **判断点是否在多边形内(不一定是凸多边形)**
      - **射线法(常用)**:从该点任意做一条和所有边都不平行的射线(随机一个角度,平行的概率几乎为$0$)。交点个数为偶数,则在多边形外,为奇数,则在多边形内。$O(n)$。
      - **转角法**:如果一个点在多边形内部,它可以与一个多边形边上的动点连一条边,这个动点运动一周后,这条连出来的边会转恰好一圈;如果在外部,那么这条连出来的边会转$0\degree$。$O(n)$。
  - **判断点是否在凸多边形内**
      - 首先判断该点是否在$\overrightarrow{P_1P_2}$和$\overrightarrow{P_{n-1}P_n}$的左边。然后在[2,n-1]里二分找到最大的i满足该点在$\overrightarrow{P_1P_i}$的左边。最后判断该点是否在$\overrightarrow{P_iP_{i+1}}$的左边。$O(\log n)$。
      - 判断该点是否在所有边的左边(逆时针存储多边形)。$O(n)$。
  • 皮克定理

    皮克定理是指一个计算点阵中顶点在格点上的多边形面积公式。该公式可以表示为:\(S=a+\dfrac{b}{2}-1\)。其中\(a\)表示多边形内部的点数,\(b\)表示多边形边界上的点数,\(S\)表示多边形的面积。

.1.7.圆

《思路很简单,你只需要让计算机学会联立解方程就可以了》

  • 圆的表示
    • 圆心+半径[常用]
typedef long double ld;
struct Circle
{
    PV2 centre;
    ld rad;
};
  • 不共线三点A、B、C

    求出AB、BC的中垂线u和v,求出直线u和v的交点w,则圆心是w,距离是|wa|。

Circle get_circle(Point a,Point b,Point c)
{
    Line u=get_mid(a,b),v=get_mid(a,c);
    Point w=get_line_intersection(u,v);
    return {w,get_dis(w,a)};
}
  • 点是否在圆上

    判断点a与该圆c的圆心c.o的距离和半径c.r的关系。dcmp(get_dis(c.o,a),c.r)

  • 圆与直线的关系并返回圆与直线的距离和交点

//返回圆与直线的距离和交点
double get_circle_line_intersection(Circle a,Line b,Point &ins1,Point &ins2)
{
    //求垂径
    Point e=get_line_intersection(b,Line{a.o,rotate(b.v,PI/2)});
    
    //求圆心到直线的距离
    double d=get_dis(a.o,e);
    
    if(dcmp(a.r,d)<0) return d;//直线与圆相离
    else if(dcmp(a.r,d)==0)//直线与圆相切
    {
        ins1=ins2=e;
        return a.r;
    }
    
    //由垂径定理求交点
    double len=sqrt(a.r*a.r-d*d);
    insa=e+unit(a-b)*len;
    insb=e+unit(b-a)*len;
    return d;
}

Point ins1,ins2;
double d=getget_circle_line_intersection(a,b,ins1,ins2);
if(dcmp(a.r,d)<0) //直线与圆相离
else if(dcmp(a.r,d)==0) //直线与圆相切
else //直线与圆相交

  • 圆与线段的关系并返回圆与线段的距离和交点
//返回圆与线段的距离和交点
double get_circle_line_intersection(Circle a,Line b,Point &ins1,Point &ins2)
{
    //求垂径
    Point e=get_line_intersection(b,Line{a.o,rotate(b.v,PI/2)});
    
    //求圆心到直线的距离
    double d=get_dis(a.o,e);
    if(!on_segment(e,b)) mi_dis=min(get_dis(o,b.x),get_dis(o,b.y));
    
    if(dcmp(a.r,d)<0) return d;//直线与圆相离
    else if(dcmp(a.r,d)==0)//直线与圆相切
    {
        ins1=ins2=e;
        return a.r;
    }
    
    //由垂径定理求交点
    double len=sqrt(a.r*a.r-d*d);
    insa=e+unit(a-b)*len;
    insb=e+unit(b-a)*len;
    return d;
}

double da=get_dis(a.o,b.x),db=get_dis(a.o,b.y);
if(dcmp(a.r,da)>=0 && dcmp(a.r,db)>=0)//线段在圆内
else
{
    Point ins1,ins2;
    double d=getget_circle_line_intersection(a,b,ins1,ins2);
    if(dcmp(a.r,d)<0) //线段与圆相离
    else if(dcmp(a.r,d)==0) //线段与圆相切
    else if(dcmp(a.r,b.x)>=0)//线段的x端在圆内,y端在圆外
    else if(dcmp(a.r,b.y)>=0)//线段的y端在圆内,x端在圆外
    else //线段与圆相交
}

  • 两圆交点
const PV2 inf = PV2{INF, INF};
int CP_circles(Circle a, Circle b, PV2 &insa = inf, PV2 &insb = inf)
// CP 就是 Common Point 公共点的意思
{
    ld d = vLength(b.centre - a.centre);
    if (sign(a.rad + b.rad - d) < 0) return 1; // 相离
    if (sign(fabs(a.rad - b.rad) - d) > 0) return 5; // 内含
    ld lambda = (a.rad * a.rad + d * d - b.rad * b.rad) / 2 / d;
    PV2 u = a.centre + (unit(b.centre - a.centre) * lambda);
    if (sign(a.rad + b.rad - d) == 0) {insa = insb = u; return 2;} // 外切
    if (sign(fabs(a.rad - b.rad) - d) == 0) {insa = insb = u; return 4;} // 内切
    ld mu = sqrt(a.rad * a.rad - lambda * lambda);
    PV2 v = rotate(unit(u), PI / 2);
    insa = u + (v * mu), insb = u - (v * mu);
    return 3; // 相交
}
  • 点到圆的切线
const Line infl = Line{inf, inf};
void TL_circle_point(Circle a, PV2 b, Line &tl1 = infl, Line &tl2 = infl)
// TL 就是 Tangent Lines 切线的意思
{
    ld d = vLength(b - a.centre);
    if (sign(a.rad - d) > 0) return; // 点在圆内
    PV2 u = a.centre + (unit(b - a.centre) * a.rad)
    tl1 = Line{b, rotate(rotate(u - a.centre, acos(a.rad / d)), PI / 2)};
    tl2 = Line{b, rotate(rotate(u - a.centre, -acos(a.rad / d)), -PI / 2)};
    return;
}
  • 两圆公切线

    注意,两圆有外公切线和内公切线之分,所以这里分开写两个函数。

void TL_circles_outer(Circle a, Circle b, Line &tl1 = infl, Line &tl2 = infl)
{
    ld d = vLength(b.centre - a.centre);
    if (sign(fabs(a.rad - b.rad) - d) > 0) return; // 内含无外公切线
    PV2 u;
    if (sign(a.rad - b.rad) > 0)
        u = a.centre + unit(b.centre - a.centre) * (d * a.rad / (a.rad - b.rad));
    else
        u = b.centre + unit(a.centre - b.centre) * (d * b.rad / (b.rad - a.rad));
    TL_circle_point(a, u, tl1, tl2);
    return;
}
void TL_circles_inner(Circle a, Circle b, Line &tl1 = infl, Line &tl2 = infl)
{
    ld d = vLength(a.centre - b.centre);
    if (sign(a.rad + b.rad - d) > 0) return; // 此时没有内公切线
    u = a.centre + (unit(b.centre - .centre) * (a.rad / (a.rad + b.rad)));
    TL_circle_point(a, u, tl1, tl2);
    return;
}
  • 两圆相交面积

    化为两个抛去圆心角三角形的扇形面积之和即可

ld swtArea(Circle a, ld angle)
// sectorWithoutTriangleArea
{
    ld sectorArea = a.rad * a.rad * angle / 2;
    ld triangleArea = a.rad * a.rad * sin(angle) / 2;
    if (sign(angle - PI) < 0) return sectorArea - triangleArea;
    else return sectorArea + triangleArea;
}
ld circlesArea(Circle a, Circle b)
{
    PV2 cp1, cp2;
    int t = CP_circles(a, b, cp1, cp2);
    if (t == 1 || t == 2) return 0;
    if (t == 4 || t == 5) return PI * min(a.rad, b.rad) * min(a.rad, b.rad);
    PV2 half = (cp1 + cp2) / 2;
    bool p = sign(vDot(a.centre - half, b.centre - half)) < 0;
    ld anglea = asin(vLength(half) / a.rad) * 2;
    ld angleb = asin(vLength(half) / b.rad) * 2; 
    if (!p) return swtArea(a, anglea) + swtArea(b, angleb);
    if (p)
    {
        if (sign(a.rad - b.rad) < 0)
            return swtArea(a, PI * 2 - anglea) + swtArea(b, angleb);
        else
            return swtArea(a, anglea) + swtArea(b, PI * 2 - angleb);
    }
}

  • 扇形的面积
double get_sector_area(Point o,Point a,Point b)
{
    Point ao=a-o,bo=b-o;
    double angle=acos((ao&bo)/ao.len()/bo.len());
    //if(sign(ao*bo)<0) angle=-angle;   //若求有向面积,因为acos返回一定是正值,所以注意根据题意,得到的角度是否取相反数
    return r*r*angle/2;
}

.1.8.三维计算几何

.1.8.1.三维向量运算

三维向量的加减、数乘运算、模长计算和二维计算方式相同。

  • 三维向量的点积

    \(\vec{a}\cdot\vec{b}=\left|\vec{a}\right|\left|\vec{b}\right|\cos\theta\)

    \(\vec{a}=\left(x_1,y_1,z_1\right),\vec{b}=\left(x_2,y_2,z_2\right)\),则\(\vec{a}\cdot\vec{b}=x_1x_2+y_1y_2+z_1z_2\)

  • 三维向量的叉积

    结果是一个向量。对于\(\vec{a}=\left(x_1,y_1,z_1\right)\)\(\vec{b}=\left(x_2,y_2,z_2\right)\)\(\vec{a}\times\vec{b}\)的几何代数意义为:

    • 模长\(\left|\vec{a}\right|\left|\vec{b}\right|\sin\theta\)
    • 方向:右手定则(右手的四指从\(\vec{a}\)的方向转向\(\vec{b}\)的方向,则大拇指(垂直于四指)指向的方向就是叉积结果向量的方向。

    \(\vec{a}\times\vec{b}=\det\left(\begin{bmatrix}\vec{i} & \vec{j} & \vec{k}\\x_1 & y_1 & z_1\\x_2 & y_2 & z_2\end{bmatrix}\right)=\left(y_1z_2-y_2z_1\right)\vec{i}+\left(z_1x_2-z_2x_1\right)\vec{j}+\left(x_1y_2-x_2y_1\right)\vec{k}=\left(y_1z_2-y_2z_1,z_1x_2-z_2x_1,x_1y_2-x_2y_1\right)\),其中\(\vec{i},\vec{j},\vec{k}\)分别为\(x,y,z\)轴方向的单位向量。

.1.8.2.多面体欧拉定理

顶点数-棱长数+表面数=2。一般用于复杂度分析。

.1.9.平面

平面一般都指三维空间下的有向平面。

  • 平面及其方向的表示

    三点确定一个平面。

    逆时针存储确定平面的三个点,这样一个平面的正反面就有不同的表示平面就有方向了

    平面问题一般需要恰好三点在一个平面上才好解决问题,因此需要微小扰动破坏多点在同一平面上,强制恰好三点在同一平面上

  • 求平面法向量

    任取两个平面上不共线的向量\(\vec{a},\vec{b}\),法向量为\(\vec{a}\times\vec{b}\)

    法向量方向:叉乘的方向。

  • 求确定平面的三点围成的面积

    叉乘的结果的模长除以2。

  • 点和平面的关系

    \(D\)在平面上\(\Leftrightarrow\)求出平面法向量\(\vec{c}=\vec{a}\times\vec{b}\),任取平面内一点\(E\),满足\(\overrightarrow{ED}\)\(\vec{c}\)上的投影为\(0\),即\(\overrightarrow{ED}\cdot\vec{c}=0\)

    \(D\)在该平面的法向量\(\vec{c}\)一侧\(\Leftrightarrow\)\(\overrightarrow{ED}\cdot\vec{c}>0\)。否则,在另一侧。

    n个平面把立体空间划分成若干个区域,判断一点在哪个区域:根据该点在这n个平面的哪一侧情况判断。

  • 求点到平面的距离

    即在上一个问题的基础上求出\(\overrightarrow{ED}\)\(\vec{c}\)上的投影长度,即\(\dfrac{\overrightarrow{ED}\cdot\vec{c}}{\left|\vec{c}\right|}\)

struct Plane
{
    int v[3];//存储确定该平面的三点的编号
    
    Point norm()//求平面法向量
    {
        return (poi[v[1]]-poi[v[0]])*(poi[v[2]]-poi[v[0]]);
    }
    
    double area()//求确定平面的三点围成的面积
    {
        return norm().len()/2;
    }
    
    bool above(Point a)//判断点是否在平面上或该有向平面的法向量一侧
    {
        return ((a-poi[v[0]])&norm())>=0;//&:点乘
    }
}

.1.10.球面几何

核心:球心角\(\theta\)

球面距离\(l=\theta r\)

.1.11.枚举与计数

  • 直线:枚举两个点。
  • 矩形:
    • 网格矩形(边平行于坐标轴):枚举右下单位矩形和左上单位矩形/枚举右下点和左上点,注意去掉两点共线的情况。

      在n*m矩形阵中的矩形的个数\(=\sum\limits_{i_1=1}^n\sum\limits_{j_1=1}^m1\sum\limits_{i_2=1}^{i_1}\sum\limits_{j_2=1}^{j_1}1=\frac{(1+n)n(1+m)*m}{4}\)

    • 正方形:枚举对角的两个点/枚举相邻的两个点和方向/枚举中心和一个点。

    • 矩形:枚举三个点/枚举中心和两个点。

      n个二维平面上的点最多只能组成\(O(n^{2.5})\)个矩形。

  • 圆:枚举圆心和圆上一点/枚举三个点/【已知半径】枚举两个点(垂径定理求圆心)。
  • 凸包内两条相交对角线:枚举四个点(四边形内两条对角线)。
  • 组合型图形:找到“组合点”(或者“组合线”)。枚举“单位图形”,得到所有的“组合点”并统计每个“组合点”对应的“单位图形”的个数。枚举“组合点”,通过其对应的“单位图形”的个数计算组合型图形的个数。

.1.12.技巧

  1. 极限有限化。

    某些几何问题的解有无穷多个,可以通过取该解的极限情况(“图形卡到边界”),使得枚举的情况有限化。

    枚举可以参考《数学.1.9.枚举与计数》

    1. 通过平移、旋转合法的直线,使直线处于合法的极限情况(“卡到”两个“极点”),进而使得枚举的情况(枚举两个“极点”确定一条直线)有限化。
  2. 选择特殊点(或者特殊线)。

    当需要找到一个满足要求的点时,可以选择图形的特殊点。

    1. 找到一个在凸包内部(不含边界)的点
      • 凸包的三个顶点构成的三角形的重心\((\frac{x_1+x_2+x_3}{3},\frac{y_1+y_2+y_3}{3})\)
      • 凸包的三个顶点构成的三角形的中位线的中点\((\frac{2x_1+x_2+x_3}{4},\frac{2y_1+y_2+y_3}{4})\)
  3. 整数计算化。

    1. 整点多边形的面积的两倍一定是整数:利用叉积计算时不除以2,最后再把结果除以2。
  4. 旋转坐标系。

    适用条件:题目中的几何图形是倾斜的,或者旋转后题目数据有一些性质可以操过去

    本质上是把题目给定的点旋转。

    \(e.g.\)顺时针旋转坐标系\(\alpha ^\circ\)for(int i=1;i<=n;i++) poi[i]=rotate(poi[i],a);

  5. 仿射。

    适用条件:化椭为圆。

    对平面上所有点做一个变换:\(x'=x/a,y'=y/b\),则椭圆\(\frac{x^2}{a^2}+\frac{y^2}{b^2}=1\)将变成正圆\({x'}^2+{y'}^2=1\)。椭圆的题目就可以用圆的算法来解决了。for(int i=1;i<=n;i++) poi[i].x/=a,poi[i].y/=b;

  6. 微小扰动。

    为避免某些不常规情况,给每个点的坐标加上一个微小扰动来破坏这种不常规情况,并且又不会影响答案正确性。但是注意,这样扰动之后所有的有关精度误差的操作(如sign 函数)都不能再做了,一旦用了这个函数,就抵消微小扰动了。微小扰动的EPS设为min(1e-8,题目要求精度的\(10^{-4}\))

    \(e.g.\)平面问题一般需要恰好三点在一个平面上才好解决问题,因此需要微小扰动破坏多点在同一平面上,强制恰好三点在同一平面上

double drand()
{
    return ((double)rand()/RAND_MAX-0.5)*EPS;
}

struct Point
{
    void shake()
    {
        x+=drand(),y+=drand(),z+=drand();
    }
};

for(int i=1;i<=n;i++) poi[i].shake();

.2.凸包

.2.1. 二维凸包

.2.1.1.静态求凸包、动态维护凸壳——xy坐标排序(\(Andrew\)算法)\(O(N\log N)\)

给定平面上的点,求凸包(将凸包上的点按逆时针存入栈中)。

  1. 将点排序:x为第一关键字(从小到大),y为第二关键字(从小到大)。

  2. 从前往后扫描点,维护下凸包,并标记下凸包的点防止上凸包扫描到这些点;取消标记第一个点,以便最终下凸包与上凸包在第一个点形成闭合;再从后往前扫描点,维护上凸包。

    无论上/下凸包,均使用一个栈维护,且当\(\overrightarrow{st[top-1]\ st[top]}\times \overrightarrow{st[top-1]\ i} ≤0\)时,弹出不合法栈顶。

    注意:

    1. 当多点在同一条直线上时,根据题目决定是否保留中间点,若保留,把上式的≤改成<,且要对点去重

      若计算周长时所有的点全部处于一条直线上,则必须保留中间点,否则最终栈只会剩下两个起点,周长误为0。
      或者不用改代码,加一行特判:if(top≤2) ans=get_dis(a[1],a[n])*2;//因为点已经排好序了

    2. 若先维护上凸包再维护下凸包,把上式的≤改成≥。

      一般先维护下凸包,因为这样凸包上的点就是按逆时针存储了,方便后面例如旋转卡壳的操作。

    3. 若下面的代码不使用used数组,且上式为≤,则刚开始维护上凸包时可能会出错。

int n;
double ans;
PDD a[N];

int st[N],top;  //栈维护凸包
bool used[N];   //used[i]:第i个点是否被使用

void andrew()
{
    //初始化
    top=0;
    memset(used,false,sizeof used);
    
    //将点排序
    sort(a+1,a+n+1);
    
    //先从前往后扫描点,维护下凸包
    for(int i=1;i<=n;i++)
    {
        while(top>=2 && sign(area(a[st[top-1]],a[st[top]],a[i]))<=0)    //弹出不合法栈顶。若先维护上凸包再维护下凸包,把andrew()的不等号全部反向。
        {
            if(sign(area(a[st[top-1]],a[st[top]],a[i]))<0) used[st[top]]=false; //当多点在同一条直线(叉乘等于0)时,弹出栈顶不要取消标记,因为这些点一定不可能成为下凸包
            top--;
        }
        st[++top]=i;
        used[i]=true;   //标记上凸包的点防止下凸包扫描到这些点
    }
    
    //取消标记第一个点,以便最终上凸包与下凸包在第一个点形成闭合
    used[1]=false;
    
    //再从后往前扫描点,维护上凸包
    for(int i=n;i>=1;i--)
    {
        if(used[i]) continue;
        while(top>=2 && sign(area(a[st[top-1]],a[st[top]],a[i]))<=0) top--;
        st[++top]=i;
    }
    
    return ;
}

scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lf%lf",&a[i].first,&a[i].second);
andrew();   //凸包存储在栈st[]中

//求凸包周长
if(top<=2) ans+=get_dis(poi[1],poi[n])*2;//注意不是get_dis(poi[st[1]],poi[st[2]]),因为当所有的点全部处于一条直线上时,最终栈只会剩下两个起点
else for(int i=1;i<top;i++) ans+=get_dis(poi[st[i]],poi[st[i+1]]);
printf("%.2lf\n",ans);

.2.1.2.动态维护凸包——极角排序\(O(N\log N)\)

尽量转化问题去维护凸壳。

在线任意删点较难维护,可以离线线段树分治维护撤销。

  • 所有点共线

  • 存在三点不共线

    令极点为这三点构成的三角形的重心。它一定在凸包内部(不含边界)。将所有坐标乘以3可以实现整数计算化。(或者该三角形的中位线的中点,将所有坐标乘以4)

    点的排序依据是极角序(若极角相等则比较模长,即该点到极点的距离)。可以用set维护凸包上的点(环形)。

    凸包上的点的充要条件是任意极角序相邻的三点构成的有向三角形面积非负。

    当插入一个点时,
    1. 判断它是否为极点。若是,则返回。
    2. 否则,判断如果它在凸包上是否将合法:它在set环形的前驱、它自己、后继构成的有向三角形面积是否为正(若允许多点共线则非负)。若非正,则返回。
    3. 否则,插入该点,然后删除不合法前驱,删除不合法后继。

.2.1.3.dp\(O(N^3)\)

适用条件:有约束条件。

枚举起点终点创建有向边,将所有的有向边按极角排序。\(f_i\):当前构建凸包的最后一个点是点i的……。枚举凸包的起点\(O(N)\),以排序后的有向边为阶段进行状态转移\(O(N^2)\)

pii p[N];   //点(x,y)
int eidx;
pii e[N*N]; //有向边(u,v):以点u为起点,点v为终点的有向边
int f[N];   //当前构建凸包的最后一个点是点i的……
int ans;

bool cmp(pii e1,pii e2)
{
    return atan2(p[e1.y].y-p[e1.x].y,p[e1.y].x-p[e1.x].x)<atan2(p[e2.y].y-p[e2.x].y,p[e2.y].x-p[e2.x].x);
}

for(int i=1;i+1<=n;i++)
    for(int j=i+1;j<=n;j++)
    {
        e[++eidx]={i,j};
        e[++eidx]={j,i};
    }
sort(e+1,e+eidx+1,cmp);
for(int i=1;i<=n;i++)   //需要枚举起点,才能保证构建的凸包是闭合的
{
    //保证凸包以点i为起点
    memset(f,-0x3f,sizeof f);
    f[i]=0;
    
    for(int j=1;j<=eidx;j++) merge(f[e[j].y],f[e[j].x]);    //以排序后的有向边为阶段进行状态转移
    calc(ans,f[i]);//统计答案
}

.2.2.三维凸包:增量法\(O(N^2)\)

这里的平面都是有向平面。

凸包储存在平面数组plane[N]中。

  1. 处理多点共面:微小扰动破坏多点在同一平面上,强制恰好三点在同一平面。
  2. 初始凸包先加入前三个点的正反面两个方向的平面。
  3. 4~n枚举点i。若该点在凸包内,跳过;否则,点i向当前的凸包做一个投影,如果一个面能被照到,则删去。然后,若一条线的两侧分别是能被照到的平面和不能照到的平面,则称这条线为分界线。将点i与分界线组成的平面加入凸包
    • 判断一个面能否被照到:即判断该点位于这个平面的上方还是下方,即在平面内任取一个点和该点的向量在法向量上的投影是正是负。
    • 确定分界线:每一面的点都是逆时针存下来的,对于点A,B构成的边AB,如果\(\overrightarrow{AB}\)所在的边能被照到而\(\overrightarrow{BA}\)不能被照到,那AB就是一条分界线。
int n,m;
double ans;
bool g[N][N];   //g[a][b]:向量ab所在的边代表的面能否被照到

double drand()
{
    return ((double)rand()/RAND_MAX-0.5)*EPS;
}

struct Point
{
    double x,y,z;
    
    double operator & (Point qw)    //点乘
    {
        return x*qw.x+y*qw.y+z*qw.z;
    }
    Point operator * (Point qw) //叉乘
    {
        return {y*qw.z-z*qw.y,z*qw.x-x*qw.z,x*qw.y-y*qw.x};
    }
    
    double len()    //模长
    {
        return sqrt(x*x+y*y+z*z);
    }
    
    void shake()    //微小扰动
    {
        x+=drand(),y+=drand(),z+=drand();
    }
}poi[N];

struct Plane
{
    int v[3];
    
    Point norm()//法向量
    {
        return (poi[v[1]]-poi[v[0]])*(poi[v[2]]-poi[v[0]]);
    }
    
    double area()//面积
    {
        return norm().len()/2;
    }
    
    bool above(Point a)//点a是否在平面的上方
    {
        return ((a-poi[v[0]])&norm())>=0;
    }
}plane[N],np[N];

void get_convex_3d()
{
    //初始凸包先加入前三个点的正反面两个方向的平面
    plane[++m]={1,2,3};
    plane[++m]={3,2,1};
    
    for(int i=4;i<=n;i++)//增量法。4~n枚举点i
    {
        int nidx=0;
        for(int j=1;j<=m;j++)
        {
            bool flag=plane[j].above(poi[i]);   //判断该点位于这个平面的上方还是下方
            if(!flag) np[++nidx]=plane[j];  //保留不能被照到的面
            for(int k=0;k<3;k++) g[plane[j].v[k]][plane[j].v[(k+1)%3]]=flag;    //标记该面
        }
        for(int j=1;j<=m;j++)
            for(int k=0;k<3;k++)
            {
                int a=plane[j].v[k],b=plane[j].v[(k+1)%3];
                if(g[a][b] && !g[b][a]/*分界线*/) np[++nidx]={a,b,i};   //将点i与分界线组成的平面加入凸包(注意逆时针顺序)
            }
        m=nidx;
        for(int j=1;j<=m;j++) plane[j]=np[j];
    }
    return ;
}

scanf("%d",&n);
for(int i=1;i<=n;i++)
{
    scanf("%lf%lf%lf",&poi[i].x,&poi[i].y,&poi[i].z);
    poi[i].shake(); //微小扰动,处理多点共面
}
get_convex_3d();

//求凸包面积
for(int i=1;i<=m;i++) ans+=plane[i].area();
printf("%.6lf\n",ans);

.2.3.凸单调优化dp

《动态规划8.5.凸单调性优化dp》

.3.半平面交

定义:平面上有多条有向直线,每个有向直线将平面划分成2个集合:有向直线左边的平面和右边的平面。求所有有向直线的左边的平面的集合的交。从这些直线中求得一个直线集合,表示交的边缘。

交可以不是一个闭合图形。交一定是凸的。

凸包的面积\(\Leftrightarrow\)多条有向直线的半平面交。

  1. 将向量按与水平线所成的角度排序。

    atan2(y,x):广义\(\arctan(\frac{y}{x})\):从(0,0)指向(1,0)的向量与从(0,0)指向(x,y)的向量的有向夹角。

double get_angle(Line x)//求向量与水平线所成的角度
{
    return atan2(x.ed.second-x.st.second,x.ed.first-x.st.first);
}

bool cmp1(Line x,Line y)
{
    double xa=get_angle(x),ya=get_angle(y);
    if(cmp(xa,ya)==0) return area(x.st,x.ed,y.ed)<0;//若角度相同,则按在其他有向直线的左侧排序
    return xa<ya;
}

  1. 从前往后扫描所有向量,用双端队列维护半平面交。
    1. 若当前的直线的角度与上一条直线相同,由于前面的排序,上一条直线一定在该直线的左侧,该直线的左边的平面的集合是上一条直线的子集,不考虑该直线。

    2. 检查队头和队尾。若队头(尾)与队头(尾)后(前)面的直线的交点在当前直线的右侧(用叉积判断),弹出不合法队头(尾)。必须先删队尾。

      对于多线交于一点的情况:若只保留一条最关键的直线,则on_right 的判定条件是叉积≤0;若保留每一条直线,则on_right 的判定条件是叉积<0。

    3. 插入当前的直线。

  2. 检查队头和队尾。
  3. 判断情况
    • 空与非空
      1. 若半平面交为空,则最后队列会剩2条直线。
      2. 区分空半平面交和2条直线形成的非空半平面交:选取一个点(比如以2条直线的交点为起点,方向分别平行于2条直线的2个单位向量相加,结果指向的点)满足在最后队列里的2条直线的左侧,O(N)判断该点是否在所有直线的左侧。若该点在所有直线的左侧,则非空;否则,为空。
    • 非空:闭合与开放
      • 最后队列剩2条直线:开放。

      • 最后队列剩3及以上条直线:

        求出队列里第一条直线和最后一条直线的交点,O(N)判断该点是否在队列其他所有直线的左侧(on_right 的判定条件是叉积≤0。)。若该点在队列其他所有直线的左侧,则闭合;否则,开放。

  4. 若要求半平面交形成的凸包,则依次求队列中相邻两条直线的交点(注意队头和队尾也算相邻),得到代表半平面交的凸包上的点。

注意:

  • 设置较高精度(1e-8及更高)。
  • 直线如向量有方向
int n;
PDD poi[N];

int lidx;
struct Line //两点式
{
    PDD st,ed;
}line[N];

int q[N];   //双端队列维护半平面交

int aidx;
PDD ans[N];  //半平面交以凸包上的点的形式存储在ans[]中

double get_angle(Line x)//求向量与水平线所成的角度
{
    return atan2(x.ed.second-x.st.second,x.ed.first-x.st.first);
}

bool cmp1(Line x,Line y)
{
    double xa=get_angle(x),ya=get_angle(y);
    if(cmp(xa,ya)==0) return sign(area(x.st,x.ed,y.ed))<0;//若角度相同,则按在其他有向直线的左侧排序
    return xa<ya;
}

PDD get_line_intersection(PDD p,PDD v,PDD q,PDD w)
{
    PDD u=p-q;
    double t=cross(w,u)/cross(v,w);
    return {p.first+t*v.first,p.second+t*v.second};
}

PDD get_line_intersection(Line x,Line y)
{
    return get_line_intersection(x.st,x.ed-x.st,y.st,y.ed-y.st);//两点式转点向式,再求交点
}

//判断y和z的交点是否在x的右侧
bool on_right(Line x,Line y,Line z)
{
    PDD o=get_line_intersection(y,z);
    return sign(area(x.st,x.ed,o))<=0;//对于多线交于一点的情况:若只保留一条最关键的直线,则判定条件为≤0;若保留每一条直线,则判定条件为<0
}

double half_plane_intersection()
{
    //将向量按与水平线所成的角度排序
    sort(line+1,line+lidx+1,cmp1);
    
    //从前往后扫描所有向量
    int hh=1,tt=0;
    for(int i=1;i<=lidx;i++)
    {
        if(i>1 && cmp(get_angle(line[i]),get_angle(line[i-1]))==0) continue;    //与上一条直线角度相同,由于排序,上一条直线一定在该直线的左侧,该直线的左边的平面的集合是上一条直线的子集,不考虑该直线
        
        //检查队头和队尾
        //必须先删队尾
        while(hh+1<=tt && on_right(line[i],line[q[tt-1]],line[q[tt]])) tt--;
        while(hh+1<=tt && on_right(line[i],line[q[hh+1]],line[q[hh]])) hh++;
        
        q[++tt]=i;
    }
    
    //检查队头和队尾
    //必须先删队尾
    while(hh+2<=tt && on_right(line[q[hh]],line[q[tt-1]],line[q[tt]])) tt--;
    while(hh+2<=tt && on_right(line[q[tt]],line[q[hh+1]],line[q[hh]])) hh++;
    
    //依次求队列中相邻两条直线的交点,得到代表半平面交的凸包上的点
    q[++tt]=q[hh];//使队头和队尾相邻
    for(int i=hh;i<tt;i++) ans[++aidx]=get_line_intersection(line[q[i]],line[q[i+1]]);
    
    //求半平面交(一个凸包)的面积
    return polygon_area(ans,aidx);
}

scanf("%d",&n);
while(n--)
{
    int m;
    scanf("%d",&m);
    for(int i=1;i<=m;i++) scanf("%lf%lf",&poi[i].first,&poi[i].second);
    for(int i=1;i<=m;i++) line[++lidx]={poi[i],poi[i%m+1]}; //将给定的凸包的点转化多个有向直线
}

printf("%.3lf\n",half_plane_intersection());

.3.1.应用

  1. 凸包的面积\(\Leftrightarrow\)多个有向直线的半平面交。
  2. 多个凸包的面积交\(\Leftrightarrow\)所有形成这些凸包的有向直线的半平面交。
  3. 多个凸包的面积并\(\Leftrightarrow\)容斥原理(面积和-面积交)。
  4. 在平面直角坐标系内,有多个一次函数\(f_i\)。设函数\(g(x)=\max\limits_{i=1} f_i(x)\),则函数g可用这些一次函数的有向直线的半平面交求解。
    • 若有多条相同的直线,每一条不同的直线开一个单独的vector ,存储有哪些编号是这条直线。
    • 只考虑第一象限:把x轴和y轴的有向直线也加入半平面交作为限制条件。
    • 对于多线交于一点的情况,若每一条直线都要保留,则on_right 的判定条件是叉积<0。

.4.最小圆覆盖

给定二维平面上的一些点,求一个最小的能够覆盖所有点的圆。圆的边上的点也视作在圆内被覆盖。

性质

  1. 最小覆盖圆是唯一的。

    只需求出最小覆盖圆边上的三个点即可。

  2. 若点p不在点集S的最小覆盖圆内部,那么点p一定在点p和点集S的最小覆盖圆的边上。

    求最小覆盖圆可使用增量法。

增量法\(O(N)\)

  1. 将点随机化(保证复杂度)。

  2. 初始的最小覆盖圆的圆心是第一个点,半径是0。

  3. 从2~n枚举点i以枚举“1i的最小覆盖圆”。若点i在当前的最小覆盖圆内,跳过;**否则**由性质2:点i必在1i的最小覆盖圆的边上,确定圆边上的第一个点i:令当前的最小覆盖圆的圆心是点i半径是0

    在上一轮循环的基础上,再从1i-1枚举点j**以枚举“满足点i在圆边上的1i的最小覆盖圆”。若点j在当前的最小覆盖圆内,跳过;否则由性质2:点j必在“满足点i在圆边上的1~i的最小覆盖圆”的边上,确定圆边上的第二个点j:令当前最小覆盖圆的圆心是点i和j的中点半径是****\(\frac{|ij|}{2}\)**(也就是ij是该圆的直径)。

    在上一轮循环的基础上,再从1j-1枚举点k**以枚举“满足点i和j在圆边上的1i的最小覆盖圆”。若点k在当前的最小覆盖圆内,跳过;否则由性质2:点k必在“满足点i和j在圆边上的1~i的最小覆盖圆”的边上,确定圆边上的第三个点k:令当前的最小覆盖圆是三点i、j、k确定的圆**。三点确定一圆,不必再开一层循环,继续当前的循环即可。

第三层循环的复杂度是O(N)的。第二层循环有\(\frac{3}{N}\)的概率(一般枚举到最终最小覆盖圆的边上的三个点之一才会进入下一循环,因此需要一开始的随机化点保证复杂度)进入第三层循环,因此后两层循环的复杂度是\(O(N+\frac{3}{N} * O(N))=O(N+3)=O(N)\)。同理,总的复杂度是\(O(N)\)

scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lf%lf",&poi[i].x,&poi[i].y);
random_shuffle(poi+1,poi+n+1);//将点随机化
Circle c={poi[1],0};//初始的最小覆盖圆的圆心是第一个点,半径是0
for(int i=2;i<=n;i++)//从2~n枚举点i以枚举“1~i的最小覆盖圆”
{
    if(dcmp(get_dis(c.o,poi[i]),c.r)>0)//若点i不在当前的最小覆盖圆内,则点i必在1~i的最小覆盖圆的边上
    {
        c={poi[i],0};//确定圆边上的第一个点i
        for(int j=1;j<i;j++)//再从1~i-1枚举点j以枚举“满足点i在圆边上的1~i的最小覆盖圆”
        {
            if(dcmp(get_dis(c.o,poi[j]),c.r)>0)//若点j不在当前的最小覆盖圆内,点j必在“满足点i在圆边上的1~i的最小覆盖圆”的边上
            {
                c={(poi[i]+poi[j])/2,get_dis(poi[i],poi[j])/2};//确定圆边上的第二个点j
                for(int k=1;k<j;k++)//再从1~j-1枚举点k以枚举“满足点i和j在圆边上的1~i的最小覆盖圆”
                {
                    if(dcmp(get_dis(c.o,poi[k]),c.r)>0)//若点k不在当前的最小覆盖圆内,则点k必在“满足点i和j在圆边上的1~i的最小覆盖圆”的边上
                    {
                        c=get_circle(poi[i],poi[j],poi[k]);//确定圆边上的第三个点k
                        //三点确定一圆,不必再开一层循环,继续当前的循环即可
                    }
                }
            }
        }
    }
}
//得到最小覆盖圆c

.5.旋转卡壳思想

具体思想。

  1. 求凸包(逆时针存储)。

    注意这里求凸包最后栈要删掉用于闭合的两个相同的点,否则下面双指针j在凸包上走一圈后会走2个相邻且相同的点,导致前后两个叉乘相同而j卡在那里。

  2. 将情况利用卡壳离散化。

    \(e.g.\)证明两点:1.每条答案的边必经过凸包上的点;2.最优解一定在旋转的平行线与凸包的一条边重合时的情况中。

  3. 对于每一条与凸包的一条边重合的平行线,旋转另一条平行线利用卡壳找到最值。

  4. 在旋转过程中,答案随着编号有单调性,用双指针解决。

.5.1.旋转卡壳求凸包直径——平面最远点对

  1. 求凸包(逆时针存储)。

    注意这里求凸包最后栈要删掉用于闭合的两个相同的点,否则下面双指针j在凸包上走一圈后会走2个相邻且相同的点,导致前后两个叉乘相同而j卡在那里。

  2. 特判所有的点共线的情况。

  3. 逆时针枚举凸包上的边,对于每条边双指针找出离该边最远的点,并将该点与边的两端点的两个距离都加入候选答案。

    求离某条边最远的点:面积法。多个点与这条边形成多个同底的三角形。面积最大的三角形所代表的点离这条边最远。

    发现离某条边最远的点(答案)随着逆时针枚举边(编号)有单调性,用双指针优化成\(O(N)\)

void andrew()
{
    top--;  //注意这里求凸包最后栈要删掉用于闭合的两个相同的点,否则下面双指针j在凸包上走一圈后会走2个相邻且相同的点,导致前后两个叉乘相同而j卡在那里
}

int rotating_calipers()
{
    if(top<=2) return get_dis(poi[1],poi[n]);//特判所有的点共线的情况
    int res=0;
    for(int i=1,j=3;i<=top;i++)//逆时针枚举凸包上的边
    {
        Point a=poi[st[i]],b=poi[st[i%top+1]];  //不要忘记i要在凸包上走一圈了
        while(area(a,b,poi[st[j]])<area(a,b,poi[st[j%top+1]])) j=j%top+1; //对于每条边双指针找出离该边最远的点,不要忘记j要在凸包上走过一圈了!!!
        res=max(res,max(get_dis(a,poi[st[j]]),get_dis(b,poi[st[j]])));//将该点与边的两端点的两个距离都加入候选答案
    }
    return res;
}

andrew();//求凸包(逆时针存储)
printf("%d\n",rotating_calipers());

《2.5.1.分治求解平面最近点对\(O(N \log N)\)

.5.2.旋转卡壳求最小矩形覆盖

性质

平面上的点形成凸包。

  • 四条边必经过凸包上的点。
  • 矩形上的一条边与凸包上的某条边重合。

故用旋转卡壳求解。逆时针枚举凸包上的边DE,对于每条边双指针找出离该边最远的点A(面积最大值)、离该边最右边的点B(\(\overrightarrow{AD}\)\(\overrightarrow{ED}\)方向上的投影数量最大值)、离该边最左边的点C(\(\overrightarrow{AD}\)\(\overrightarrow{ED}\)方向上的投影数量最小值)。

double min_area=INF;    //最小覆盖矩形的面积
Point ans[5];   //最小覆盖矩形的四个点

double project(Point a,Point b,Point c) //投影数量
{
    return ((b-a)&(c-a))/(b-a).len();
}

Point unit(Point a) //单位向量
{
    return a/a.len();
}

void rotating_calipers()
{
    for(int i=1,a=3/*a求的是面积最大值,所以从3开始*/,b=2/*b求的是投影最大值,所以从2开始*/,c;i<=top;i++)
    {
        Point d=poi[st[i]],e=poi[st[i%top+1]];
        while(dcmp(area(d,e,poi[st[a]]),area(d,e,poi[st[a%top+1]]))<0) a=a%top+1;
        while(dcmp(project(d,e,poi[st[b]]),project(d,e,poi[st[b%top+1]]))<0) b=b%top+1;
        if(i==1) c=a;//c求的是投影最小值,为保证单调性,c从a开始
        while(dcmp(project(d,e,poi[st[c]]),project(d,e,poi[st[c%top+1]]))>0) c=c%top+1;
        Point x=poi[st[a]],y=poi[st[b]],z=poi[st[c]];
        double h=area(d,e,x)/(e-d).len(),w=((y-z)&(e-d))/(e-d).len();   //求高和宽
        if(h*w<min_area)
        {
            min_area=h*w;
            
            //求四点坐标
            Point u=unit(rotate(e-d,-PI/2));
            ans[1]=d+unit(e-d)*project(d,e,y);
            ans[4]=d+unit(e-d)*project(d,e,z);
            ans[2]=ans[1]+u*h;
            ans[3]=ans[4]+u*h;
        }
    }
    return ;
}

andrew();
rotating_calipers();

.6.三角剖分思想

作用:将复杂的多边形简单化。

  1. 将多边形每条边按逆时针方向定序,转化为向量。
  2. 原点O向向量的2个端点连边,此时多边形被剖分成多个三角形。
  3. 三角形面积是有向面积,用叉乘求,三角形的面积总和即为原多边形面积。
  4. 把多边形问题转化为三角形面积。

.6.1.三角剖分求多边形与圆的面积交

将多边形三角剖分后,问题转化为求三角形与圆的面积交。

分类讨论。

.7.扫描线\(O(N^2 \log N)\)

适用条件:求不规则图形的周长或面积。

  1. 以点(顶点和交点)为依据增加扫描线。然后将将扫描线排序。

  2. 单独分析2个相邻的扫描线间会有什么情况?怎么求这些情况?

  3. 依次求出2个相邻的扫描线间的贡献:分别求出几何图形对前一个扫描线的右侧和后一个扫描线的左侧覆盖长度。然后结合步骤2进行计算。

    求出几何图形对当前的1个扫描线的side侧的覆盖长度:

    1. 如果不规则图形是由多个类型相同的几何图形组成的,则枚举几何图形1~n,将每个几何图形对当前扫描线side侧的覆盖长度以区间的形式存入q[i]。
    2. 区间合并求出所有几何图形对当前的扫描线的覆盖长度。
int n;
struct Graph{}g[N];
vector<double> xs; //扫描线
int qidx;
PDD q[N];   //每个几何图形对当前扫描线的覆盖长度的区间的形式

int line_length(double a,int side)  //求出几何图形对当前的1个扫描线x=a的side侧的覆盖长度
{
    qidx=0;
    
    //枚举几何图形1~n,将每个几何图形对当前扫描线的覆盖长度以区间的形式存入q[i]
    for(int i=1;i<=n;i++) if(/*若几何图形与扫描线相交*/) q[++qidx]=get_intersection_length(a,side,g[i]);
    
    if(qidx==0) return 0;   //没有覆盖长度
    
    //区间合并
    sort(q+1,q+qidx+1);
    int res=0;
    int st=q[1].first,ed=q[1].second;
    for(int i=2;i<=qidx;i++)
        if(q[i].first<=ed) ed=max(ed,q[i].second);
        else
        {
            res+=ed-st;
            st=q[i].first,ed=q[i].second;
        }
    res+=ed-st; //别忘了最后还要加上当前的ed-st
    
    return res;
}

int range_area(double l,double r)//求出2个相邻的扫描线间的贡献
{
    return calc(line_length(l),line_length(r)); //求出几何图形对前一个扫描线的右侧和后一个扫描线的左侧覆盖长度。然后分析计算
}

double ans=0;
for(int i=1;i<=n;i++)
{
    cin>>g[i];
    xs.push_back(abaabaaba);    //以顶点为依据增加扫描线
}
for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++)
        xs.push_back(get_graph_intersection(g[i],g[j]));    //以点交点为依据增加扫描线
sort(xs.begin(),xs.end());  //将扫描线排序
for(int i=0;i+1<xs.size();i++)  if(dcmp(xs[i],xs[i+1])!=0/*排除相同的扫描线*/) ans+=range_area(xs[i],xs[i+1]);  //依次求出2个相邻的扫描线间的贡献
printf("%lf\n",ans);

.7.1.扫描线求三角形面积并\(O(N^3 \log N)\)

链接。

.7.2.线段树优化扫描线\(O(N \log N)\)

《数据结构8.4.扫描线》

数据结构·序列

.7.3.圆的扫描线

将圆分成两部分:上半圆和下半圆。列出对应的函数解析式。

.8.自适应辛普森积分

适用条件:求复杂图形面积。

自适应辛普森积分是求解形如\(\int_{l}^{r}f(x)\,dx\)的积分,几何意义是\(x\in[l,r]\)\(f(x)\)\(x\)轴形成的有向面积(\(y\)轴正方向为正)。注意使用自适应辛普森积分时需要特判在当前区间的情况下积分能否收敛(趋近于某个值),对于发散(趋近于\(+\infty\))的积分输出INF

  1. 辛普森积分:把当前的l、r、mid=(l+r)/2三点看作在一个抛物线上,结果是这个抛物线的面积\(S=\frac{f(l)+4*f(mid)+f(r)}{6}*(r-l)\)
  2. 自适应:根据精度自适应积分区间。递归求解,若左半区间的辛普森积分+右半区间的辛普森积分与当前区间的辛普森积分的误差小于EPS,则返回左半区间的辛普森积分+右半区间的辛普森积分;否则继续递归求解。
double f(double x)//原函数的定义
{
    return abaabaaba;
}

double simpson(double l,double r)//辛普森积分
{
    double mid=(l+r)/2;
    return (r-l)*(f(l)+4*f(mid)+f(r))/6;
}

double asr(double l,double r,double s)//自适应
{
    double mid=(l+r)/2;
    double left=simpson(l,mid),right=simpson(mid,r);
    if(fabs(left+right-s)<EPS) return left+right;   //相比于返回上一轮的s,返回这一轮精度更高的left+right肯定更好
    return asr(l,mid,left)+asr(mid,r,right);
}

double l,r;
scanf("%lf%lf",&l,&r);
if(check(l,r)) printf("%.6lf\n",asr(l,r,simpson(l,r)));//特判在当前区间的情况下积分能否收敛
else puts("INF");//对于发散的积分输出INF

.8.1.自适应辛普森积分求复杂图形的面积

  1. \(\int_{l}^{r}f(x)\,dx\)中的区间[l,r]定义为[几何图形上的点的坐标在x轴的最小值-100,几何图形上的点的坐标在x轴的最大值+100]。对于几何图形是一个点的情况直接跳过,减小误差。
  2. \(\int_{l}^{r}f(x)\,dx\)中的函数\(f(x_0)\)定义为几何图形对扫描线\(x=x_0\)的覆盖长度。

例题:圆的面积并。

.群论

适用条件:置换(置换只考虑有等价类的置换,否则。\(e.g.\)“若两种方案经旋转后能完全重合,则视为同一种方案”,则考虑旋转这一类置换。)的计数问题。

.1.群

是一种代数系统,包含一个集合G和一种二元运算\(\cdot\),且满足以下四条性质:

  1. 封闭性:\(\forall x, y\in G,\text{Have}\, x\cdot y\in G\)
  2. 结合律:\(\forall x, y, z\in G, \text{Have}\,x\cdot(y\cdot z)=(x\cdot y)\cdot z\)
  3. 存在单位元(也称幺元):\(\exist e\in G, \forall x \in G, \text{Have}, x\cdot e = e\cdot x = x\)
  4. 都存在逆元(简称为逆):\(\forall x\in G, \text{Have}, x^{-1},\text{s.t.},x\cdot x^{-1}=e\)\(\text{s.t.}\)的意思是使……满足/成立)

这个群习惯性记作群\((G,\cdot)\)

.2.置换群

技巧

  1. \(\sum\limits_{i=1}^n calc(\gcd(i,n))\):如果n是1e9级别的,可以枚举n的约数j作为\(gcd_j\),i=1~n中满足\(\gcd(i,n)=gcd_j\)的i一共有\(\varphi (\frac{n}{gcd_j})\)个(证明:\(\gcd(i,n)=gcd_j\Leftrightarrow \gcd(\frac{i}{gcd_j},\frac{n}{gcd_j}=1)\),也就是互质)。\(O(\sqrt N * \log N)\)
for(int i=1;1ll*i*i<=n;i++)
    if(n%i==0)
    {
        sum+=euler(n/i)*calc(i);
        if(i*i!=n) sum+=euler(i)*calc(n/i);
    }

.2.1.置换

注意区分“一类”置换和“一个”置换。

置换:映射到一种排列。一个含有n个元素的集合A的置换称为一个n次置换。\(e.g.\)一个4次置换f:1,2,3,4→3,4,2,1,则f(1)=3,f(2)=4,f(3)=2,f(4)=1。

循环置换:1,2,3,...,n→2,3,...,n,1。

置换的乘积(从左往右乘)f(x)*g(x)=g(f(x))。

  • 任何一个置换均可拆成若干个循环置换的乘积。
  • 两个n次置换的乘积还是一个n次置换
  • n次置换的乘法满足结合律
  • 存在“什么都不变”的n次置换:$\begin{bmatrix}1&2&\cdots&n\1&2&\cdots&n\end{bmatrix} $
  • 对于一个n次置换,总存在一个逆置换:\(\begin{bmatrix}a_1&a_2&\cdots&a_n\\b_1&b_2&\cdots&b_n\end{bmatrix}\) 的逆置换是$\begin{bmatrix}b_1&b_2&\cdots&b_n\a_1&a_2&\cdots&a_n\end{bmatrix} $

.2.2.置换群

定义:置换集合\(S_n\)和置换的乘法\(\cdot\)构成的一个群\((S_n,\cdot)\)

一个置换群的子群仍是一个置换群。

.2.3.\(Burnside\)引理

置换只考虑有等价类的置换(注意“什么都不变”的置换也满足该条件,也要在考虑范围内)。\(e.g.\)“若两种方案经旋转后能完全重合,则视为同一种方案”,则考虑旋转这一类置换。

一个置换的“不动点”:一种方案,满足其经过该置换后仍然不变。

每个置换的“不动点”个数的平均值就是原问题的不同方案数。

注意“什么都不变”的n次置换也要在考虑范围内!属于这一类置换的个数是1,不动点数是\(方法^n\)

.2.4.\(Poyal\)定理

前提条件:置换拆成的循环置换之间使用的方法不互相限定、相互影响。

一个置换的循环数:定理“任何一个置换均可拆成若干个循环置换的乘积”中,该置换被拆成的循环置换的个数。

\(一个置换的不动点数=方法^{循环数}\)。(证明:一个拆成的n次循环置换的n个元素都使用一个相同的方法就是原置换的一个不动点。故必须满足前提条件)

例题:串珠子

.3.交换群

定义:如果二元运算\(\cdot\)还满足交换律即\(\forall x, y\in G, \text{Have}\, x\cdot y=y\cdot x\),那么这个群被称为交换群,也称作阿贝尔群。

.多项式

..基础知识

...拉格朗日插值\(O(N\sim N^2+QN)\)

给定n个点\((x_i,y_i)\),可以唯一确定一个n-1次多项式y=f(x)。

\(y=f(x)=L(x)=\sum\limits_{i=1}^{n}(y_{i}\prod\limits_{j\in[1,n]\cap\Z\operatorname{and}j\not=i}\dfrac{x-x_j}{x_i-x_j})\)

\(x_i\)取值是连续的时,可以通过预处理前缀积和后缀积做到\(O(N)\)预处理\(l_i(x)\)中分母的逆元\(\prod\limits_{j\in[1,n]\cap\Z\operatorname{and}j\not=i}\dfrac{1}{x_i-x_j}\)

  • 证明

    由“n个点可以唯一确定一个n-1次多项式”得:设拉格朗日插值法的基函数:\(i,j\in[1,n],l_{i}(x_j)=\begin{cases}1,&i=j\\0,&i\ne j\\\end{cases}=A\times(x-x_1)\times\cdots\times(x-x_{i-1})\times(x-x_{i+1})\times\cdots \times (x-x_n)\)

    目前已满足\(l_{i}(x_j)=0(i\ne j)\),现在确定A以满足\(l_{i}(x_j)=1(i=j)\)\(A=\dfrac{1}{(x_i-x_0)\times\cdots\times(x_i-x_{i-1})\times(x_i-x_{i+1})\times\cdots \times (x_i-x_n)}\)

    所以$l_{i}(x)=\prod\limits_{j\in[1,n]\cap\Z\operatorname{and}j\not=i}^{n}!\dfrac{x-x_j}{x_i-x_j} $

    所以\(f(x)=L(x)=\sum\limits_{i=0}^{n}(y_{i}\times l_{i}(x))\)

int n;
LL x[N],y[N];   //给定的n个点
LL inv[N];  //l_i(x)中分母的逆元
LL facxx[N],invxx[N];   //x-x_j的逆元
map<int,int> h;//记录给定的n个点h(x_i)=y_i以特判防止除以0

//预处理l_i(x)中分母的逆元
void lagrange()
{
    for(int i=1;i<=n;i++)
    {
        h[x[i]]=y[i];
        inv[i]=1;
        for(int j=1;j<=n;j++) if(i!=j) inv[i]=inv[i]*(x[i]-x[j])%MOD;
        inv[i]=qpow(inv[i],MOD-2);
    }
    return ;
}

LL f(int xx)
{
    //特判。否则下面出现除0
    if(h.count(xx)) return h[xx];
    
    //线性求x-x_j的逆元
    facxx[0]=1;
    for(int i=1;i<=n;i++) facxx[i]=(facxx[i-1]*(xx-x[i]))%MOD;
    invxx[n]=qpow(facxx[n],MOD-2);
    for(int i=n-1;i>=1;i--) invxx[i]=invxx[i+1]*(xx-x[i+1])%MOD;
    for(int i=1;i<=n;i++) invxx[i]=invxx[i]*facxx[i-1]%MOD;
    
    LL res=0;
    for(int i=1;i<=n;i++) res=(res+y[i]*facxx[n]%MOD*invxx[i]/*连乘积要求j!=i*/%MOD*inv[i]%MOD)%MOD;
    
    return (res+MOD)%MOD;
}

scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lld%lld",&x[i],&y[i]);
lagrange();

int xx;
printf("%lld\n",f(xx));

...反演

...1.基础知识

  1. 反演本质:两个函数(数列)之间的双向(求和)关系。\(e.g.\)前缀和\(F[n]=\sum\limits_{i=0}^{n} G[i]\)与差分\(G[n]=F[n]-F[n-1]\)

  2. 元矩阵A[n,m]是满足\(\forall i,j, A[i,j]=[i==j]\)的矩阵

    两个矩阵A、B互逆\(\Leftrightarrow\)\(A*B=元矩阵\)

  3. 反演\(F[n]=\sum\limits_{i=0}^{INF} \{A[n,i]*G[i]\}\Leftrightarrow G[n]=\sum\limits_{i=0}^{INF} \{B[n,i]*F[i]\}\)(把系数看作矩阵,注意两边的i不是同一个i)\(\Leftrightarrow\)A、B互逆

    推论:

    1. A、B互逆\(\Leftrightarrow\)\(A^T\)\(B^T\)互逆

    2. A、B互逆\(\Leftrightarrow\)数乘c(c≠0):Ac、Bc互逆

    3. -1的移动:$\sum\limits_{t=0} { (-1)^{n-t}*A_{n,t}B_{t,m} } =[n==m] $$\Leftrightarrow$ \(\sum\limits_{t=0} \{ (-1)^{t}*A_{n,t}*(-1)^{m}B_{t,m} \} =[n==m]\)。(证明:数乘\((-1)^{m-n}\),然后分成两部分)
      可以使得只有一边有 -1−1 的幂的形式和两边都有的形式互推。

    4. \(A_1\)\(B_1\)互逆,\(A_2\)\(B_2\)互逆,则\(A_1*A_2\)\(B_1*B_2\)互逆。

      可以使得多个维度的反演叠加:

      假如有:

      \(F[n]=\sum\limits_{i=0}A_1[n,i]G[i]\Leftrightarrow G[n]=\sum\limits_{i=0}B_1[n,i]F[i]\)

      \(F[n]=\sum\limits_{i=0}A_2[n,i]G[i]\Leftrightarrow G[n]=\sum\limits_{i=0}B_2[n,i]F[i]\)

      那么有如下结论(二维情况):

      \(F[n,m]=\sum\limits_{i=0}\sum\limits_{j=0}A_1[n,i]A_2[m,j]G[i,j]\Leftrightarrow G[n,m]=\sum\limits_{i=0}\sum\limits_{j=0}B_1[n,i]B_2[m,j]F[i,j]\)

      也即 : 多维反演,系数=每个维度的反演系数之

...1.莫比乌斯反演

详见《数学1.4.莫比乌斯反演》。

...2.二项式反演

  1. 常见反演形式:

    \(F[n]=\sum\limits_{i=0} \{ (-1)^{i}*C_{n}^{i}*G[i] \} \Leftrightarrow G[n]=\sum\limits_{i=0} \{ (-1)^{i}*C_{n}^{i}*F[i] \}\),显然i有上限n(\(C_{n}^{n^+}=0\))。证明:\(C_{n}^{i}*(-1)^{i}\)\(C_{n}^{i}*(-1)^{i}\)互逆。

    \(F[n]=\sum\limits_{i=0} \{ C_{n}^{i}*G[i] \} \Leftrightarrow G[n]=\sum\limits_{i=0} \{ (-1)^{n-i}*C_{n}^{i}*F[i] \}\)。证明:上式-1的移动。

    \(F[n]=\sum\limits_{i=n} \{ C_{i}^{n}*G[i] \} \Leftrightarrow G[n]=\sum\limits_{i=n} \{ (-1)^{i-n}*C_{i}^{n}*F[i] \}\)。证明:上式\(C_{n}^{i}\)\((-1)^{n-i}*C_{n}\)互逆\(\Leftrightarrow\)\((C_{n}^{i})^T=C_{i}^{n}\)\(((-1)^{n-i}*C_{n})^T=(-1)^{i-n}*C_{i}\)互逆。

    \(F[n]=\sum\limits_{i=n} \{ (-1)^{i}*C_{i}^{n}*G[i] \} \Leftrightarrow G[n]=\sum\limits_{i=n} \{ (-1)^{i}*C_{i}^{n}*F[i] \}\)。证明:上式-1的移动。

  2. 二项式定理:\((x+y)^n=\sum\limits_{k=0}^{n} \{ C_{n}^{k}*x^{n-k}*y^k \}=\sum\limits_{k=0}^{n} \{ C_{n}^{k}*x^k*y^{n-k} \}\)

  3. 正难则反思想。

  4. 钦定法。

    适用条件:“至少”、“最多”、“恰好”组合计数类问题。

    下面以“ 有一个 \(n \times n\) ( \(n \leq 10^6\) )的正方形网格,用红色,绿色,蓝色三种颜色染色,求有多少种染色方案使得至少一行或一列是同一种颜色。”为例:注意学习6个步骤和红色的变形

    1. 设答案F。

      正难则反。设\(F[i][j]\):恰好有i行和j列是同一种颜色(其余行和列不能同颜色,较难求解)。\(ANS=3^{n*n}-F[0][0]\)

    2. 设钦定G。

      \(G[i][j]\):钦定有i行和j列是同一种颜色(钦定法:只管钦定的内容,不考虑未钦定的其余行和列,即不管其余行/列是否是同一种颜色,怎么容易写出G的表达式怎么钦定,一道题可以有多种钦定的方法,只不过每种钦定到第三步用F表示G有不同)。

      \(G[i][j]=\begin{cases}3^{n*n}, & i=0,j=0,\\ C_n^j*3^j*3^{n*(n-j)}, & i=0,j≥1\\ C_n^i*3^i*3^{n*(n-i)}, & i≥1,j=0 \\ C_n^i*C_n^j*3*3^{n*n-(n*(i+j)-i*j)}, &i≥1,j≥1\end{cases}\)

      注意分类讨论。当i≥1,j≥1时,因为行和列有公共网格所以i行和j列是同一颜色:3种情况。

    3. 用不好求的表示好求的:F表示G。

      \(G[i][j]=\sum\limits_{x=i}\sum\limits_{y=j} \{ C_x^i*C_y^j*F[x][y] \}\):在F的x行和y列中钦定i行和j列。

    4. 反演:用G表示F。

      \(F[i][j]=\sum\limits_{x=i}\sum\limits_{y=j} \{ (-1)^{x-i+y-j}*C_x^i*C_y^j*G[x][y] \}\)

    5. 继续求解。

      \(F[0][0]=\sum\limits_{x=0}\sum\limits_{y=0} \{ (-1)^{x+y}*G[x][y] \}\)

      预处理阶乘及其逆元\(O(N)\)

      当x=0,y=0时,可以\(O(\log N)\)求解:\(F[0][0]+=1*(3^n*3^n)\)

      当x=0,y≥1或x≥1,y=0时,可以\(O(N \log N)\)求解:x或y为0本质一样,不妨只算x,贡献翻倍即可:\(F[0][0]+=2*\sum\limits_{x=1} \{ (-1)^x*C_n^x*3^{x+n*(n-x)} \}\);

      当x≥1,y≥1时,目前只能\(O(N^2)\)求解。

    6. 降低复杂度。

      当x≥1,y≥1时,\(F[0][0]+=\sum\limits_{x=1}\sum\limits_{y=1} \{ (-1)^{x+y}*C_n^x*C_n^y*3*3^{n*n-(n*(x+y)-x*y)}\}\)

      由分配律,右式\(=3^{1+n*n}*\sum\limits_{x=1}\{ (-1)^x*C_n^x*3^{-n*x}*\sum\limits_{y=1}\{ (-1)^y*C_n^y*3^{-n*y}*3^{x*y}\} \}\)

      处理O(N^2)的xy乘积项:\(\sum\limits_{y=1}\{ (-1)^y*C_n^y*3^{-n*y}*3^{x*y}\}\)

      整理,-1=1*-1,使-1变成更灵活的1:\(=\sum\limits_{y=1}\{ C_n^y*1^y*(-1)^y*3^{(x-n)^y}\}\)

      整理,1的变换:\(=\sum\limits_{y=1}\{ C_n^y*1^{n-y}*(-1*3^{x-n})^y\}\)

      二项式定理:\(=(1-3^{x-n})^n-1\),注意求和下标不到0,最后减去\(C_n^0*1^n*(-1*3^{x-n})^0=1\)

      所以\(O(N\log N)\)\(F[0][0]+=3^{1+n*n}*\sum\limits_{x=1}\{ (-1)^x*C_n^x*3^{-n*x}*((1-3^{x-n})^n-1)\}\)

      同理当x=0,y≥1或x≥1,y=0时,可以\(O(\log N)\)求解\(F[0][0]+=2*3^{n*n}*((1-3^{1-n})^n-1)\)

    代码详见《错题本数学2022.7.7.【推式子+二项式反演】Sky Full of Stars》

    错题本

..多项式计算

卷积:\(c_i=\sum\limits_{j\oplus k==i}a_jb_k\),其中\(\oplus\)为二元运算符。

求卷积的一般过程:

  1. 求值:a→a'=s(a)。
  2. 点乘:c'=a'·b'。
  3. 插值:c'→c=d(c')。

s(a),d(a)是两种序列变换,且互为逆变换。一个典型的例子是前缀和与差分。

c=d(s(a)·s(b))。

技巧

求连卷积:1st对这些运算多项式分别求值;2nd连点乘;3rd对这个答案多项式插值。

...1.和卷积

适用条件:\(\oplus\)\(+\)

一般的卷积形式,也被称为多项式乘法。

\(c_i=\sum\limits_{j+ k==i}a_jb_k\)\(h(n)=\sum\limits_{i=0}^nf(i)g(n-i)\)

...1.1.快速傅里叶变换FFT\(O(N\log N)\)

适用条件:无模数。

数组大小开2倍!!!因为n次多项式m次多项式得到n+m次多项式。并且要开**\(2^n\)***的形式**,因为分治。

\(e.g.\)对于数据范围是1e5,const int N=(1<<18)+10;//1e5+1e5=2e5 < 1<<18

结果记得除以tot!!!并且+0.5防止精度误差。

作用:解决多项式乘法。本质上是快速求出一个多项式A特定的n个点\((x_1,A(x_1)),(x_2,A(x_2)),...,(x_n,A(x_n))\)

我们只关心系数,并不关心x具体是多少。x的具体值并不影响系数

由性质“任意n个不同点,可唯一确定一个n-1次多项式”得:先把2个多项式用FFT从系数表达式转化为点表达式\(O(N \log N)\),再用点表达式计算乘法\(O(N+M)\),最后把结果用FFT从点表达式转化为系数表达式\(O(N \log N)\)

1的n次单位根\(\omega_n^k\)

  • 性质(利用单位圆记忆)
    • \(\forall i≠j,w_n^i≠w_n^j\)。因此FFT满足点表达式要求的“n个不同点”。
    • \(w_n^k=\cos \frac{k*2\pi}{n}+i*\sin \frac{k*2\pi}{n}\)
    • \(w_n^0=w_n^n=1\)
    • \(w_{2n}^{2k}=w_n^k\)
    • \(w_n^{k+\frac{n}{2}}=-w_n^k\)
    • \(w_n^i*w_n^j=w_n^{i+j}\)

FFT正变换:从系数表达式转化为点表达式\(O(N\log N)\)

转化为n+m+1个点。也就是刚好可以唯一确定乘积多项式的点数。虽然对于当前的n次多项式只需要n+1个点即可确定,但是多取一些点并不影响正确性。

  1. 点表达式n个点的x选为1的n次单位根\(\omega_n^k\)

  2. 按下标的奇偶分成左半区间和右半区间。

    \(A_1(x)=a_0+a_2x+a_4x^2+...\)\(A_2(x)=a_1+a_3x+a_5x^2+...\),则\(A(x)=A_1(x^2)+x*A_2(x^2)\)

  3. 对左半区间和右半区间分类讨论。

    考虑将\(\omega_{n}^{0..n-1}\)代入作为n个点,那么对于\(\forall k\in\left[0,\frac{n}{2}-1\right]\),有:

    左:\(A\left(\omega_n^k\right)=A_1\left(\omega_n^{2k}\right)+\omega_n^kA_2\left(\omega_n^{2k}\right)=A_1\left(\omega_{\frac{n}{2}}^{k}\right)+\omega_n^kA_2\left(\omega_{\frac{n}{2}}^{k}\right)\)

    右:\(A\left(\omega_n^{k+\frac{n}{2}}\right)=A_1\left(\omega_n^{2k+n}\right)+\omega_n^{k+\frac{n}{2}}A_2\left(\omega_n^{2k+n}\right)=A_1\left(\omega_{\frac{n}{2}}^{k}\right)-\omega_n^kA_2\left(\omega_{\frac{n}{2}}^{k}\right)\)

    可以发现右半区间的式子和左半区间的式子仅有第二个单项式正负符号的不同,因此在求解左半区间的时候可以一起求解右半区间,因此只需要递归一半区间求解。

  4. 分治递归求解。

FFT逆变换:从点表达式转化为系数表达式\(O(N\log N)\)

可证明若点表达式是\((x_k=\omega_n^k,y_k=A\left(\omega_n^k\right))\),则系数表达式\(A\left(x\right)=a_0+a_1x+\cdots+a_{n-1}x^{n-1}\),其中$a_k=\frac{c_k}{n} \(,其中\)c_k=\sum\limits_{i=0}{n-1}y_i\left(\omega_n\right)i=B\left(\omega_n\right)\(,其中B是我们设出来的另一个多项式,系数就是\)y_k\(,注意B(x)中的\)\omega_n{-k}$与FFT正变换的$\omega_nk$正负符号有差异。

因此接下来可以用FFT正变换解决FFT逆变换了。

卡常

FFT常数大,必须卡常。

  • 迭代代替递归。

    迭代初始各数的位置等于各数i的下标的倒序比特位rev[i]。

    递推求倒序比特位:(文字描述定义数位低位在前,高位在后)假设对于二进制下位数是b-1的数我们已经预处理好了。对于当前二进制下位数是b的数,我们利用已经处理好的倒序比特位先把后b-1位翻转`rev[i>>1]>>1`。然后把第1位插在后面`|((i&1)<<(bit-1))`。
    
     e.g.0011010 翻转后是0101100。(注意前导零要补到bit-1位)
    
  • 结构体重载运算符时加const和&。

  • 手算cos(-x)=cos(x),sin(-x)=-sin(x)。

数组大小开2倍!!!因为n次多项式m次多项式得到n+m次多项式。并且要开**\(2^n\)***的形式**,因为分治。

\(e.g.\)对于数据范围是1e5,const int N=(1<<18)+10;//1e5+1e5=2e5 < 1<<18

结果记得除以tot!!!并且+0.5防止精度误差。

const int N=(1<<18)+10;//1e5+1e5=2e5 < 1<<18
int n,m;
int tot;    //分治区间总长度
int rev[N]; //fft采用迭代,迭代初始各数的位置等于各数的下标的倒序比特位
struct Complex  //复数
{
    double x,y; //x:实部;y:虚部
    Complex operator + (const Complex &qw) const//const+&:卡常
    {
        return {x+qw.x,y+qw.y};
    }
    Complex operator - (const Complex &qw) const
    {
        return {x-qw.x,y-qw.y};
    }
    Complex operator * (const Complex &qw) const
    {
        return {x*qw.x-y*qw.y,x*qw.y+y*qw.x};
    }
}a[N],b[N]; //多项式系数

void pre_bit()
{
    int bit=0;
    while((1<<bit)<n+m+1) bit++;    //直到1<<bit >= n+m+1。+1:n+m次多项式有n+m+1项,需要n+m+1个点确定
    tot=1<<bit;
    for(int i=1;i<tot;i++) rev[i]=(rev[i>>1]>>1/*利用已经处理好的倒序比特位先把后b-1位翻转*/)|((i&1)<<(bit-1)/*然后把第1位插在后面*/);  //注意下标从0开始。递推求倒序比特位
    return ;
}

void fft(Complex res[],int inv) //inv=1:转化为点表达式;inv=-1:转化为系数表达式。迭代实现递归,卡常
{
    for(int i=0;i<tot;i++) if(i<rev[i]/*防止交换2次等于没换*/) swap(res[i],res[rev[i]]);    //fft采用迭代,用预处理的倒序比特位确定迭代初始各数的位置
    for(int mid=1;mid<tot;mid<<=1)  //遍历每一层。直到当前区间一半的长度==tot,就不用合并了
    {
        Complex w1={cos(PI/mid),inv*sin(PI/mid)};   //w1=2*PI/n,n=2*mid->w1=PI/mid。手算cos(-x)=cos(x),sin(-x)=-sin(x),卡常。这里是为下面w_n^k设计的单位量。而A(w)中的w是其上一层的一半,因此不必为其设计单位量
        for(int i=0;i<tot;i+=mid<<1/*别忘了乘2*/)    //遍历当前层的各个区间,注意下标从0开始
        {
            Complex wk={1,0};
            for(int j=0;j<mid;j++,wk=wk*w1/*注意这里是乘而不是加*/)  //遍历当前层对应的下一层的元素来得到当前层的信息
            {
                Complex x=res[i+j],y=wk*res[i+j+mid];//注意这里是传进来的res,不要手滑打成了a
                res[i+j]=x+y,res[i+j+mid]=x-y;
            }
        }
    }
    return ;
}

scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++) scanf("%lf",&a[i].x);   //注意下标从0开始
for(int i=0;i<=m;i++) scanf("%lf",&b[i].x);
pre_bit();
fft(a,1),fft(b,1);  //转化为点表达式
for(int i=0;i<tot;i++) a[i]=a[i]*b[i];  //点表达式的运算
fft(a,-1);  //转化为系数表达式
for(int i=0;i<=n+m;i++) printf("%d ",(int)(a[i].x/tot+0.5));    //注意最后要除以tot!!!+0.5:防止精度误差

FFT求高精度乘法\(O(N\log N)\)

把一个数字转化为多项式的形式:\(f(x)=a_0+a_1*x+a_2*x^2+...\),也就是f(10)(我们只关心系数,并不关心x具体是多少。x的具体值并不影响系数。)。然后跑一遍fft即可。

注意:1.读入的时候数字倒着存!2.最后结果的数位记得进位(最多进位到n+m+1)!

int n,m;
int tot;
int rev[N];
struct Complex {}a[N],b[N];
int c[N],up;    //最终数的乘积。记得进位

string num;
cin>>num;   //不用getline(cin),因为会读入行末空格
n=num.length()-1;
for(int i=0;i<=n;i++) a[i].x=num[n-i]-'0';  //注意num倒着存!!!
cin>>num;
m=num.length()-1;
for(int i=0;i<=m;i++) b[i].x=num[m-i]-'0';

pre_bit();
fft(a,1),fft(b,1);
for(int i=0;i<tot;i++) a[i]=a[i]*b[i];
fft(a,-1);

for(int i=0;i<=n+m;i++) c[i]=(int)(a[i].x/tot+0.5);
for(int i=0;i<=n+m+1;i++)   //进位。最多进位到n+m+1
{
    if(c[i]!=0) up=i;
    c[i+1]+=c[i]/10;
    c[i]%=10;
}
for(int i=up;i>=0;i--) printf("%d",c[i]);
puts("");

...1.2.NTT

适用条件:模数是998244353。

\(998244353-1=2^{23}*7*17\),998244353的原根是3。

把FFT中的\(w_n^1\)换成\(g^{(mod-1)/n}\),复数运算改为模mod意义下的正整数运算即可。

//注释或省略的部分都是与FFT相同的部分
const int MOD=998244353,G=3,IG=332748118;   //G:MOD的原根;IG:G在模MOD意义下的逆元
int inv_tot;    //预处理tot在模MOD意义下的逆元
LL a[N],b[N];

void NTT(LL res[],int inv)
{
    // for(int i=0;i<tot;i++) if(i<rev[i]) swap(res[i],res[rev[i]]);
    // for(int mid=1;mid<tot;mid<<=1)
    // {
        LL w1=qpow(inv==1 ? G : IG,(MOD-1)/(mid<<1));
        // for(int i=0;i<tot;i+=mid<<1)
        // {
            LL wk=1;
            for(int j=0;j<mid;j++,wk=wk*w1%MOD)
            {
                LL x=res[i+j],y=wk*res[i+j+mid]%MOD;
                res[i+j]=(x+y)%MOD,res[i+j+mid]=(x-y+MOD)%MOD;
            }
        // }
    // }
    return ;
}

// scanf("%d%d",&n,&m);
// for(int i=0;i<=n;i++) scanf("%lld",&a[i]);
// for(int i=0;i<=m;i++) scanf("%lld",&b[i]);
// pre_bit();
ntt(a,1),ntt(b,1);
// for(int i=0;i<tot;i++) a[i]=a[i]*b[i]%MOD;
ntt(a,-1);
for(int i=0;i<=n+m;i++) printf("%lld ",a[i]*inv_tot%MOD);

...2.积卷积

适用条件:\(\oplus\)\(\times\)

即狄利克雷卷积。

《数学1.4.6.狄利克雷卷积》

...3.最值卷积

适用条件:\(\oplus\)\(\max/\min\)。下面以max卷积为例:

容斥原理:\(c_i=\sum\limits_{\max(j,k)=i}a_jb_k=\sum\limits_{\max(j,k)≤i}a_jb_k-\sum\limits_{\max(j,k)≤i-1}a_jb_k\)。其中\(\sum\limits_{\max(j,k)≤i}a_jb_k=(\sum\limits_{j≤i}a_j)*(\sum\limits_{k≤i}b_k)\)

先对a,b求出前缀和,再将a,b对应位置分别相乘得到c,最后对c进行差分。

...4.位运算卷积

适用条件:\(\oplus\)为位运算符。

位运算的重要性质:每一位的独立性。两个n 位二进制数的位运算可以看作n维空间\(\{0,1\}^n\)上的两个向量之间的运算。与运算相当于取min,或运算相当于取max,异或运算相当于不进位加法。

...4.1.快速莫比乌斯变换FMT\(O(bit*2^{bit})\)

适用条件:\(\oplus\)\(\&/|\)

以或卷积为例:

类似于最值卷积,容斥原理:\(c_i=\sum\limits_{j|k=i}a_jb_k=\sum\limits_{j|k\sube i}a_jb_k-\sum\limits_{j|k\sube ps(i)}a_jb_k\)。其中\(\sum\limits_{j|k\sube i}a_jb_k=(\sum\limits_{j\sube i}a_j)(\sum\limits_{k\sube i}b_k)\);ps(i):i的真子集。

\(a'_i=FMT_{or}(a)_i=\sum\limits_{j\sube i}a_j\)\(c_i'=a_i'b_i'\)\(c_i=IFMT_{or}(c')_i=\sum\limits_{j\sube i}c_j-\sum\limits_{j\sube ps(i)}c_j\)\(IFMT_{or}\)\(FMT_{or}\)的逆变换。\(FMT_{or}\)\(IFMT_{or}\)实际上分别是高维空间前缀和(子集和)与差分。

\(c=IFMT_{or}(FMT_{or}(a)·FMT_{or}(b))\)

\(FMT_{and}\)实际上是超集和。

void FMT_or(LL x[],int inv)
{
    for(int i=0;i<n;i++)//高维前缀和每个维度做前缀和的先后顺序可以任意
        for(int j=0;j<(1<<n);j++)//由于每个维度的大小都是2,所以就算从前往后做差分也不影响答案
            if((j>>i)&1)
                add(x[j],inv*x[j-(1<<i)]);
    return ;
}

FMT_or(a,1),FMT_or(b,1);
for(int i=0;i<(1<<n);i++) c[i]=a[i]*b[i]%MOD;
FMT_or(c,-1);
for(int i=0;i<(1<<n);i++) printf("%lld ",c[i]);

void FMT_and(LL x[],int inv)
{
    for(int i=0;i<n;i++)
        for(int j=0;j<(1<<n);j++)
            if((j>>i)&1)
                add(x[j-(1<<i)],inv*x[j]);
    return ;
}
    
FMT_and(a,1),FMT_and(b,1);
for(int i=0;i<(1<<n);i++) c[i]=a[i]*b[i]%MOD;
FMT_and(c,-1);
for(int i=0;i<(1<<n);i++) printf("%lld ",c[i]);

容斥原理求单项插值系数\(O(|\text{subset}(state)|)\)

已知\(c'(c'_i=\sum\limits_{j\sube i}c_j)\),求\(c_{state}\)

ll c_state=0;
for(int s=state;;s=(s-1)&state)
{
    if(ppcnt[state-s]&1) c_state=(c_state-c[s]+MOD)%MOD;
    else c_state=(c_state+c[s])%MOD;
    if(!s) break;
}

...4.2.快速沃尔什变换FWT\(O(bit*2^{bit})\)

适用条件:\(\oplus\)\(\operatorname{xor}\)

本质上是在每一维实施长度为2的FFT。对于每一维:\(\{a_0,a_1\}\xrightarrow{FWT}\{a_0+a_1,a_0-a_1\},\{a_0,a_1\}\xrightarrow{IFWT}\{\frac{1}{2}(a_0+a_1),\frac{1}{2}(a_0-a_1)\}\)

  • 证明正确性

    只要证明每一维(每个比特位)是正确的,那么对于整体也是正确的:

    \(\{a_0,a_1\}\xrightarrow{FWT}\{a_0+a_1,a_0-a_1\},\{b_0,b_1\}\xrightarrow{FWT}\{b_0+b_1,b_0-b_1\} \\\{a_0+a_1,a_0-a_1\}·\{b_0+b_1,b_0-b_1\}=\{(a_0+a_1)(b_0+b_1),(a_0-a_1)(b_0-b_1)\} \\\{(a_0+a_1)(b_0+b_1),(a_0-a_1)(b_0-b_1)\}\xrightarrow{IFWT}\{a_0b_0+a_1b_1,a_0b_1+a_1b_0\}\)

void FWT_xor(LL x[],int inv)
{
    for(int i=0;i<n;i++)
        for(int j=0;j<(1<<n);j++)
            if((j>>i)&1)
            {
                LL x0=x[j-(1<<i)],x1=x[j];
                x[j-(1<<i)]=(x0+MOD+x1+MOD)%MOD*inv%MOD,x[j]=(x0+MOD-x1+MOD)%MOD*inv%MOD;
            }
    return ;
}

FWT_xor(a,1),FWT_xor(b,1);
for(int i=0;i<(1<<n);i++) c[i]=a[i]*b[i]%MOD;
FWT_xor(c,qpow(2,MOD-2));
for(int i=0;i<(1<<n);i++) printf("%lld ",c[i]);

...4.3.子集卷积\(O(bit^22^{bit})\)

适用条件:\(c_i=\sum\limits_{j\&k=0,j|k=i}a_jb_k\)

j&k=0,j|k=i\(\Leftrightarrow\)popcount(j)+popcount(k)=popcount(i),j|k=i。

\(f_{popcount(i),i}=a_i\)。然后做或卷积+背包合并即可。

如果从小到大枚举popcount(i)依次做或卷积+背包合并,就可以在dp题中优化dp。

注意popcount(i)是可以达到n的,注意取等。

for(int i=0;i<(1<<n);i++) a[count(i)][i]=read();
for(int i=0;i<(1<<n);i++) b[count(i)][i]=read();
for(int i=0;i<=n;i++)//注意popcount(i)是可以达到n的,注意取等
{
    FMT_or(a[i],1),FMT_or(b[i],1);
    for(int j=0;j<=i;j++)
        for(int k=0;k<(1<<n);k++)
            add(c[i][k],a[j][k]*b[i-j][k]%MOD);
    FMT_or(c[i],-1);
}
for(int i=0;i<(1<<n);i++) printf("%lld ",c[count(i)][i]);

子集卷积优化子集dp\(O(3^N)→O(N^22^N)\)

适用条件:“划分成若干个集合”,\(f[state]=a_{state}*(\sum\limits_{s\subsetneqq state}f[state\operatorname{xor}s]g[s])\)

f[0][0]=//边界
FMT_or(g[0],1);
for(int i=1;i<=n;i++)
{
    FMT_or(f[i-1],1),FMT_or(g[i],1);
    for(int j=1;j<=i;j++)
        for(int k=0;k<(1<<n);k++)
            add(f[i][k],g[j][k]*f[i-j][k]%MOD);
    FMT_or(f[i],-1);
    for(int j=0;j<(1<<n);j++) f[i][j]=f[i][j]*a[j]%MOD;
}
printf("%lld\n",f[n][(1<<n)-1]);

.生成函数

定义:给定一个序列\(a_0,a_1,a_2,...,a_n,...\),则生成函数\(g(x)=a_0+a_1x+a_2x^2+...+a_nx^n+...\),其中令-1<x<1,因为这样才能收敛。相当于将原序列映射到多项式。

.1.函数模型1:乘法原理

我们不关心x,只关心多项式的系数。

  1. 将原问题分解成若干个子问题。

  2. 先求每个子问题的生成函数。

    原问题\(\Leftrightarrow\)多项式或函数问题

    原问题的一种选法\(\Leftrightarrow\)多项式乘法分配律展开后的某一项

    原问题的子问题\(\Leftrightarrow\)子函数\(g(x)=g_0(x)g_1(x)g_2(x)...g_n(x)...\)

    方案数\(\Leftrightarrow\)多项式系数

    第i种方案(具有可加性)\(\Leftrightarrow\)\(x_i\)

  3. 然后整个生成函数等于子函数的乘积。

技巧

  1. 收敛。

    令-1<x<1。

    • \(1+x+x^2+x^3+...=\dfrac{1-x^n}{1-x}\xlongequal{n→+\infty}\dfrac{1}{1-x}\)

      \(\dfrac{1}{(1-x)^k}\)\(n\)次项的系数为\(C_{n+k-1}^{k-1}\)

      • 证明

        引例:求\(a_1+a_2+...+a_k=n\)的自然数解的数量。

        组合数方法:由组合数得\(=C_{n+k-1}^{k-1}\)

        生成函数方法:设子函数\(f_1(x)=1+x+x^2+x^3+...=\dfrac{1}{1-x},f_2(x)=\dfrac{1}{1-x},...,f_k(x)=\dfrac{1}{1-x}\)。则\(f(x)=f_1(x)f_2(x)...f_k(x)=\dfrac{1}{(1-x)^k}\)

        将组合数映射到生成函数即可得证。

  2. 通分约分。

    多个子函数相乘时,可以通过通分和约分化简乘积。

.1.1.生成函数解决升级版多重背包方案数

每件物品有复杂的携带限制($e.g.$0个或1个或3个;奇数个;4的倍数个……),计算携带 N 件物品的方案数。

  1. 将原问题(F。第N次项的系数是最终答案)分解成若干个子问题:对于每件物品单独考虑携带几件。

  2. 根据原问题\(\Leftrightarrow\)多项式或函数问题的分析,设计子问题的生成函数。

    • \(e.g.\)
      • 0个或1个或3个:\(f_1(x)=1+x+x^3\)
      • 奇数个:\(f_2(x)=x+x^3+x^5+...=\frac{x}{1-x^2}\)
      • 4的倍数个:\(f_3(x)=1+x^4+x^8+…=\frac{1}{1−x^4}\)
  3. 把子函数乘起来,注意使用通分约分技巧化简乘积。求出第N次项的系数。

例题:食物。

posted @ 2025-10-14 01:04  Brilliance_Z  阅读(12)  评论(0)    收藏  举报