目录
(2)线性筛--->>时间复杂度为O(N)(所以一般情况下都使用这个)
数论的一些模板:
这是我之前写的一些模板:详细的步骤过程在里面,这里只是核心步骤
根据蓝桥杯重点考察的回顾一下重点的数论算法
(1)朴素筛法求素数
核心:
内循环遍历完质因子的倍数,并将其标记为合数
#include<iostream>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i++)
{
if (!st[i])
{
primes[cnt++] = i;
for (int j = i + i; j <= n; j += i) st[j] = 1;//间隔
}
}
}
int main()
{
int n;
cin >> n;
get_primes(n);
cout << cnt;
}
(2)线性筛--->>时间复杂度为O(N)(所以一般情况下都使用这个)
核心:
思路与上面的朴素筛法差不多,都是标记倍数,不过线性筛每次时标记离自己最近那个倍数
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n)//12
{
for (int i = 2; i <= n; i++)
{
if (!st[i])//如果没有被标记过:证明时素数
{
primes[cnt++] = i;//primes[0]=2 primes[1]=3
}
for (int j = 0; primes[j] <= n / i; j++)
{//st[4]=1 st[6]=1 st[9]=1
st[primes[j] * i] = 1;//遍历素数数组,标记素数的i倍为true
if (i % primes[j] == 0) break;//2%2==0 3%3==0 意思时遍历到最后primes数组的最后一个数了 break
}
}
}
int main()
{
int n;
cin >> n;
get_primes(n);
cout << cnt;
return 0;
}
(3)分解数字为质因数
核心:
公理:每个数字都可被拆分为质因数^指数的乘积形式(如下图所示)
#include<iostream>
using namespace std;
void divide(int n)
{
for (int i = 2; i <= n / i; i++)
{
if (n % i == 0)
{//i一定是质数,因为此时2到i-1的质因子已经被除干净了
int s = 0;//计算次数
while (n % i == 0)
{
n /= i;//i为什么是质数的原因
s++;
}
printf("%d %d\n", i, s);
}
}
if (n > 1) printf("%d %d\n", n, 1);//特判:当有一个比较大的质因子时
cout << endl;
}
int main()
{
int n;
cin >> n;
while (n--)
{
int x;
cin >> x;
divide(x);
}
return 0;
}
(4)约数的个数
核心:如下图
#include<iostream>
#include<unordered_map>
using namespace std;
const int mod = 1e9 + 7;
typedef long long ll;
int main()
{
int n;
cin >> n;
ll ans = 1;//因为要连乘,所以初始化为1
unordered_map<int, int> hash;
while (n--)
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i++)
{
while (x % i == 0)
{
x /= i;//分解质因数
hash[i]++;//质因数的指数++(次数即指数)
}
}
if (x > 1) hash[x]++;//保留最后一个质因数
}
for (auto &i : hash) ans = ans * (i.second + 1) % mod;//约数公式
cout << ans;
return 0;
}
(5)约数之和
核心:如下图
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;
while (n--)
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i++)
while (x % i == 0)
{
x /= i;
primes[i] ++;
}
if (x > 1) primes[x] ++;
}
LL res = 1;
// ------------------约数之和公式-------------------------
for (auto p : primes)
{
LL a = p.first, b = p.second;
LL t = 1;
while (b--) t = (t * a + 1) % mod; // while (b -- ) t = (t * a + 1) % mod; 是什么意思?
res = res * t % mod;
}
cout << res << endl;
return 0;
}
(6)欧几里得算法(辗转相除)
核心:
gcd(a,b)=gcd(b,a%b)
(1)当b==0时,0与任意一个数的最大公约数都是它本身
(2)当b!=0时就是gcd(a,b)=gcd(b,a%b)
#include<iostream>
#include<algorithm>
using namespace std;
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
int main()
{
int n;
cin >> n;
while (n--)
{
int a, b;
cin >> a >> b;
cout << gcd(a, b) << endl;
}
return 0;
}
(7)扩展欧几里得算法
作用:求出最大公约数的同时,根据裴蜀定理求出满足ax+by=gcd(a,b)(不一定是最大公约数)
的x0,y0,然后根据公式求出多组满足的解x,y
#include<iostream>
#include<algorithm>
using namespace std;
void exgcd(int a, int b, int& x, int& y)
{
if (!b)
{
x = 1, y = 0;
return;
}
exgcd(b, a % b, y, x);
y -= a / b * x;
return;
}
int main()
{
int n;
cin >> n;
while (n--)
{
int a, b;
cin >> a >> b;
int x, y;
exgcd(a, b, x, y);
printf("%d %d\n", x, y);
}
return 0;
}
复习的内容就到这里了,现在要开始刷题了^ ^
公约数(欧几里得+更相减损)
等差数列
概述:给出一段序列,求出满足该段序列的最短等差数列的长度
初始思路:
(1)既然要求的是最短的等差数列的长度,那么就要要求公差比较大,那么这个序列就短
(2)那么问题来了:公差怎么取???
如果公差取得比较大,那么很有可能就不满足等差序列这个条件了:比如下面这个例子
2 4 8 (如果公差取4,那么2->4,就不满足公差为4,这个性质了->不满足等差数列)
综上所述:我们要求的是一个数列中满足题意的最小的公差即可,
那么现在的问题就转为了:如何求上述的特殊公差?
根据数据范围我们可以直到这个算法不能写O(N^2)
2 ≤ N ≤ 100000(两层循环会爆)
0 ≤ Ai≤ 10^9
算法优化:
优化:首先遍历一遍数组是跑不了了,要做的优化就是求最小的公差那部分
有没有一种可能,可以用另外一种方式求公差,那就是欧几里得算法求公约数
为什么可以这样写???为说明会联想到公约数???
因为在等差数列中,首项为a1,剩下的数均可表示为a1+nd,那么是不是只需要同时减去a[0],也就是首项,那么剩下的数均可表示为nd,那么n不同,d一定相同,那么是不是就完美满足了最大公约数这个性质了^ ^
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N];
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
sort(a, a + n);//排序是为了让首项为a[0]
int d = 0;//0与任何数的最大公约数都是它的本身
for (int i = 1; i < n; i++) d = gcd(d, a[i] - a[0]);//减去首项a1
if (!d) printf("%d\n", n);//如果公约数为0.那么就证明这时一个常数数列
else printf("%d\n", (a[n - 1] - a[0]) / d + 1);//上面图片的公式
return 0;
}
最大比例(等比数列)
图片来源:AcWing视频
(1)对于每一个等比数列,都可以写为首项*公比的形式
那么对于本题而言,要求的是分数形式的最大公比,那么就型需要p/q,来代替r,作为公比
其中p/q,对于其中的p,q而言,这两个数需要两两互质,这样它们才是最简分式
(2)那么为了简介,将上式写为b数列的形式,那么,在b数列中
注意此时先预处理进行排序,那么b0,就是最小的数,将它作为首项
b0后的每一每一项除以b0,就等于(p/q)的alpha次方
(3)对于让(p/q)最简有两个条件必须满足:
<1>,p,q互质(最大公约数为1)
<2>,p,q不能再拆分为次幂形式
比如说4/9,已经满足条件<1>了,但是它可以写为(2/3)^2次方,所以此时仍不是最简
(4)回到题目所求:最大比例,那么其实也就是最大公比
也就是上图所说的K最大即可,但是也要注意满足该数列为等比数列
那么下面:
对于一个相邻的(p/q),(注意因为已经排好序了,那么后面的元素一定是比前面的大的)
下图中:alphai是指对于k的后一个数,那么就有:
alphai=k*s,那么就很明显了,对于alphai而言s,是它和k的公约数
(5)辗转相减法(更相减损数):
/*更相减损术:第一步:任意给定两个正整数;判断它们是否都是偶数。若是,则用2约简;若不是则执行第二步。
第二步:以较大的数减较小的数,接着把所得的差与较小的数比较,并以大数减小数。继续这个操作,直到所得的减数和差相等为止。
则第一步中约掉的若干个2的积与第二步中等数的乘积就是所求的最大公约数。*///更相减损术总用较大的减去较小的
/*例子:
98-63=35
63-35=28
35-28=7
28-7=21
21-7=14
14-7=7
所以,98和63的最大公约数等于7。*///我们这里要用更相减损术的是指数,所以要让(p/q)^x1,(p/q)^x2,...,(p/q)^x(N-1),两两计算,互除,除到结果为1,即x1=x2,此时幂次为0,结果为1
//把分子分母分别去算,结果是相同的因为,分子分母的幂次是相同
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 110;
int n;
LL x[N];//存入输入的数据
LL a[N], b[N];//分别表示分子p和分母q
LL gcd(LL a, LL b)//辗转相除
{
return b ? gcd(b, a % b) : a;
}
LL gcd_sub(LL a, LL b)//辗转相减
{
if (b > a)swap(a, b);
if (b == 1)return a;
return gcd_sub(b, a / b);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)cin >> x[i];//读入
sort(x, x + n);
LL dd = 0;//0与任何数的最大公约数都是其本身
int cnt = 0;
for (int i = 1; i < n; i++)//预处理得到b数组
{
if (x[i] != x[i - 1])//去重
{//对于第一项而言:相当于取出了首项
//对于第一项之后的每一项而言:相当于取出了最大公约数
dd = gcd(x[i], x[0]);//最大公约数
a[cnt] = x[i] / dd;//分子的最简==原来的值/(原来的值与上一项的最大公约数)
b[cnt] = x[0] / dd;//第一项的最简形式作为分母
cnt++;
}
}
LL up = a[0], down = b[0];//up分子 down分母
for (int i = 1; i < cnt; i++)//分开求分子分母的指数最大公约数
{
up = gcd_sub(up, a[i]);//分子
down = gcd_sub(down, b[i]);//分母
}
cout << up << "/" << down;
return 0;
}
线性同余方程+扩展欧几里得
五指山
扩展欧几里得定理:指对于任意正整数对(a,b),一定存在非零整数x和y,使得 ax+by=gcd(a,b)
如果我们求出x和y的一对,我们记为x0和y0 那么其他的x和y可以通过x0,y0表示:
那么其他的x和y可以表示为:x=x0+kb,y=y0-k*a
回到本题:
给出初始位置,目标位置,圆环的长度,一次能走多少步,让我们求最少走多少步可以到达目标位置
标注字母:
将初始位置标记为:x,将目标位置标记为:y
将步长标记为:d,将圆环的长度标记为:a
将步数标记为:b,将走过的环数标记为:n
那么就有这样的方程:x+bd = y+an 整理得:-an+bd = y-x
分析式子:题目要求的是最小的步数,也就是式子中的b
式子中一共有2个未知数,也就是步数:b 和 环数:n
分析到这里,应该马上与裴蜀定理联想起来:
因此我们只需要判断y-x是否为gcd(n,d)的整数倍就能判断是否有解了
若有解我们利用扩展欧几里得定理就可以求得-an+bd = gcd(n,d)中的a和b
根据一组解,从而可以推出多组解,其他解就可以表示出来了
那么,要求的是最小步数,所以只要记录最小的b即可,但是要注意b只能为正数(方向不可相反)
求一下b = b0+k*(n/gcd(n,d))的最小值即可
minb = b0%(n/gcd(n,d));证明过程需要回看 = =
上述来自:
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
typedef long long LL;
LL exgcd(LL a,LL b,LL &x,LL &y)
{
if(!b){
x = 1,y = 0;
return a;
}
int d = exgcd(b,a%b,y,x);
y -= a/b*x;
return d;
}
int main()
{
int T;
scanf("%d",&T);
while(T--){
LL n,d,x,y,a,b;
scanf("%lld%lld%lld%lld",&n,&d,&x,&y);
int gcd = exgcd(n,d,a,b);
if((y-x)%gcd)
{
printf("Impossible\n");
}
else
{
b*=(y-x)/gcd;//公式
n/=gcd;//公式
printf("%lld\n",(b%n+n)%n);//保证为正数
}
}
return 0;
}
C循环
题解来自:
k 位存储系统:
所有数值保留最后k位
注意:都是以二进制进行存储的
(A + xC)mod 2^k = B 循环就结束了
//用拓展欧几里得算法, 形如ax + by = d
A + xC - y2^k = B
xC - y2^k = B - A
形如ax + by = d
当求出一组解x0, y0, 那么所有解都可以用x0, y0表示
此时就与上题是一模一样的了
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
LL exgcd(LL a, LL b, LL &x, LL &y) //拓展欧几里得算法 ax + by = d
{
if (b == 0) //边界情况
{
x = 1, y = 0;
return a;
}
LL d = exgcd(b, a % b, y, x); //y*b + x(a mod b) = d;
y -= a / b * x; //公式推系数(下方图一)
return d;
}
int main()
{
LL a, b, c, k;
while (cin >> a >> b >> c >> k, a || b || c || k) //逗号表达式
{
LL x, y; //系数 公式:x*C - y2^k = B - A
LL z = 1ll << k; //为了方便,公式:x*C - y*z = B - A
LL d = exgcd(c, z, x, y);
if ((b - a) % d) //有没有解取决于B - A是否能整除d
cout << "FOREVER" << endl; //求余不为0,一定无解
else //否则一定有解
{
x *= (b - a) / d; //因为等式右边原本是d,现在是(B - A), 所以等式右边x, y乘上(B-A)/d
z /= d; //根据公式,x = x0 + k*(b/d),这里的b就是z,此时所有的解x和x0的差,都是z的倍数
cout << (x % z + z) % z << endl; //x的最小非负整数解,就是x0 mod z, c++中取模可能是负数
}
}
return 0;
}
筛质数+分解质因数+组合计数
X的因子链
(1)公理:每一个数都可以被分解为多个质数相乘的形式
其中题目要求的是:满足的子序列的最大长度和满足最长的子序列的个数
那么由算术基本定理可知:
最大长度即:质因子出现的次数总和
(2)观察下图的右上角:
一个正整数可以被拆分为如下形式:
那么满足的最长序列长度已经知道了:就是2+3+1(次数之和即可)
那么,现在的另外一个问题是:如何求满足的序列个数,也就是2个2,3个3,1个5,可以组合成多少种方式???
这里就用到了组合数学:
正确的解法是:(次数的总和)!/(分别的次数!) 如下图左上角
线性筛法:
根据以上的综合分析可知:
对于线性筛:我们要开primes数组来存放质数,要开st数组来判断是否为质数,要用cnt来记录质数的个数
对于题目本身而言:我们要记录该数的最小质数,也就是要开min_p数组来存放最小质因子
要用sum数组来记录每个质因子出现的次数:目的是为了使用上述的组合数求个数和长度
#include<cstdio>
typedef long long ll;
const int N = (1 << 20) + 10;
int primes[N];// 存质数
int min_p[N]; //存最小质因子
int cnt;
bool st[N]; // 表示对应元素是否被筛过
int sum[N]; //记录每个质因子出现的次数
// 线性筛法(欧拉筛法)
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) {
min_p[i] = i;
primes[++cnt] = i;
}
for (int j = 1; primes[j] * i <= n; j++) {
int t = primes[j] * i;
st[t] = true; //标记合数
min_p[t] = primes[j];
if (i % primes[j] == 0) {
//如果i是前面某个素数的倍数时, 说明i以后会由某个更大的数乘这个小素数筛去
//同理, 之后的筛数也是没有必要的, 因此在这个时候, 就可以跳出循环了
break;
}
}
}
}
int main() {
get_primes(N);
int x;
while (scanf("%d", &x) != EOF) {
//tol用于记录最大长度,k表示第i个质因子的下标, 从0开始
int k = 0, tol = 0;
// 依次处理各个质因子, 求出对应质因子出现的次数
while (x > 1) {
int p = min_p[x]; // 通过while, 依次取出最小质因子
sum[k] = 0;//初始化为0
//处理当前质因子, 求其出现的次数
while (x % p == 0)
{
sum[k]++;//质数的次数++
tol++;//质数出现1次,长度+1
x /= p;//除以该质数,相当于筛掉
}
k++; // 准备处理下一个质因子
/*
例:
x=12 --> 3*2^2
p = 2
sum[1]=1
x=6
sum[1]=2
x=3
****************
p=3
sum[2]=1
x=1 --> 结束
*/
}
//求所有质因子出现总次数的全排列
ll res = 1;
for (int i = 2; i <= tol; i++) {
res *= i; //分子
}
//去除各个质因子重复出现的次数
for (int i = 0; i < k; i++) {
for (int j = 1; j <= sum[i]; j++) {
res /= j;//分母
}
}
printf("%d %lld\n", tol, res); //输出最长序列的长度, 以及满足最大长度的序列的个数
}
return 0;
}
作者:Bug-Free
链接:https://www.acwing.com/solution/content/23671/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
聪明的燕姿
(1)算术基本定理
那么题目要求的就是有多少个数字的约数之和满足给定的数字,并按顺序输出这些数字
即套用画线公式
由数据分析知道:满足该式子的数会很少,所以可以用暴力搜索
(2)首先想出框架:
<1>外层for循环,枚举质数
<2>内层for循环,枚举该质数的从0到alpha的次幂之和
判断条件:如果这个和可以被S(也就是给定的数字所整除),那么就向下搜索
搜索边界:如果S==1时就停止搜索(因为S==1)即这条路径上的所有节点都能整除S,
证明这条路径的组合,满足了上述的公式,此时return
因为是满足条件才向下搜索,所以不用担心其他的情况导致不能返回
(3)剪枝优化:
<1>当枚举的质数的平方数已经大于了S,那么可以提前返回,所以第一层循环才可以这么写
因为这里可以认为成前面的数的 最高次幂*该质数的乘积 类似与 目前遍历到的质数的平方数
<2>if判断也是如此,如果S=(1+pi)这种情况
那么我们需要同时判断S-1是否大于上一个质数 且 S-1是否质数
因为我们要保证质数是从小到大枚举的,只有剩下的S-1是大于上一层的质数的时候,(1+S)才有可能成为最初的那个S的一个因子
因为我们要表示S’ = (1+ 2 + 2^2+…)(1 + 3 + 3^2 + …)…(1+S)
越往后枚举对应的那个Pi也越大,所以S-1一定要大于上一层的质数才能满足条件
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 50000;//N = sqrt(2e9)
bool st[N];
int primes[N],cnt = 0;//线性筛
int S,res[N],len;
void get_primes(int n)//线性筛
{
for(int i = 2;i <= n;i++)
{
if(!st[i]) primes[cnt++] = i;
for(int j = 0;primes[j]*i <= n;j++)
{
st[primes[j]*i] = true;
if(i % primes[j] == 0) break;
}
}
}
int is_prime(int n)//判断是否为质数
{
if(n < N) return !st[n];//没有被筛过说明就是质数,返回true
for(int i = 0;primes[i] <= n / primes[i];i++)
{
if(n % primes[i] == 0) return false;
}
return true;
}
//last参数:表示上一个枚举的质数是谁
//product参数:当前进行到哪一个括号里面的最高次项Pi^ai的乘积和
//S参数:代表着从一开始的S除以(1+pk+pk^2+…+pk^ak)后剩余的乘积
void dfs(int last,int product,int S)//last表示上一个用的质数的下标是什么,product当前最高次项的结果,S表示每次处理后剩余多少
{
if(S == 1)//当S和j两个数相等时
{
res[len++] = product;
return ;//记录返回
}
//比如20 = 2^2 * 5
//N = P1^a1 * P2^a2 * ... * Pn^an
//S = (1+p1+p1^2+...+p1^a1)(1+p2+p2^2+...+p2^a2)...(1+pn+pn^2+...+pn^an)
//42 = (1 + 2 + 2^2)*(1 + 5),其中2^2和5就分别是最高次项p1^2*p2^1
if(S-1 > ((last < 0) ? 0 : primes[last]) && is_prime(S-1))//剪枝
{
/*因为我们要保证质数是从小到大枚举的,只有剩下的S-1是大于上一层的质数的时候,
(1+S)才有可能成为最初的那个S的一个因子
*/
res[len++] = product * (S-1);
}
for(int i = last+1;primes[i] <= S / primes[i];i++)
{
int p = primes[i];
for(int j = 1+p,t = p;j <= S;t *= p,j += t)//60行对应的公式
{
if(S % j == 0)/*即S % (1+pk+pk^2+…+pk^ak) == 0然后S /= (1+pk+pk^2+…+pk^ak),再dfs到下一层*/
{
dfs(i,product*t,S/j);//第三个参数(每次都要除以)
}
}
}
}
int main()
{
get_primes(N-1);
while(cin>>S)
{
len = 0;
dfs(-1,1,S);
//sort(res,res+len);//不需要排序
cout << len << endl;
if (len)
{
sort(res, res + len);//按顺序排序输出
for (int i = 0; i < len; i ++ ) cout << res[i] << ' ';
cout << endl;
}
}
return 0;
}
正则问题
如果不看视频:真滴难想
第一感觉意味是:栈(逆波兰表达式求值)
看下图和代码注释把^ ^
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int k;
string str;
int dfs()
{
int res = 0;
while (k < str.size())
{
if (str[k] == '(') // 处理 (......)
{
k ++ ; // 跳过 '('
res += dfs();//答案+=左括号到其他(‘|’ 或 ‘)’ 或 ‘(’)之前x的数量
k ++ ; // 跳过 ')'
}
else if (str[k] == '|')
{
k ++ ; // 跳过 '|'
res = max(res, dfs());//左边和右边取最大值
}
else if (str[k] == ')') break;
else
{
k ++ ; // 跳过 'x'
res ++ ;//计算x的个数
}
}
return res;
}
int main()
{
cin >> str;
cout << dfs() << endl;
return 0;
}