数学基础和技巧学习指南
前置芝士
众数
对数
\(log_1(x)\)是没有意义的,会报错。
生成[l,r]的log2数组
以 2 为底的对数实际上是在求一个数在二进制下的最高有效位的位置,这个位置从 0 开始计数,也就是说,如果一个数有 n 位二进制表示,那么它的最高有效位的位置就是 n-1。
for(int i=1;i<=n;i++)
lg[i]=lg[i-1]+(1<<(lg[i-1]+1)==i)
#此时 lg[i] 就等于它的二进制表示中最高有效位的位置
# i&(i-1) 表示将 i 的最低有效位的 1 置为 0
lg=[0]*11
lg[0]=-1
for i in range(1,11):
lg[i]=lg[i-1]+(i&(i-1)==0)
正整数
[公式:向上取整]
求x的位数
//1
(int)log10(num)+1 //[log10(x)+1]取整
//2
int f(long long x){
int res=0;
while(x){
res++;
x/=10;
}
return res;
}
求x的各个位的和
int s(int x){
int sum=0;
while(x){
sum+=x%10;
x/=10;
}
return sum;
}
求1→n中的m的倍数的和
cnt=3+6+9+....+n//3*3
(1+2+3+....+n//3)*3
def s(m:int)->int:
return (1+n//m)*(n//m)//2*m
完全平方数
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。
等差数列
尼科彻斯定理
连续的奇数就是一个公差为2的等差数列
根据题意说的尼科彻斯定理:任何一个整数m的立方都可以写成m个连续奇数之和;
让 \(m*m*m=x*m+m*m-m\),即可算出 x=m*m-m+1。
求和公式
可以用中位数求和,S[l,r]。
等比数列
求和公式
质数
性质
两个连续的自然数一定是互质数。如:4和5、13和14是互质数。
相邻的两个奇数一定是互质数。如:5和7、75和77是互质数。
两个数中的较大一个是质数,这两个数一定是互质数。如:3和19、16和97是互质数。
两个数中的较小一个是质数,而较大数是合数且不是较小数的倍数,这两个数一定是互质数。如:2和15、7和54是互质数。
较大数比较小数的2倍多1或少1,这两个数一定是互质数。如:13和27、13和25是互质数。
两个数分别除以它们的最大公约数,所得的商一定互质。
两个数的最小公倍数分别除以这两个数,所得的商一定互质。
两个互质的数a和b最小不能表示的数就是(a-1)(b-1)-1,也就是说两个互质的数a,b可以表示(a-1)(b-1)之后的所有数字。
中间是偶数的连续三个自然数互质。
因子
阶乘
10!=3628800=3.6*10^6
康托展开
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
康托展开可以求解一个排列的序号,比如:12345 序号为 1 ,12354序号为2,按字典序增加编号递增,依次类推。
康托逆展开可以求解一个序号它对应的排列是什么。
\(X=a_n(n-1)!+a_{n-1}(n-2)!+\cdots+a_1\cdot0!\)
\(a_i\) 的意思是从右往左数第 i 位这个数是这个数前未出现的数,第\(a_i\)大。计算str[i]是第几大的数,或者说在此之后计算有几个比他小的数
单个质数判断
[6倍高效判断素数法]
除了2和3外,其余素数都与6的倍数相邻,这些素数都满足6n±1。
这是个trivial的素数分布特征,因模6的余数中只有1和5与6互素,故从5开始的素数必与6的倍数相邻。
//从5开始,如果能整除6n+-1,肯定是合数
bool isprime(int num){
if(num==2||num==3){return true;}
if(num%6!=1&&num%6!=5){return false;}
int len=(int)sqrt(num);
for(int i=5;i<=len;i+=6){
if(num%i==0||num%(i+2)==0)
return false;
}
return true;
}
区间[1,n]的质数判断
(1)
vector<int> get_primes(int n) {
vector<int> res(n + 1, 1);
res[0] = res[1] = 0;
int len = sqrt(n);
for (int i = 2; i <= len; i++) {
if (res[i]) {
for (int j = i * i; j <= n; j += i) {
res[j] = 0;
}
}
return res;
}
(2)线性筛:O(n)
//return:范围[1,n]的素数个数
const int N=1e8+10;
int pr[N],cnt;
bool vis[N];
int init(int n){
int res=0;
for(int i = 2; i<=n; i++){
if(!vis[i]) pr[cnt++] = i,res++;
for(int j = 0; j <cnt&&pr[j]*i<=n; j++){
vis[pr[j] * i] = true;
if(i % pr[j] == 0) break;//确保每个数被自己的最小质数筛掉
}
}
return res;
}
区间[l,r]的质数判断
const int N=50010;
int l,r,cnt;
int pr[N],ispr[N];
void init(){
ispr[1]=1;
for(int i=2;i<N;i++){
if(!ispr[i]){
pr[++cnt]=i;
}
for(int j=1;j<=cnt&&i*pr[j]<N;j++){
ispr[i*pr[j]]=1;
if(i%pr[j]==0) break;//确保每个数被自己的最小质数筛掉
}
}
}
int ans[1000010],res=0;
void solve(){
cin>>l>>r;
init();
for(int i=1;i<cnt&&pr[i]<=r;i++){
for(ll j=max(2,(l+pr[i]-1)/pr[i]);j*pr[i]<=(ll)r;j++){
ans[j*pr[i]-l]=1;
}
}
if(l<=1) ans[1-l]=1;
for(int i=0;i<=r-l;i++){
if(!ans[i]) res++;
}
cout<<res;
}
求n的因子数
时间复杂度:\(O(\sqrt{n})\)
//获取n的不重复因子数数组
vector<int> findFactors(ll n){
vector<int> res;
for(int i=1;i<=n/i;i++){
if(n%i==0){
if(i==n/i){
res.push_back(i);
}else{
res.push_back(i);
res.push_back(n/i);
}
}
}
return res;
}
求n的质因子及个数
int a[100010],idx;
int b[100010];
void init(int n){
for(int i=2;i<=n/i;i++){
if(n%i==0){
a[idx]=i;
while(n%i==0){
b[idx]+=1;
n/=i;
}
idx+=1;
}
}
if(n>1) a[idx]=n,b[idx++]+=1;
}
求区间[1,n]的因子数
时间复杂度:O(nlogn)
所以可以遍历1~n,将这n个数做为约数,去求他们对应的倍数的值。
const int N=1e6+10;
int a[N];//存放每个数的约数个数
void solve(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n/i;j++){
a[i*j]++;
}
}
}
求阶乘n!中每个质因子出现次数
int prime[1000],cnt=0,rat[1000];
bool p[1000];
void findprime(int n)//欧拉筛来找素数
{
for (int i = 2; i <= n; i++)
{
if (!p[i])
prime[cnt++] = i;
for (int j = 0; j < cnt; j++)
{
if (i*prime[j] > n)break;
p[i*prime[j]] = true;
if (i%prime[j] == 0)break;
}
}
}
int rate(int n, int p)//分解质因数,求得每个质因数在n!中的出现个数
{
int res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
void solve(){
int n, m;
cin>>n>>m;
findprime(n);
for (int i = 0; i < cnt; i++)
rat[i] = rate(n, prime[i]) - rate(m, prime[i]) - rate(n - m, prime[i]);//计算每个质因数的幂的次数
}
求\(a^b\)的所有因子和
[solved]
既然要求因子和,那我们必然要先分解质因数
根据整数的唯一分解定理,整数a进行质因数分解对应的式子唯一,有:
\(a^b\)的质因子分解:
因子和:
【细节】
(x-1)=== k*mod:(x-1)不存在逆元,我们需要进行特判。
ll a, b, f[10010][2], cnt = 0;
const ll mod = 9901;
ll qmi(ll a, ll b) { //快速幂
ll res = 1;
while (b) {
if (b & 1) {
res = res * a % mod;
}
a = a * a % mod;
b >>= 1;
}
return res % mod;
}
ll sum(ll x, ll y) {
ll res = 0;
y *= b;
cout<<x<<" "<<y<<endl;
if (x % mod == 1) {
res = (y + 1) % mod; //当逆元不存在时
} else {
res = (qmi(x % mod, y+1) - 1) % mod * qmi((x - 1) % mod, mod - 2) % mod; //当逆元存在时
}
return res % mod;
}
void solve() {
cin >> a >> b;
if (a == 0) { //特判,0的因数和就是0
cout << 0 << endl;
return;
}
for (int i = 2; i * i <= a; i++) { //分解质因数
if (a % i == 0) {
cnt++;
f[cnt][0] = i; //记录质因数
f[cnt][1] = 1; //记录幂次
a = a / i;
while (a % i == 0) {
f[cnt][1]++;//记录幂次
a = a / i;
}
}
}
if (a > 1) { //可能a仍为因子
cnt++;
f[cnt][0] = a;
f[cnt][1] = 1;
}
ll res = 1;
for (int i = 1; i <= cnt; i++) {
res = res * sum(f[i][0], f[i][1]) % mod;
}
cout << (res+mod)%mod<< endl;
}
细胞分裂
[problem pesection]
博士要培养细菌平均装到 M 个试管中去做实验。M 很大,一般的数据类型装不下它。碰巧的是,M 正好可以分成 m1m2 的形式。
一共有 n 种细菌,对于每个第 i 种细菌,每天都可以分裂成 Si 个细菌。给出 m1 、m2 、n 和 对应的 Si。Si 什么时候可以变成 M 的倍数?
[solved]
质因数分解:任意的大于1 的整数都一定可以分解成质数幂乘积的形式
数学表达式为:\(n = 2^x1^ * 3^x2^ * 5^x3^ * 7 ^x4^ ……\)(x1,x2,x3…会根据 n 的不同而改变)
【样例分析】
2
24 1
30 12
\(M = 2 ^3 * 3 ^1\)
-------- | 第一天 | 第二天 | 第三天 | 第四天 |
---|---|---|---|---|
一号 | \(2^1 * 3^1 * 5^1\) | \(2 ^2 * 3 ^2 * 5 ^2\) | \(2 ^3 * 3 ^3 * 5 ^3\) | \(2 ^4 * 3 ^4 * 5 ^4\) |
二号 | \(2^2 * 3^1\) | \(2^4 * 3^2\) | \(2^6 * 3^3\) | \(2^8 * 3 ^4\) |
一号细菌在第三天的时候可以整除 M
二号细菌在第二天的时候可以整除 M
答案就是 2 了
step1:筛选出来所有的质数
step2:找 M 的质因子,同时遍历待选细菌,如果细菌不能被 M 的质因数整除这个细菌就不会增值成 M 的倍数
step3:遍历待选细菌,选出最佳答案。如果这时候没有待选细菌,就输出 -1
typedef struct Node{
int num; // 质因数
int sum; // 次幂
}node;
int pr[5000];
bool flag[30005];
int num = 0;
// 任意的大于1 的整数都一定可以分解成质数幂乘积的形式
void Init(){ // 筛选质数
for(int i = 2; i < 30000; i++){
if(!flag[i]) pr[num++] = i;
for(int j = 0; j < num; j++){
if(pr[j] * i > 30005) break;
flag[pr[j] * i] = true;
if(i % pr[j] == 0) break;
}
}
}
void solve(){
Init();
ll n;
cin >> n;
ll m1, m2;
cin >> m1 >> m2;
queue<ll>v; //存待选的细菌
ll b;
for(int i = 1; i <= n; i++){
cin >> b;
v.push(b);
}
vector<node>v1; // 存 m1^m2 的质分解的结果
for(int i = 0; m1 != 1; i++){
if(m1 % pr[i] == 0){ //如果 pr[i] 是 m1^m2 的质因子就存起来
node a = {pr[i], 0};
while(m1 % pr[i] == 0){ // 算一下是多少次幂
m1 /= pr[i];
a.sum++;
}
a.sum *= m2; // 别忘了 m2
v1.push_back(a);
int len = v.size();
for(int j = 1; j <= len; j++){ // 顺便判断待选细菌符合不符合
int c = v.front(); //从队列中拿出来
v.pop();
if(c % pr[i] == 0) v.push(c); //符合就再放回去
}
}
}
if(v.empty()) cout << -1 << endl; // 如果已经没有待选的答案,就输出 -1
else{
ll res = 0x3f3f3f3f;
int len = v.size();
for(int i = 1; i <= len; i++){ // 寻找最佳答案
int c = v.front();
ll temp = 0;
for(int j = 0; j < v1.size(); j++){ // 遍历 m1^m2 的质因子
ll sum = 0;
while(c % v1[j].num == 0){
sum++;
c /= v1[j].num;
}
temp = max(temp, (v1[j].sum + sum - 1) / sum);
}
res = min(temp, res);
v.pop();
}
cout << res << endl;
}
}
查看字符串字典序的排名
朴素法
【指定排位为1的字符串】
【c++】
const int N = 17;
ll jc[N+10]{1, 1};//阶乘
string s1; // 起始串
ll rk1; // 起始串排名
ll tak(string &s)
{
ll ans = 1;
int sz = s.size();
for (int i = 0; i < sz; i++)
{
int t = 0;
for (int j = i + 1; j < N; j++)
{
if (s[j] < s[i])//判断后面比当前小的数目,可以判断这个位的可选项
t++;
}
ans += jc[sz - 1 - i] * t;
}
return ans;
}
void init()
{
for (int i = 1; i <= N; i++)
{
jc[i] = jc[i - 1] * i;
}
rk1 = tak(s1);
}
树状数组优化:O(nlogn)
【指定排位为1的字符串】
【c++】
//c++
/*N=1000000 o(nlogn)*/
const int N = 17;
ll jc[N+10]{1, 1};//阶乘
string s1; // 起始串,需要输入
ll rk1; // 起始串排名
ll ft[28];
int lowbit(int x){
return x&-x;
}
int query(int x){
int res=0;
for(int i=x;i>=1;i-=lowbit(i)) res+=ft[i];
return res;
}
void update(int x,int n,ll val){
for(int i=x;i<=n;i+=lowbit(i)) {ft[i]+=val;}
}
ll tak(string &s)//排名
{
ll ans = 1;
int sz = s.size();
for(int i=0;i<sz;i++){update(s[i]-'a'+1,sz,1);}
for (int i = 0; i < sz; i++)
{
ans += jc[sz - 1 - i] * query(s[i]-'a');
update(s[i]-'a'+1,sz,-1);
}
return ans;
}
void init()//初始化
{
for (int i = 1; i <= N; i++)
{
jc[i] = jc[i - 1] * i;
}
rk1 = tak(s1);
}
void solve(){
s1="aejcldbhpiogfqnkr";//起始串
init();
string s2="ncfjboqiealhkrpgd";
ll rk2=tak(s2);
}
查看数字全排列排名
[c++]
//c++
/*N=1000000 o(nlogn),mod=998244353;*/
const int N = 100010;
const int mod=998244353;
ll jc[N+10]{1, 1};//阶乘
ll ft[N+10];//树状树状
int a[N+10],n;//全排列数组
int lowbit(int x){
return x&-x;
}
int query(int x){
int res=0;
for(int i=x;i>=1;i-=lowbit(i)) res+=ft[i];
return res;
}
void update(int x,ll val){
for(int i=x;i<=n;i+=lowbit(i)) {ft[i]+=val;}
}
ll tak()//排名
{
ll ans = 1;
for(int i=0;i<n;i++){update(a[i],1);}
for (int i = 0; i < n; i++)
{
ans = (ans+jc[n - 1 - i] * query(a[i]-1))%mod;
update(a[i],-1);
}
return ans;
}
void init()//初始化
{
for (int i = 1; i <= N; i++)
{
jc[i] = jc[i - 1] * i%mod;
}
}
void solve(){
init();
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
ll rk=tak();
cout<<rk<<endl;
}
计算一元多项式的值
[朴素计算]
double F(double x)
{
double f=0;
for(int i=n;i>=0;i--)
{
double t=1;
for(int j=1;j<=i;j++)
t*=x;
f+=a[i]*t;
}
return f;
}
[秦九韶算法]
int n;//位数(n+1)
double a[15] //依次系数
double F(double x)
{
double sum=0;
for(int i=n;i>=0;i--)
sum=sum*x+a[i];
return sum;
}
摩尔投票
摩尔投票是一种用来解决绝对众数问题的算法。在一个集合中,如果一个元素的出现次数比其他所有元素的出现次数之和还多,那么就称它为这个集合的 绝对众数 。等价地说,绝对众数的出现次数大于总元素数的 一半 。
摩尔投票的过程非常简单,让我们把找到绝对众数的过程想象成一次选举。我们维护一个m,表示当前的候选人,然后维护一个 cnt。对于每一张新的选票,如果它投给了当前的候选人,就把 cnt 加1,否则就把cnt 减1(也许你可以想象成,B的一个狂热支持者去把A的一个支持者揍了一顿,然后两个人都没法投票)。特别地,计票时如果cnt=0 ,我们可以认为目前谁都没有优势,所以新的选票投给谁,谁就成为新的候选人。
O(n) 时间复杂度、 O(1) 空间复杂度的
int m = 0, cnt = 0;
for (int i = 0; i < n; ++i)
{
if (cnt == 0)
m = A[i];
if (m == A[i])
cnt++;
else
cnt--;
}
// 最后需要验证答案是否符合要求
扩展摩尔投票
要选出 N 个候选人,并且要求每个人的得票都超过总票数的 1/(N+1)。
int m[N], cnt[N];
for (auto e : nums) {
int i = find(m, m + N, e) - m;
if (i != N) { // 如果当前票投给了候选人之一
cnt[i]++;
continue;
}
int j = find(cnt, cnt + N, 0) - cnt;
if (j != N) { // 如果当前存在一个位置"虚位以待"
m[j] = e;
cnt[j] = 1;
continue;
}
for (auto &c : cnt)
c--;
}
// 最后需要验证答案是否符合要求