基础数论知识|青训营笔记

111 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的第15天

作为一名前端人员,不仅要掌握庞大的前端知识体系,有时候在性能优化的时候掌握一些技巧,比如数论的知识,可以有效帮助开发者增强程序的性能与效率。

容斥原理

博客

概念

vsYttx.png

要把1-n中2、3、5的倍数全部筛掉,那么先筛2的倍数,再筛3的倍数,再筛5的倍数,这样的话6、10、15的倍数就被筛了两遍,30的倍数就被筛了三遍。因此,只要减去6、10、15的倍数,但是这样一减,30的倍数又被减了3遍,也就是说,30的倍数没有被加过,所以再加上就可以了。

容斥原理可以用来求1-n中的所有质数。

实现

先分解质因子:

for (int i = 2i * i <= k; i++) {
       if (k % i == 0) {
           p[++tail] = i;  //p就是储存质因子的数组
           while (k % i == 0) k /= i;  //把k中所有i的质因子全部除去
      }
  }
   if (k > 1p[++tail] = k;  //最后如果大于一,则最后一个数一定是质因子,这一步可能有一点难理解,可以多想想

再实现容斥:

long long fun(long long x){
   long long res=0;  //记录1-x中与k不互质的数量
   for(int i=1;i<(1<<tail);i++){  //这里的1<<tail是指2的tail次方,表示tail个质因子有多少种组合情况 
       long long cur=1,cnt=0;  //cur表示在当前选中的质因子中的乘积,cnt表示当前选中的数量是奇数还是偶数 
       for(int j=0;j<tail;j++){  //这个循环是枚举tail的二进制形式 
           if((i>>j)&1){  //这个是判断i的第j位是不是1,如果是则表示选中第j个数 
               cnt++;  //表示选中了几个数,每选中一个就加一 
               cur*=p[j+1];  //选中第j个数就用cur乘以第j个质因子数,注意质因子数组是从1开始的,所以要加一 
          }
      }
       if(cnt&1) res+=x/cur;  //如果cnt是偶数就相加 
       else res-=x/cur;  //奇数就相减 
  }
   return x-res;  //res储存的是1-x中与K不互质的数量,所以要用x-res得到互质的数量 
}
​
​

康托展开

定义 :给定自然数排列,求出由小到大中所有排列的该排列序号

以下是O(n^2)做法,可用树状数组存tmp,将时间复杂度降低到O(nlogn)

void jiecheng(int x)
{
   f[0]= f[1] =1;
   for(int i = 2; i <= n; i++) f[i] = f[i - 1] * i;
}
int kangtuo()
{
   int ans =1;
   int len = str.length();
   
   for(int i=0;i<len;i++)
  {
       int tmp =0;
       for(int j=i+1;j<len;j++)
      {
           if(str[i]>str[j]) tmp++;
      }
       ans+=tmp*f[len-i+1];
  }
   return ans;
}

逆展开

定义 :给定一个序号,求出对应的自然数排列

void jie_cheng(int n)
{
   f[0] = f[1] = 1; // 0的阶乘为1
   for(int i = 2; i <= n; i++) f[i] = f[i - 1] * i;
}
​
vector<char> vec; //存需要排列的字符
void rev_kangtuo(int k) //输出序号为 k 的字符序列
{
   int n = vec.size(), len = 0;
   string ans = "";
   k--; // 算的时候是按 12345 是第0位
   for(int i = 1; i <= n; i++){
       int t = k / f[n - i]; // 第 i 位需要 第 t + 1 大的数
       k %= f[n - i];        //剩下的几位需要提供的排列数
       ans += vec[t] ; // vec[t] 就是第 t + 1 大的数
       vec.erase(vec.begin() + t)
//用过就删了,不用vector用暴力也可以,就是说枚举,然后一个一个的比较大小,并记录有几个没用过的字符且字典序比它小
  }
   cout << ans << '\n';
}

乘法逆元

定义

ap互质 a * b = 1 (mod p) --> a与b的乘积在模p时恒等于1,则b是a在模p情况下的逆元

可以根据乘法逆元,将 (a / b) % c 转化为 (a * x) % c ,将除法转化为乘法。

解法

费马小定理

假如p是质数,且gcd(a,p)=1,那么 a^ {p-1}≡1(mod p) 。

根据费马小定理,a 的逆元即为 a^{p-2}

需要结合快速幂
ll fpm(ll x, ll power, ll mod) {
   x %= mod;
   ll ans = 1;
   for (; power; power >>= 1, (x *= x) %= mod)
   if(power & 1) (ans *= x) %= mod;
   return ans;
}
int main() {
ll x = fpm(a, p - 2, p); //x为a在mod p意义下的逆元
}
扩展欧几里得定理

必存在 x y ,满足等式 ax + by = gcd(a,b) 因此,若gcd(a,b)=1,则可以求乘法逆元

可递归得出结论:x = y' ,y = x'- a/b * y'

void exgcd(ll a, ll b, ll &x, ll &y)
{
   if (!b) //递归到特解开始回溯
  {
       x = 1;
       y = 0;
       return;
  }
   exgcd(b, a % b, x, y); //求得x2,y2
   ll temp = x;
   x = y;                  //回溯,利用x2递推x1
   y = temp - (a / b) * y; //回溯,利用y2递推y1
}
int main()
{
   ll a, b, x, y;
   cin >> a >> b;
   exgcd(a, b, x, y);
   cout << x << ' ' << y << endl;
   return 0;
}
递推公式

从一到n的逆元:

这个式子必须从1开始递推

image.png