简单数学
PAT (Basic level) 1003 我要通过!
- 这道题重点还是理解题意。题中给出了 3 个条件。第一个条件没什么好说的,重点看后两个:
- 条件2: P A T PAT PAT、 A P A T A APATA APATA、 A A P A T A A AAPATAA AAPATAA、 A A A P A T A A A AAAPATAAA AAAPATAAA、 …、 x P A T x xPATx xPATx 都输出 “YES”, 这里 x x x 为空或是任意数量的 A A A
- 条件3: 假设有一个字符串的格式是 a P b T c aPbTc aPbTc, 这里 a 、 b 、 c a、b、c a、b、c 可以为空或是任意数量的 A A A. 如果这个字符串已知是 YES, 那么在 b b b 与 T T T 之间添加一个 A A A、 c c c 后面添加字符串 a a a 之后形成的新字符串 a P b A T c a aPbATca aPbATca 也是 YES
- 例如 P A T PAT PAT 是 YES, 那么 P A A T PAAT PAAT 也是YES; P A A T PAAT PAAT 是 YES, 那么 P A A A T PAAAT PAAAT 也是 YES; A P A T A APATA APATA 是 YES, 那么 A P A A T A A APAATAA APAATAA 也是 YES
- 注意到而由条件3得到的所有字符串一开始一定是条件2中的字符串变形而来。因此可以这么解决:
- 记录 P P P 左边 A A A 的个数为 x x x、 P P P 和 T T T 中间 A A A 的个数为 y y y、 T T T 右边 A A A 的个数为 z z z。每使用条件3 一次, x x x 不变、 y y y 变为 y + 1 y+1 y+1、 z z z 变为 z + x z+x z+x。反过来, 如果沿着箭头逆回去, 每逆一次, x x x 不变、 y y y 变为 y − 1 y-1 y−1 、 x x x 变为 z − x z-x z−x。这样可以一直逆回去, 直到 y = 1 y=1 y=1 时 判断 x x x 和 z z z 是否相等: 如果 x = z x=z x=z。则输出 “YES”: 否则, 输出 “NO”
- 更进一步,由于 y y y 每次回退的步骤中都减1, 因此从初始字符串回退到 P T PT PT 之间只剩一个 A A A, 需要进行 y − 1 y-1 y−1 次回退。而 T T T 右边 A A A 的个数 z z z 在经过 y − 1 y-1 y−1 次回退后将变为 z − x ∗ ( y − 1 ) z-x*(y-1) z−x∗(y−1)。这时, 判断条件2成立的条件是 z − x ∗ ( y − 1 ) z-x*(y-1) z−x∗(y−1) 是否等于 P P P 左边 A A A 的个数 x x x:如果 z − x ∗ ( y − 1 ) = x z-x*(y-1)=x z−x∗(y−1)=x, 则输出"YES"; 否则, 输出"NO"
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int N;
int main(int argc, const char* argv[])
{
cin >> N;
while (N--)
{
string s;
bool flag = true;
cin >> s;
size_t p_pos = -1, t_pos = -1;
for (size_t i = 0; i != s.size(); ++i)
{
if (s[i] != 'P' && s[i] != 'A' && s[i] != 'T')
{
flag = false;
break;
}
else if (s[i] == 'P')
{
if (p_pos != -1)
{
flag = false;
break;
}
else {
p_pos = i;
}
}
else if (s[i] == 'T')
{
if (t_pos != -1)
{
flag = false;
break;
}
else {
t_pos = i;
}
}
}
if (flag)
{
if (p_pos == -1 || t_pos == -1 || t_pos <= p_pos + 1)
{
flag = false;
}
else {
size_t x = p_pos, y = t_pos - p_pos - 1, z = s.size() - t_pos - 1;
flag = (z - x * (y - 1) == x) ? true : false;
}
}
if (flag)
{
printf("YES\n");
}
else {
printf("NO\n");
}
}
return 0;
}
PAT (Advanced level) 1049 Counting Ones
找数学规律
- 先举一个例子来说明本题的做法:令 n = 30710 n=30710 n=30710, 并记从最低位 0 0 0 到最高位 3 3 3 的位号分别为 1 号位、2 号位、3 号位、4 号位及 5 号位。
- (1) 考虑 1 号位在 1 ∼ n 1\sim n 1∼n 过程中在该位可能出现的 1 的个数.
- 注意到 n n n 在 1 号位的左侧是 3071 3071 3071
- 由于 1 号位为 0 (小于 1), 因此在 1 ∼ n 1\sim n 1∼n 中, 仅在高四位为 0000 ∼ 3070 0000\sim 3070 0000∼3070 的过程中,1 号位才可取到 1, 即 00001 、 00011 、 00021 、 … 、 30691 、 30701 00001、00011、00021、… 、30691、30701 00001、00011、00021、…、30691、30701, 总共有 3071 3071 3071 种情况
- 因此在 1 ∼ n 1\sim n 1∼n 过程中,1 号位将出现 3071 个 1 (即 3071 × 1 0 0 3071\times10^0 3071×100).
- (2) 考虑 2 号位在 1 ∼ n 1\sim n 1∼n 过程中在该位可能出现的 1 的个数
- 注意到 n n n 在 2 号位左侧是 307, 在 2 号位右侧是 0
- 由于 2 号位为 1, 因此在 1 ∼ n 1\sim n 1∼n 中,仅在高三位为 000 ∼ 306 000\sim 306 000∼306 的过程中,对任意的低一位(即 0 ∼ 9 0\sim 9 0∼9), 2 号位才总可以取到 1, 总共有 3070 = 307 × 1 0 1 3070=307\times 10^1 3070=307×101 种情况
- 上面高三位最大只考虑到 306 306 306, 接下来考虑高三位为 307 307 307 的情况,当然此时还得保持 2 号位为 1, 那么也就是计算 30710 ∼ n 30710 \sim n 30710∼n 有多少 2 号位为 1 的数。由于 n n n 就是 30710 30710 30710, 因此这里只有一个数
- 因此在 1 ∼ n 1\sim n 1∼n 过程中,2 号位将出现 3071 = 307 × 1 0 1 + 1 3071=307 \times10^1+1 3071=307×101+1 个 1
- (3) 考虑 3 号位在 1 ∼ n 1\sim n 1∼n 过程中在该位可能出现的 1 的个数
- 注意到 n n n 在 3 号位左侧是 30 30 30, 在 3 号位右侧是 10 10 10
- 由于 3 号位为 7 (大于1),因此在 1 ∼ n 1\sim n 1∼n 中,在高二位为 00 ∼ 30 00\sim30 00∼30 的过程中,对任意的低二位(即 00 ∼ 99 00\sim99 00∼99), 3 号位均可取到 1, 总共有 3100 = ( 30 + 1 ) × 1 0 2 3100=(30+1)\times10^2 3100=(30+1)×102 种情况
- 因此在 1 ∼ n 1\sim n 1∼n 过程中,3 号位将出现 3100 个 1
- (4) 同理,4、5 号位上出现 1 的情况数分别为 3000 3000 3000 和 10000 10000 10000
- 综上所述, 1 ∼ n 1\sim n 1∼n 中共会出现 22242 = 3071 + 3071 + 3100 + 3000 + 10000 22242 = 3071 + 3071 + 3100 + 3000 + 10000 22242=3071+3071+3100+3000+10000 个 1
- (1) 考虑 1 号位在 1 ∼ n 1\sim n 1∼n 过程中在该位可能出现的 1 的个数.
- 总结的规则如下:
- 步骤 1: 以
ans
表示 1 的个数,初值为 0。设需要计算的数为n
, 且是一个m
位(十进制)的数。从低到高枚举n
的每一位 (控制一个a
, 每次乘 10 表示进一位),对每一位计算1~n
中该位为 1 的数的个数 - 步骤 2: 设当前处理至第
k
位, 那么记left
为第k
位的高位所表示的数,now
为第k
位的数,right
为第k
位的低位所表示的数, 然后根据当前第k
位 (now
) 的情况分为三类讨论:- 若
now == 0
, 则ans += left * a
- 若
now == 1
, 则ans += left * a + right + 1
- 若
now >= 2
, 则ans += (left + 1) * a
- 若
- 步骤 1: 以
#include <cstdio>
using namespace std;
int main(int argc, const char* argv[])
{
long long n = 0, ans = 0, a = 1;
scanf("%lld", &n);
while (n / a > 0)
{
long long left = n / (a * 10);
long long right = n % a;
long long now = n / a % 10;
if (now == 0)
{
ans += left * a;
}
else if (now == 1)
{
ans += left * a + right + 1;
}
else {
ans += (left + 1) * a;
}
a *= 10;
}
printf("%lld", ans);
return 0;
}
递归
- 自己做的时候并没有想到上面的方法,而是改用递归解题,过程还挺绕的,完全写对花了不少时间…orz
- 时间复杂度不知道具体是多少,但应该在 O ( n l o g n ) O(nlogn) O(nlogn) 量级以内
#include <cstdio>
#include <cmath>
using namespace std;
long long N;
long long Cnt = 0;
// func 用于统计 1 ~ n-1 中 1 的个数
long long func(long long n)
{
if (n <= 10) // n - 1 为个位数,直接求解
{
return (n > 1) ? 1 : 0;
}
else {
int bit_len = 1;
long long pow_10 = 10;
for (; n / pow_10 != 0; ++bit_len, pow_10 *= 10)
{
}
pow_10 /= 10; // pow_10 为 n 最高位的权值;例如 1234 对应的 pow_10 为 1000,234 对应的 pow_10 为 100
int high_bit = n / pow_10; // n 的最高位数字
if (n == pow_10)
{
// n 为 10 的整数次幂
// 例如 0 ~ 999 中 1 出现的次数等于 0 ~ 99 中 1 出现的次数乘 10 加上 100 (对应百位数为 1 的情况)
// 即,f(1000) = f(100) * 10 + 1000 / 10
return func(pow_10 / 10) * 10 + (pow_10 / 10);
}
if (n % pow_10 == 0)
{
// n 为 pow_10 的整数倍
// 例如 f(300) = f(100) * 3 + 100
return func(pow_10) * high_bit + ((high_bit > 1) ? pow_10 : 0);
}
// 一般情况
// 例如 f(1235) = f(1000) + ( f(235) + 235 ) (f(1000) 对应 0 ~ 999, 后面一部分对应 1000 ~ 1234)
return func(n % pow_10) + func(high_bit * pow_10) + ((high_bit == 1) ? (n - pow_10) : 0);
}
}
int main(int argc, const char* argv[])
{
scanf("%lld", &N);
printf("%lld", func(N + 1));
return 0;
}
PAT (Advanced level) 1069 The Black Hole of Numbers
- 这题我是用的
string
。特别需要注意的是,输入的时候不一定是输入四位数字,因此需要先补 0 使它称为四位数字。我在这个地方卡了好久,误以为输入的时候一定是四位数字 - 补 0 的时候可以用
insert
方法 - 还要注意直接输入 6174 也要进行转换运算而不能直接输出 6174,因此要使用
do
-while
结构
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
string N;
int main(int argc, const char* argv[])
{
cin >> N;
N.insert(0, 4 - N.size(), '0');
int res;
do
{
int L, R;
sort(N.rbegin(), N.rend());
L = stoi(N);
sort(N.begin(), N.end());
R = stoi(N);
res = L - R;
printf("%04d - %04d = %04d\n", L, R, res);
N = to_string(res);
N.insert(0, 4 - N.size(), '0');
} while (res != 6174 && res != 0);
return 0;
}
PAT (Advanced level) 1104 Sum of Number Segments
- 这里解题思路就是推出来每个数字出现在连续数段中的次数: 位置在 i i i ( i i i 从 0 开始) 的数字出现的次数为 ( 1 + i ) ( N − i ) (1+i)(N-i) (1+i)(N−i);推出这个之后只需要 O ( n ) O(n) O(n) 即可解题
- 我是在测试点2卡住了,原因是浮点型精度往往是有误差的,如果对浮点型精度进行大数据量的运算, 可能会导致误差累积,影响最终结果。而测试点2就是大数据量的运算
- 解决方法是将输入的小数乘 1000,用
long long
型变量存储, 最后除以 1000 输出即可 - p.s. 希望再也不要碰到这种坑!!!
- 解决方法是将输入的小数乘 1000,用
#include <cstdio>
using namespace std;
int N;
long long Sum = 0;
int main(int argc, const char* argv[])
{
scanf("%d", &N);
double tmp = 0;
for (size_t i = 0; i != N; ++i)
{
scanf("%lf", &tmp);
Sum += (long long)(tmp * 1000) * (i + 1) * (N - i);
}
printf("%.2f\n", Sum / 1000.0);
return 0;
}
最大公约数与最小公倍数
最大公约数
- 求解最大公约数常用欧几里得算法(即辗转相除法)
- 欧几里得算法基于下面这个定理:
- 设 a 、 b a、b a、b 均为正整数, 则 g c d ( a , b ) = g c d ( b , a % b ) gcd(a, b) = gcd(b, a\ \%\ b) gcd(a,b)=gcd(b,a % b)
g c d ( a , b ) gcd(a, b) gcd(a,b) 表示 a a a 和 b b b 的最大公约数
易证 a a a 和 b b b 的公约数一定是 b b b 和 a % b a\ \%\ b a % b 的公约数, b b b 和 a % b a\ \%\ b a % b 的公约数也一定是 a a a 和 b b b 的公约数;因此可以得到以上结论
- 由上面这个定理可以发现, 如果 a < b a<b a<b, 那么定理的结果就是将 a a a 和 b b b 交换;如果 a > b a>b a>b, 那么通过这个定理总可以将数据规模变小,并且减小得非常快。这样似乎可以很快得到结果,只是还需要一个东西:递归边界,众所周知: 0 和任意一个整数 a a a 的最大公约数都是 a a a , 这个结论就可以当作递归边界
int gcd(int a, int b) {
if(b == 0)
return a;
else
return gcd(b, a % b);
}
最小公倍数
- l c m ( a , b ) = a b / g c d ( a , b ) lcm(a, b)=ab/gcd(a,b) lcm(a,b)=ab/gcd(a,b)
- a b ab ab 在实际计算时有可能溢出, 因此更恰当的写法是 a / g c d ( a , b ) ∗ b a/ gcd(a,b)*b a/gcd(a,b)∗b
l c m ( a , b ) lcm(a, b) lcm(a,b) 来表示 a a a 和 b b b 的最小公倍数
PAT (Basic level) 1008 数组元素循环右移问题
- 先说一下注意点:右移位数可能大于数组元素个数,因此要先进行取模操作
- 如果只是为了解题的话,根本不用管最小移动次数,直接先输出后 m m m 个数据,最后输出前 n − m n-m n−m 个数据就行了
- 下面主要讲解如何让移动的次数最少:
- 将序列中一个元素先拿出至临时变量, 然后把将要移动到空出位置的元素移至该空出位置, 再把新空出的位置用将要移动到这个新空出的位置的元素代替, 以此类推, 直到所有元素移动完毕
- 如下图所示,先将 5 放进临时变量
temp
, 由于M
值为 2, 因此最终一定是 3 在原先 5 的位置上,于是把 3 移动到 5 的位置;同样的, 3 的位置在最终态时一定是 1, 于是把 1 移动到 3 的位置;接下来, 由于 1 左边第二个位置是一开始 5 的位置,因此不再继续,把temp
的值 5 放在 1 的位置,步骤1 结束。而步骤 2 同理, 最终得到需要的结果。这里只从 5 跟 6 开始是因为右移两位后 5 和 6 会移到序列最前方,因此对题目给定的N
和M
, 可以从N - M
号位开始枚举起始位,直到N - 1
号位结束(这里的位号都指从 0 开始的位号)
- 不过这样会产生一个问题:有可能经过一轮循环之后,
N - M
号位之后又多个位置已经得到了最终结果,这样当继续枚举起始位时就会产生重复,最后导致错误。例如假设N = 8、M = 3
, 这种情况下只需要枚举一次起始位,即从8 - 3 = 5
号位开始循环, 一轮循环下来就可以得到整个序列的最终结果, 而不需要再去从 6 号位跟 7 号位开始循环 - 为了解决这个问题,设
d
为N
和M
的最大公约数, 那么从N - M
号位开始枚举起始位,直到N - M + d - 1
位结束 (枚举一个最大公约数的范围)- 例如
N = 8
、M = 3
时,就只需要从N - M = 5
号位开始、直到N - M + d - 1 = 5
号位结束, 也就是只需要枚举5
号位作为起始位即可
- 例如
#include <cstdio>
int gcd(int a, int b) {
// 求 a 和 b 的最大公约数
if (b == 0) return a;
else return gcd(b, a % b);
}
int main() {
int a[110];
// temp 为临时变量,pos 存放当前处理的位置, next 为下一个要处理的位置
int n, m, temp, pos, next;
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
m = m % n; // 修正 m
if (m != 0) {
int d = gcd(m, n);
for (int i = n - m; i < n - m + d; i++) {
// 枚举一个最大公约数的范围
temp = a[i]; // 把当前位置元素先拿走
pos = i; // 记录当前要处理的位置
do {
// 计算下一个要处理的位置
next = (pos - m + n) % n;
// 如果下一个位置不是初始点
// 则把下一个位置的元素赋值给当前处理位置
if (next != i) a[pos] = a[next];
else a[pos] = temp; // 把一开始拿走的元素赋值给最后这个空位
pos = next; // 传递位置
} while (pos != i); // 循环直到当前处理位置回到初始位置结束
}
for (int i = 0; i < n; i++) {
// 输出数组
printf("%d", a[i]);
if (i < n - 1) printf(" ");
}
}
return 0;
};
分数的四则运算
分数的表示和化简
分数的表示
struct Fraction (
int up, down; // 分子、分母
};
- 对这种表示制订三项规则:
- 使
down
为非负数。如果分数为负, 那么令分子up
为负即可 - 如果该分数恰为 0, 那么规定其分子为 0, 分母为1 (这点很重要,否则后面的分数运算结果会发生错误)
- 分子和分母没有除了 1 以外的公约数
- 使
分数的化简
- 如果分母
down
为负数, 那么令分子up
和分母down
都变为相反数 - 如果分子
up
为 0, 那么令分母down
为 1 - 约分: 求出分子绝对值与分母绝对值的最大公约数
d
, 然后令分子分母同时除以d
Fraction reduction(Fraction result) {
if (result.down < 0) {
// 分母为负数, 令分子和分母都变为相反数
result.up = -result.up;
result.down = - result.down;
}
if (result.up == 0) {
// 这步其实可以不特殊处理,并到下一个 else 里一起处理效果是一样的
result.down= 1;
} else {
int d = gcd(abs(result.up), abs(result.down)); // 分子分母的最大公约数
result.up /= d; // 约去最大公约数
result.down /= d;
}
return result;
}
分数的四则运算
- 强调一点: 由于分数的乘法和除法的过程中可能使分子或分母超过
int
型表示范围, 因此一般情况下, 分子和分母应当使用long long
型来存储
Fraction add(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.down + f2.up * f1.down;
result.down = f1.down * f2.down;
// 必须在每一步加法后都约分,否则容易溢出
return reduction(result);
}
Fraction minus(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.down - f2.up * f1.down;
result.down = f1.down * f2.down;
return reduction(result);
}
Fraction multi(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.up;
result.down = f1.down * f2.down;
return reduction(result);
}
// 使用前需特判除数不为 0
Fraction divide(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.down;
result.down = f1.down * f2.up;
return reduction(result);
}
分数的输出
// 输出分数
void showResult(Fraction r) {
r = reduction(r);
if (r.down == 1) {
printf("%lld", r.up); // 整数
} else if(abs(r.up) > r.down) {
// 假分数
printf("%d %d/%d", r.up / r.down, (long long)abs(r.up) % r.down, r.down);
} else {
// 真分数
printf("%d/%d", r.up, r.down);
}
}
PAT (Advanced level) 1081 Rational Sum
#include <cstdio>
#include <cmath>
using namespace std;
int N;
struct Fraction {
long long up;
long long down;
};
Fraction Sum{
0, 1};
long long gcd(long long a, long long b)
{
if (b == 0)
{
return a;
}
else
{
return gcd(b, a % b);
}
}
void Reduce(Fraction& f)
{
int g = gcd(abs(f.up), abs(f.down));
f.up /= g;
f.down /= g;
}
void Add(Fraction& sum, const Fraction& operand)
{
sum.up = sum.up * operand.down + operand.up * sum.down;
sum.down = sum.down * operand.down;
Reduce(sum);
}
int main() {
scanf("%d", &N);
for (int i = 0; i != N; ++i)
{
Fraction tmp;
scanf("%lld/%lld", &tmp.up, &tmp.down);
Add(Sum, tmp);
}
if(abs(Sum.up) >= abs(Sum.down))
{
printf("%lld", Sum.up / Sum.down);
Sum.up %= Sum.down;
if (Sum.up != 0)
{
printf(" %lld/%lld", Sum.up, Sum.down);
}
}
else {
if (Sum.up == 0)
{
printf("0");
}
else {
printf("%lld/%lld", Sum.up, Sum.down);
}
}
return 0;
};
PAT (Advanced level) 1088 Rational Arithmetic
- 还是得按照上面介绍的方法按部就班的来,自己写完这题发现上面方法中的一些小细节还是很重要的,并不是可有可无,稍加不注意就会导致答案错误
#include <cstdio>
#include <cmath>
using namespace std;
int N;
struct Fraction {
long long up;
long long down;
};
long long gcd(long long a, long long b)
{
if (b == 0)
{
return a;
}
else
{
return gcd(b, a % b);
}
}
void Reduce(Fraction& f)
{
if (f.down < 0)
{
f.up = -f.up;
f.down = -f.down;
}
int g = gcd(abs(f.up), abs(f.down));
f.up /= g;
f.down /= g;
}
Fraction add(const Fraction& sum, const Fraction& operand)
{
Fraction tmp;
tmp.up = sum.up * operand.down + operand.up * sum.down;
tmp.down = sum.down * operand.down;
Reduce(tmp);
return tmp;
}
Fraction minus(const Fraction& sum, const Fraction& operand)
{
Fraction tmp;
tmp.up = sum.up * operand.down - operand.up * sum.down;
tmp.down = sum.down * operand.down;
Reduce(tmp);
return tmp;
}
Fraction mul(const Fraction& sum, const Fraction& operand)
{
Fraction tmp;
tmp.up = sum.up * operand.up;
tmp.down = sum.down * operand.down;
Reduce(tmp);
return tmp;
}
Fraction divide(const Fraction& sum, const Fraction& operand)
{
Fraction tmp;
tmp.up = sum.up * operand.down;
tmp.down = sum.down * operand.up;
Reduce(tmp);
return tmp;
}
void show_res(const Fraction& f)
{
if (f.up < 0)
{
printf("(");
}
if (f.down == 1)
{
printf("%lld", f.up);
}
else if(abs(f.up) >= abs(f.down))
{
printf("%lld %lld/%lld", f.up / f.down, (long long)abs(f.up) % f.down, f.down);
}
else {
printf("%lld/%lld", f.up, f.down);
}
if (f.up < 0)
{
printf(")");
}
}
int main() {
Fraction a, b;
scanf("%lld/%lld %lld/%lld", &a.up, &a.down, &b.up, &b.down);
Reduce(a);
Reduce(b);
show_res(a);
printf(" + ");
show_res(b);
printf(" = ");
show_res(add(a, b));
printf("\n");
show_res(a);
printf(" - ");
show_res(b);
printf(" = ");
show_res(minus(a, b));
printf("\n");
show_res(a);
printf(" * ");
show_res(b);
printf(" = ");
show_res(mul(a, b));
printf("\n");
show_res(a);
printf(" / ");
show_res(b);
printf(" = ");
if (b.up == 0)
{
printf("Inf");
}
else {
show_res(divide(a, b));
}
printf("\n");
return 0;
};
素数
- 除了 1 和本身之外,不能被其他数整除的正整数 a a a ( a > 1 a>1 a>1) 为素数;反之为合数
- 1 既不是素数,也不是合数
素数的判断
- 只需要判定 n n n 能否被 2 , 3 , … , ⌊ n ⌋ 2, 3, …, \lfloor \sqrt n\rfloor 2,3,…,⌊n⌋ 中的一个整除, 即可判定 n n n 是否为素数。算法的复杂度为 O ( n ) O(\sqrt n) O(n)
bool isPrime(int n) {
if(n <= 1)
return false;
int sqr = (int)sqrt(1.0 * n);
for(int i = 2; i <= sqr; i++) {
if(n % i == 0)
return false;
}
return true;
}
素数表的获取
- 如果想要得出 1 ∼ n 1\sim n 1∼n 范围内的全部素数,如果对 1 ∼ n 1\sim n 1∼n 进行枚举,判断每个数是否为素数,那么复杂度为 O ( n n ) O(n\sqrt n) O(nn)
- 这个复杂度对 n ≤ 1 0 5 n\leq10^5 n≤105 的大小是没有问题的, 考试时大部分涉及素数表的题目都不会超过这个范围
- 下面学习一个更高效的算法, 它的时间复杂度为 O ( n log log n ) O(n\log \log n) O(nloglogn)
- Eratosthenes 筛法 是众多筛法中最简单且容易理解的一种。更优的欧拉筛法可以达到 O ( n ) O(n) O(n) 的时间复杂度
- 算法从小到大枚举所有数, 对每一个素数, 筛去它的所有倍数, 剩下的就都是素数了
- 例如,求 1 ~ 15 中的所有素数
const int maxn = 101; // 表长
int prime[maxn], pNum = 0; // prime 数组存放所有素数, pNum 为素数个数
bool p[maxn] = {
0}; // 如果 i 为素数,则 p[i] 为 false: 否则, p[i] 为 true
void Find_Prime() {
for (int i = 2; i < maxn; i++) {
if(p[i] == false) {
// 如果 i 是素数
prime[pNum++] = i; // 把素数 1 存到 prime 数组中
for(int j = i + i; j < maxn; j += i) {
// 筛去所有 i 的倍数
p[j] = true;
}
}
}
}
PAT (Advanced level) 1015 Reversible Primes
- 注意判断素数时不要忘了特判 n = 1 n=1 n=1 的情况
#include <cstdio>
#include <vector>
#include <cmath>
using namespace std;
int reverse(int n, int radix)
{
int res = 0;
vector<int> rev;
while (n >= radix)
{
rev.push_back(n % radix);
n /= radix;
}
if (n > 0)
{
rev.push_back(n);
}
for (auto it = rev.begin(); it != rev.end(); ++it)
{
res = res * radix + *it;
}
return res;
}
bool isPrime(int n)
{
if (n <= 1)
{
return false;
}
int ub = sqrt(n);
for (int i = 2; i <= ub; ++i)
{
if (n % i == 0)
{
return false;
}
}
return true;
}
int main(void)
{
while (true)
{
int n, d;
scanf("%d", &n);
if (n < 0)
{
break;
}
scanf("%d", &d);
if (isPrime(n) && isPrime(reverse(n, d)))
{
printf("Yes\n");
}
else {
printf("No\n");
}
}
return 0;
};
PAT (Advanced level) 1078 Hashing
- 注意:解决冲突时算出的位置 x + i 2 x+i^2 x+i2 是要对 n n n (哈希表长) 取模的;如果当 i = n i=n i=n 时还没有找到位置,那么就不可能找到插入位置
#include <cstdio>
#include <vector>
#include <cmath>
using namespace std;
bool isPrime(int n)
{
if (n <= 1)
{
return false;
}
int ub = sqrt(n);
for (int i = 2; i <= ub; ++i)
{
if (n % i == 0)
{
return false;
}
}
return true;
}
bool hash_table[15000];
vector<int> pos;
int main(void)
{
int m, n;
scanf("%d %d", &m, &n);
int tsize = m;
while(!isPrime(tsize))
{
++tsize;
}
for (int i = 0; i < n; ++i)
{
int a;
scanf("%d", &a);
int p = a % tsize;
int j = 0;
while (j < tsize && hash_table[(p + j * j) % tsize] == true)
{
j += 1;
}
if (j >= tsize)
{
pos.push_back(-1);
}
else {
hash_table[(p + j * j) % tsize] = true;
pos.push_back((p + j * j) % tsize);
}
}
for (size_t i = 0; i != pos.size(); ++i)
{
if (pos[i] >= 0)
{
printf("%d", pos[i]);
}
else {
printf("-");
}
if (i + 1 != pos.size())
{
printf(" ");
}
}
printf("\n");
return 0;
};
质因子分解
- 质因子分解是指将一个正整数 n n n 写成一个或多个质数的乘积的形式。因此不妨先把素数表打印出来。而打印素数表的方法上面已经阐述, 下面我们主要就质因子分解本身进行讲解
- 注意:由于 1 本身不是素数, 因此它没有质因子, 下面的讲解是针对大于 1 的正整数来说的
- 由于每个质因子都可以不止出现一次,因此不妨定义结构体
factor
, 用来存放质因子及其个数- 考虑到 2 × 3 × 5 × 7 × 11 × 13 × 17 × 19 × 23 × 29 2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17 \times 19\times 23 \times 29 2×3×5×7×11×13×17×19×23×29 就已经超过了
int
范围,因此对一个int
型范围的数来说,fac
数组的大小只需要开到 10 就可以了
- 考虑到 2 × 3 × 5 × 7 × 11 × 13 × 17 × 19 × 23 × 29 2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17 \times 19\times 23 \times 29 2×3×5×7×11×13×17×19×23×29 就已经超过了
struct factor (
int x, cnt; // x 为质因子, cnt 为其个数
} fac[10];
- 例如对 180 来说,
fac
数组如下:
fac[0].x = 2;
fac[0].cnt = 2;
fac[1].x = 3;
fac[1].cnt = 2;
fac[2].x = 5;
fac[2].cnt = 1;
- 对一个正整数 n n n 来说, 如果它存在 [ 2 , n ] [2, n] [2,n] 范围内的质因子,要么这些质因子全部小于等于 n \sqrt n n, 要么只存在一个大于 n \sqrt n n 的质因子,而其余质因子全部小于等于 n \sqrt n n 。这就给进行质因子分解提供了一个很好的思路:
- (1) 枚举 1 ∼ n 1\sim \sqrt n 1∼n 范围内的所有质因子 p p p, 判断 p p p 是否是 n n n 的因子。如果 p p p 是 n n n 的因子, 那么给
fac
数组中增加质因子 p p p, 并初始化其不数为 0。然后,只要 p p p 还是 n n n 的因子,就让 n n n 不断除以 p p p, 每次操作令 p p p 的个数加 1, 直到 p p p 不再是 n n n 的因子为止; 如果 p p p 不是 n n n 的因子, 就直接跳过 - (2) 如果在上面步骤结束后 n n n 仍然大于 1, 说明 n n n 有且仅有一个大于 n \sqrt n n 的质因子, 这时需要把这个质因子加入
fac
数组,并令其个数为 1
- (1) 枚举 1 ∼ n 1\sim \sqrt n 1∼n 范围内的所有质因子 p p p, 判断 p p p 是否是 n n n 的因子。如果 p p p 是 n n n 的因子, 那么给
// (1)
if(n % prime[i] == 0) {
// 如果 prime[i] 是 n 的因子
fac[num].x = prime[i];
fac[num].cnt = 0;
while(n % prime[i] == 0) {
fac[num].cnt++;
n /= prime[i];
}
num++; // 不同质因子个数加1
}
// (2)
if(n != 1) {
fac[num].x = n;
fac[num++].cnt = 1;
]
- 最后指出, 如果要求一个正整数 N N N 的因子个数, 只需要对其质因子分解, 得到各质因子 p i p_i pi 的个数分别为 e 1 , e 2 , . . . , e k e_1,e_2,...,e_k e1,e2,...,ek, 于是 N N N 的因子个数就是 ( e 1 + 1 ) ∗ ( e 2 + 1 ) ∗ ⋅ ⋅ ⋅ ∗ ( e k + 1 ) (e_1 + 1) * (e_2 + 1) *· · ·* (e_k + 1) (e1+1)∗(e2+1)∗⋅⋅⋅∗(ek+1)。原因是,对每个质因子 p p p 都可以选择其出现 0 次、1 次、… 、 e i e_i ei 次, 共 e i + 1 e_{i} + 1 ei+1 种可能, 组合起来就是答案
- 由同样的原理可知, N N N 的所有因子之和为
PAT (Advanced level) 1059 Prime Factors
#include <cstdio>
#include <vector>
#include <cmath>
using namespace std;
#define MAX_SQRT_N 50000
vector<int> primes;
bool isprime[MAX_SQRT_N];
struct Factor {
int x, cnt;
};
Factor factors[10];
int num_factors = 0;
int main(void)
{
int n;
scanf("%d", &n);
printf("%d=", n);
if (n == 1)
{
printf("1");
return 0;
}
for (int i = 2; i < MAX_SQRT_N; ++i)
{
if (isprime[i] == false)
{
for (int j = i * 2; j < MAX_SQRT_N; j += i)
{
isprime[j] = true;
}
primes.push_back(i);
}
}
for (size_t i = 0; i != primes.size(); ++i)
{
if (n % primes[i] == 0)
{
factors[num_factors].x = primes[i];
factors[num_factors].cnt = 0;
while (n % primes[i] == 0)
{
++factors[num_factors].cnt;
n /= primes[i];
}
++num_factors;
}
}
if (n > 1)
{
factors[num_factors].x = n;
factors[num_factors++].cnt = 1;
}
for (int i = 0; i < num_factors; ++i)
{
printf("%d", factors[i].x);
if (factors[i].cnt > 1)
{
printf("^%d", factors[i].cnt);
}
if (i + 1 != num_factors)
{
printf("*");
}
}
return 0;
};
PAT (Advanced level) 1096 Consecutive Factors
#include <cstdio>
#include <cmath>
using namespace std;
int main(void)
{
int n;
scanf("%d", &n);
int cnt = 0, cpos = 0;
int ub = sqrt(n);
for (int i = 2; i <= ub; ++i)
{
int j = i, prod = 1;
while (n % j == 0)
{
prod *= j;
if (n < prod || n % prod != 0)
{
break;
}
++j;
}
int tmp_cnt = j - i;
if (tmp_cnt > cnt)
{
cnt = tmp_cnt;
cpos = i;
}
}
if (cnt == 0)
{
printf("%d\n%d", 1, n);
}
else {
printf("%d\n", cnt);
while (cnt--)
{
printf("%d", cpos++);
if (cnt > 0)
{
printf("*");
}
else {
printf("\n");
}
}
}
return 0;
};
大整数运算
- 当题目出现好几十位的整数时,就要考虑使用数组存储
大整数的存储
- 使用数组即可; 整数的高位存储在数组的高位, 整数的低位存储在数组的低位。 不反过来存储的原因是,在进行运算的时候都是从整数的低位到高位进行枚举,顺位存储和这种思维相合
- 为了方便随时获取大整数的长度,一般都会定义一个
int
型变量len
来记录其长度,并和d
数组组合成结构体:
struct bign {
// big number
vector<int> d(1000, 0); // 记得加上初始值
int len = 0;
};
- 输入大整数时,一般都是先用字符串读入,然后再把字符串另存为至
bign
结构体。由于使用char
数组进行读入时, 整数的高位会变成数组的低位, 而整数的低位会变成数组的高位, 因此为了让整数在bign
中是顺位存储, 需要让字符串倒着赋给d[]
数组
bign change(char str[]) {
bign a;
a.len = strlen(str);
for(int i = 0; i < a.len; i++) {
a.d[i] = str[a.len - i - 1] -'0';
}
return a;
}
- 比较两数大小:
// 比较 a 和 b 大小,a 大、相等、a 小分别返回 1、0、-1
int compare(bign a, bign b) {
if(a.len > b.len)
return 1;
else if(a.len < b.len)
return -1;
else {
for(int i = a.len - 1; i >= 0; i--) {
if(a.d[i] > b.d[i])
return 1;
else if(a.d[i) < b.d[i))
return -1;
}
return 0;
}
}
大整数的四则运算
高精度加法
bign add(bign a, bign b) {
bign c;
int carry = 0; // carry 是进位
for(int i = 0; i < a.len || i < b.len; i++) {
// 以较长的为界限
int temp = a.d[i] + b.d[i] + carry;
c.d[c.len++] = temp % 10;
carry = temp / 10;
}
if (carry != 0) {
c.d[c.len++] = carry;
}
return c;
}
- 这样写法的条件是两个对象都是非负整数
- 如果有一方是负的, 可以在转换到数组这一步时去掉其负号, 然后采用高精度减法
- 如果两个都是负的, 就都去掉负号后用高精度加法, 最后再把负号加回去即可
高精度减法
bign sub(bign a, bign b) {
//高精度a - b
bign c;
for (int i = 0; i < a.len || i < b.len; i ++) {
if(a.d[i] < b.d[i]) {
// 借位
a.d[i + 1]--;
a.d[i] += 10;
}
c.d[c.len++] = a.d[i] - b.d[i];
}
while (c.len - 1 >= 1 && c.d[c.len - 1] == 0) {
c.len--; // 去除高位的0, 同时至少保留一位最低位
}
return c;
- 使用
sub
函数前要比较两个数的大小, 如果被减数小于减数, 需要交换两个变量, 然后输出负号, 再使用sub
函数
高精度与低精度的乘法
- 所谓的低精度就是可以用基本数据类型存储的数据, 例如
int
型。这里讲述的就是bign
类型与int
类型的乘法- 以 147 × 35 147 \times 35 147×35 为例,这里把 147 147 147 视为 高精度
bign
类型, 而 35 35 35 视为int
类型, 并且在下面的过程中, 始终将 35 35 35 作为一个整体看待
- (1) 7 × 35 = 245 7 \times35=245 7×35=245, 取个位数 5 作为该位结果, 高位部分 24 作为进位
- (2) 4 × 35 = 140 4 \times 35 = 140 4×35=140, 加上进位 24, 得 164, 取个位数 4 为该位结果,高位部分 16 作为进位
- (3) 1 × 35 = 35 1 \times 35 =35 1×35=35, 加上进位 16, 得 51, 取个位数 1 为该位结果, 高位部分 5 作为进位
- (4) 没的乘了, 此时进位还不为 0, 就把进位 5 直接作为结果的高位
- 以 147 × 35 147 \times 35 147×35 为例,这里把 147 147 147 视为 高精度
bign multi(bign a, int b) {
bign c;
int carry = 0; // 进位
for (int i = 0; i < a.len; i++) {
int temp = a.d[i] * b + carry;
c.d[c.len++] = temp % 10;
carry = temp / 10;
}
// 和加法不一样, 乘法的进位可能不止一位, 因此用 while
while (carry ! = 0) {
c.d[c.len++] = carry % 10;
carry /= 10;
}
return c;
}
- 注意:如果
a
和b
中存在负数, 需要先记录下其负号, 然后取它们的绝对值代入函数
高精度与低精度的除法
- 归纳其中某一步的步骤: 上一步的余数乘以 10 加上该步的位, 得到该步临时的被除数,将其与除数比较: 如果不够除, 则该位的商为 0: 如果够除, 则商即为对应的商, 余数即为对应的余数
- 最后一步要注意减法后高位可能有多余的 0, 要忽视它们, 但也要保证结果至少有一位数
bign divide(bign a, int b, int& r) {
// r 为余数
bign c;
c.len = a.len; // 被除数的每一位和商的每一位是一一对应的, 因此先令长度相等
for(int i = a.len - 1; i >= 0; i--){
r = r * 10 + a.d[i];
if(r < b)
c.d[i] = 0; // 不够除, 该位为0
else {
// 够除
c.d[i] = r / b; // 商
r = r % b; // 获得新的余数
}
}
while(c.len - 1 >= 1 && c.d[c.len - 1] == 0) {
c.len--; // 去除高位的0, 同时至少保留一位最低位
}
return c;
}