动态规划指南
同余定理
同余性质
同余性质是指在任意情况下,都有:
- $n\times m\bmod p$=$(n\bmod p)\times(m\bmod p)\bmod p$
- $n+m\bmod p$=$(n\bmod p)+(m\bmod p)\bmod p$
- $n-m\bmod p$=$(n\bmod p)-(m\bmod p)\bmod p$
除法不满足同余性质。
欧拉定理
欧拉函数:$\varphi(n)$ 表示在 $1$ 和 $n$ 之间与 $n$ 互质的个数,即 $\sum_{i=1}^{n}[\gcd(i,n)=1]$。
欧拉定理:如果 $\gcd(n,p)=1$,则有 $n^p\equiv1\pmod p$。
乘法逆元
乘法逆元指如果 $a\times b\equiv1\pmod p$,则称 $b$ 为 $a$ 的 $p$ 意义下逆元。
快速幂求逆元
因为 $a\times b\equiv1\pmod p$,则有 $a\times b\equiv a^{p-1}\pmod p$,同时除以 $a$,则有 $b\equiv a^{p-2}\pmod p$,所以可以用快速幂求逆元。
递推求逆元
令 $div=\lfloor\frac{i}{p}\rfloor$,$mod=i\mod p$,则有:
$$div\times i+mod\equiv 0\pmod p$$
同时转为逆元,则:
$$div^{-1}\times i{-1}+mod\equiv 0\pmod p$$
$$i^{-1}\equiv div^{-1}\times mod^{-1}\pmod p$$
$$i^{-1}\equiv div=\lfloor\frac{i}{p}\rfloor^{-1}\times(i\bmod p)^{-1}\pmod p$$
就可以递推求了。
ll inv[MAXN];
inline void prework(ll mod){
inv[1]=1;//初始值
for(int i=2;i<MAXN;++i){
inv[i]=(mod-mod/i)*inv[mod%i]%mod;//递推式
}
}
gcd 定理
exgcd
$$a\times x_1+b\times y_1=\gcd(a,b)$$
$$b\times x_2+(a\bmod b)\times y_2=\gcd(a,a\bmod b)$$
之后不断往下递归就可以了。
ll exgcd(ll a,ll b,ll &x,ll &y){
if(!b){
x=1;
y=0;//肯定有这么一组
return a;
}
ll d=exgcd(b,a%b,x,y);//公式
x=y;
y=d-(a/b)*y;//公式
return d;
}
例题
直接套板子即可。
#include<bits/stdc++.h>
#define getchar getchar_unlocked
#define MOD 19260817
using namespace std;
typedef long long ll;
inline ll scan(void){
ll res=0,f=1;
char c=getchar();
while(!isdigit(c)){
if(c=='-'){
f=-1;
}
c=getchar();
}
while(isdigit(c)){
(res=(res<<1)+(res<<3)+(c^48))%=MOD;
c=getchar();
}
return f*res;
}
inline void exgcd(ll a,ll b,ll &x,ll &y){
if(b==0){
x=1;
y=0;
return;
}
exgcd(b,a%b,x,y);
ll tmp=x;
x=y;
y=tmp-a/b*y;
}
int main(){
ll a=scan(),b=scan(),x=0,y=0;
exgcd(b,MOD,x,y);
x=(x%MOD+MOD)%MOD;
printf("%lld",x*a%MOD);
return 0;
}
裴蜀定理
裴蜀定理指如果要满足 $ax+by=c$,则需要满足 $\gcd(a,b)\mid c$。没什么特别好讲的,主要主语结论。
例题
套板子即可。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,ans;
scanf("%d %d",&n,&ans);
while(--n){
int x;
scanf("%d",&x);
if(x<0){
x=-x;
}
if(x!=0){
ans=__gcd(ans,x);
}
}
printf("%d",ans);
return 0;
}
积性函数和筛法
欧拉函数
欧拉函数上面讲到过,其实可以拆解成:$\Pi_{p\in Prime,p\mid n}(p-1)$,所以,可以用欧拉筛筛出来。
莫比乌斯函数
莫比乌斯函数的定义是这样的:
$$\mu(n)=\begin{cases}
1&n\in Prime\
0&p^2\mid n,p\in Prime\
(-1)k&n=\Pi_{i=1}p_i^{c_i},p_i\in Prime
\end{cases}$$
这也是积性函数,所以可以用欧拉筛筛出来。
数论分块
数论分块是一种优化方法,当式子变成形如:
$$\Large\sum_{i=1}^{n}G(i)\times H(\lfloor\frac{n}{i}\rfloor)$$
这样的式子,数论分块能够将其优化至将近 $\sqrt{n}$ 的复杂度。
我们可以发现,$\lfloor\frac{n}{i}\rfloor$ 这一段会有很多相同的情况,于是,我们可以利用 $\lfloor\frac{n}{i}\rfloor$ 的相同进行优化。
设 $l$ 和 $r$ 为这一块的左端点和右端点,那么有 $\lfloor\frac{n}{l}\rfloor=\lfloor\frac{n}{r}\rfloor$,可以得出 $r$ 最大为 $\lfloor\frac{n}{\lfloor\frac{n}{l}\rfloor}\rfloor$,那么就可以这样进行跳块了。
for(int l=1,r=0;l<=n;l=r+1){//跳块
r=n/(n/l);
res+=(S(r)-S(l-1))*G(n/l);//前缀和加上分块的形式
}
莫比乌斯反演
有一个性质,$\sum_{d\mid n}\mu(d)=[n=1]$,所以,这可以将形如:
$$F(n)=\sum_{d\mid n}f(\lfloor\frac{n}{d}\rfloor)$$
$$f(n)=\sum_{d\mid n}\mu(d)\times F(\lfloor\frac{n}{d}\rfloor)$$
还可以将形如:
$$[\gcd(a_1,a_2,a_3\dots a_n)=1]$$
变成:
$$\sum_{d\mid a_1,d\mid a_2,d\mid a_3\dots d\mid a_n}\mu(d)$$
例题
基于上面的反演公式,可以转化为:
$$\sum_{d=1}{n}\sum_{t=1}\rfloor}\mu(t)\sum_{i=1}^{\lfloor\frac{n}{d\times t}\rfloor}\varphi(i)\sum_{j=1}^{\lfloor\frac{m}{d\times t}\rfloor}\varphi(j)$$
之后掏出一个 $G(n)=\sum_{i=1}^{n}\varphi(i)$,有:
$$\sum_{d=1}{n}\sum_{t=1}\rfloor}\mu(t)\times G(\lfloor\frac{n}{d\times t}\rfloor)\times G(\lfloor\frac{m}{d\times t}\rfloor)$$
再掏出一个 $H(n,m,t)=\sum_{i=1}^{t}\mu(t)\times G(\lfloor\frac{n}{i}\rfloor)\times G(\lfloor\frac{m}{i}\rfloor)$。
$$\sum_{d=1}^{n}H(\lfloor\frac{n}{d}\rfloor,\lfloor\frac{m}{d}\rfloor,\lfloor\frac{t}{d}\rfloor)$$
之后预处理加上数论分块优化就可以了。
#include<bits/stdc++.h>
#define MAXN 50005
#define MAXM 55
#define MOD 23333
using namespace std;
int t,n,m;
int phi[MAXN],mob[MAXN];
vector<int> prim,G[MAXN],S[MAXM][MAXM];
bool flag[MAXN];
inline void prework(){
flag[1]=phi[1]=mob[1]=1;
for(int i=2;i<MAXN;++i){
if(!flag[i]){
prim.push_back(i);
phi[i]=i-1;
mob[i]=-1;
}
for(int j=0;j<prim.size()&&i*prim[j]<MAXN;++j){
flag[i*prim[j]]=true;
if(i%prim[j]){
phi[i*prim[j]]=phi[i]*(prim[j]-1)%MOD;
mob[i*prim[j]]=-mob[i];
}else{
phi[i*prim[j]]=phi[i]*prim[j]%MOD;
break;
}
}
}
for(int i=1;i<MAXN;++i){
G[i].push_back(0);
for(int j=1;i*j<MAXN;++j){
G[i].push_back((G[i].back()+phi[i*j])%MOD);
}
}
for(int i=1;i<MAXM;++i){
for(int j=1;j<MAXM;++j){
S[i][j].resize(MAXN/max(i,j)+1);
for(int d=1;d*max(i,j)<MAXN;++d){
for(int k=1;k*d*max(i,j)<MAXN;++k){
(S[i][j][k*d]+=G[d][i]*G[d][j]%MOD*mob[d]%MOD)%=MOD;
}
}
for(int k=1;k*max(i,j)<MAXN;++k){
(S[i][j][k]+=S[i][j][k-1])%=MOD;
}
}
}
}
inline int blocked(){
int ans=0,lim=m/MAXM;
for(int i=1;i<=lim;++i){
for(int j=1;i*j<=lim;++j){
(ans+=1ll*mob[i]*G[i][n/i/j]%MOD*G[i][m/i/j]%MOD)%=MOD;
}
}
for(int l=lim+1,r;l<=n;l=r+1){
r=min(n/(n/l),m/(m/l));
(ans+=((S[n/l][m/l][r]-S[n/l][m/l][l-1])%MOD+MOD)%MOD)%=MOD;
}
return ans;
}
int main(){
prework();
int t;
scanf("%d",&t);
while(t--){
scanf("%d %d",&n,&m);
if(n>m){
swap(n,m);
}
printf("%d\n",blocked());
}
return 0;
}
杜教筛
杜教筛是一种优化方法。它能够在 $\operatorname{O}(n^{\frac{2}{3}})$ 的时间复杂度内求出积性函数的前缀和,在算法竞赛中不常用。原理是将函数拆成一系列函数的迪丽雷克卷积,然后用数论分块优化。
迪丽雷克卷积
迪丽雷克卷积用 $$ 号表示,假如有数论函数 $f1$ 和 $f2$,则 $f=f1f2$ 有 $f(n)=\sum_{d\mid n}f1(d)\times f2(\frac{n}{d})$。
证明
要求积性函数 $f$ 的前缀和,定义前缀和函数 $S(n)=\sum_{i=1}^{n}f(i)$。再随便找一个积性函数 $g$,定义卷积 $h=f*g$。
$$\sum_{i=1}^{n}h(i)$$
$$\sum_{i=1}^{n}\sum_{d\mid i}f(d)\times g(\frac{i}{d})$$
$$\sum_{i=1}{n}g(i)\times\sum_{d=1}\rfloor}f(i)$$
$$\sum_{i=1}^{n}g(i)\times S(\lfloor\frac{n}{i}\rfloor)$$
接下来,考虑 $g(1)=1$ 之后求。
$$\sum_{i=1}^{n}g(i)\times S(\lfloor\frac{n}{i}\rfloor)-\sum_{i=2}^{n}g(i)\times S(\lfloor\frac{n}{i}\rfloor)$$
$$g(1)=\sum_{i=1}{n}h(i)-\sum_{i=2}g(i)\times S(\lfloor\frac{n}{i}\rfloor)$$
之后,可以用数论分块加递归。时间复杂度 $\operatorname{O}(n^{\frac{3}{4}})$。但是,可以用线性筛先求出 $n^{\frac{2}{3}}$ 的函数前缀和,然后再使用杜教筛,这样复杂度会最小化。可以用记忆花搜索来避免不必要的搜索。
组合计数
卡特兰数
卡特兰数是指从 $n$ 个元素中选 $m$ 个数的方案,具体可以用证明法。
- 设 $A_{n}^{m}$ 为从 $n$ 个里面选取 $m$ 个数,包含顺序不同的情况,为 $\frac{!n}{!(n-m)}$
- 那么不包含顺序的 $C_{n}^{m}$ 为 $\frac{A_nm}{A_m1}$,是 $\frac{!n}{!(n-m)\times !m}$。
- 下面两项再取模的时候可以乘以逆元。
容斥原理
容斥是指去除重复的情况。例如要求 $|S1\cap S2|$,则可以用 $|S1|+|S2|-|S1\cup S2|$。这个就是容斥原理。
例题
这一道题目考虑正难则反。首先令 $ans=\sum c_i\times d_i$,减去不合法的情况。
首先,我们把它当成一个完全背包,处理出来一个 dp 数组存储方案数,转移方程为 $dp_i\to dp_i+dp_{i-v_i}$。
考虑减去不合法的情况为更多的硬币,所以就是 $ans\to ans-dp_{V-d_i-1}\times c_i$。
#include<bits/stdc++.h>
#define MAXN 100001
using namespace std;
typedef long long ll;
ll dp[MAXN];
int main(){
int c[5],test;
scanf("%lld %lld %lld %lld %lld",&c[1],&c[2],&c[3],&c[4],&test);
dp[0]=1;
for(int i=1;i<=4;++i){
for(int j=c[i];j<MAXN;++j){
dp[j]+=dp[j-c[i]];
}
}
while(test--){
int d[5],sum,ans=0;
scanf("%lld %lld %lld %lld %lld",&d[1],&d[2],&d[3],&d[4],&sum);
for(int i=0;i<=15;++i){
int cnt=sum,val=1;
for(int j=1;j<=4;++j){
if((i>>(j-1))&1){
cnt-=c[j]*(d[j]+1);
val*=-1;
}
}
if(cnt<0){
continue;
}
ans+=val*dp[cnt];
}
printf("%lld\n",ans);
}
return 0;
}