机试 (数学问题)

简单数学

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 abc 可以为空或是任意数量的 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 y1 x x x 变为 z − x z-x zx。这样可以一直逆回去, 直到 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 y1 次回退。而 T T T 右边 A A A 的个数 z z z 在经过 y − 1 y-1 y1 次回退后将变为 z − x ∗ ( y − 1 ) z-x*(y-1) zx(y1)。这时, 判断条件2成立的条件是 z − x ∗ ( y − 1 ) z-x*(y-1) zx(y1) 是否等于 P P P 左边 A A A 的个数 x x x:如果 z − x ∗ ( y − 1 ) = x z-x*(y-1)=x zx(y1)=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 1n 过程中在该位可能出现的 1 的个数.
      • 注意到 n n n 在 1 号位的左侧是 3071 3071 3071
      • 由于 1 号位为 0 (小于 1), 因此在 1 ∼ n 1\sim n 1n 中, 仅在高四位为 0000 ∼ 3070 0000\sim 3070 00003070 的过程中,1 号位才可取到 1, 即 00001 、 00011 、 00021 、 … 、 30691 、 30701 00001、00011、00021、… 、30691、30701 0000100011000213069130701, 总共有 3071 3071 3071 种情况
      • 因此在 1 ∼ n 1\sim n 1n 过程中,1 号位将出现 3071 个 1 (即 3071 × 1 0 0 3071\times10^0 3071×100).
    • (2) 考虑 2 号位在 1 ∼ n 1\sim n 1n 过程中在该位可能出现的 1 的个数
      • 注意到 n n n 在 2 号位左侧是 307, 在 2 号位右侧是 0
      • 由于 2 号位为 1, 因此在 1 ∼ n 1\sim n 1n 中,仅在高三位为 000 ∼ 306 000\sim 306 000306 的过程中,对任意的低一位(即 0 ∼ 9 0\sim 9 09), 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 30710n 有多少 2 号位为 1 的数。由于 n n n 就是 30710 30710 30710, 因此这里只有一个数
      • 因此在 1 ∼ n 1\sim n 1n 过程中,2 号位将出现 3071 = 307 × 1 0 1 + 1 3071=307 \times10^1+1 3071=307×101+1 个 1
    • (3) 考虑 3 号位在 1 ∼ n 1\sim n 1n 过程中在该位可能出现的 1 的个数
      • 注意到 n n n 在 3 号位左侧是 30 30 30, 在 3 号位右侧是 10 10 10
      • 由于 3 号位为 7 (大于1),因此在 1 ∼ n 1\sim n 1n 中,在高二位为 00 ∼ 30 00\sim30 0030 的过程中,对任意的低二位(即 00 ∼ 99 00\sim99 0099), 3 号位均可取到 1, 总共有 3100 = ( 30 + 1 ) × 1 0 2 3100=(30+1)\times10^2 3100=(30+1)×102 种情况
      • 因此在 1 ∼ n 1\sim n 1n 过程中,3 号位将出现 3100 个 1
    • (4) 同理,4、5 号位上出现 1 的情况数分别为 3000 3000 3000 10000 10000 10000
    • 综上所述, 1 ∼ n 1\sim n 1n 中共会出现 22242 = 3071 + 3071 + 3100 + 3000 + 10000 22242 = 3071 + 3071 + 3100 + 3000 + 10000 22242=3071+3071+3100+3000+10000 个 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
#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)(Ni);推出这个之后只需要 O ( n ) O(n) O(n) 即可解题
  • 我是在测试点2卡住了,原因是浮点型精度往往是有误差的,如果对浮点型精度进行大数据量的运算, 可能会导致误差累积,影响最终结果。而测试点2就是大数据量的运算
    • 解决方法是将输入的小数乘 1000,用 long long 型变量存储, 最后除以 1000 输出即可
    • p.s. 希望再也不要碰到这种坑!!!
#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 ab 均为正整数, 则 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 nm 个数据就行了
  • 下面主要讲解如何让移动的次数最少:
    • 将序列中一个元素先拿出至临时变量, 然后把将要移动到空出位置的元素移至该空出位置, 再把新空出的位置用将要移动到这个新空出的位置的元素代替, 以此类推, 直到所有元素移动完毕
    • 如下图所示,先将 5 放进临时变量 temp, 由于 M 值为 2, 因此最终一定是 3 在原先 5 的位置上,于是把 3 移动到 5 的位置;同样的, 3 的位置在最终态时一定是 1, 于是把 1 移动到 3 的位置;接下来, 由于 1 左边第二个位置是一开始 5 的位置,因此不再继续,把 temp 的值 5 放在 1 的位置,步骤1 结束。而步骤 2 同理, 最终得到需要的结果。这里只从 5 跟 6 开始是因为右移两位后 5 和 6 会移到序列最前方,因此对题目给定的 NM, 可以从 N - M 号位开始枚举起始位,直到 N - 1 号位结束(这里的位号都指从 0 开始的位号)
      在这里插入图片描述
    • 不过这样会产生一个问题:有可能经过一轮循环之后, N - M 号位之后又多个位置已经得到了最终结果,这样当继续枚举起始位时就会产生重复,最后导致错误。例如假设 N = 8、M = 3, 这种情况下只需要枚举一次起始位,即从 8 - 3 = 5 号位开始循环, 一轮循环下来就可以得到整个序列的最终结果, 而不需要再去从 6 号位跟 7 号位开始循环
    • 为了解决这个问题,设 dNM 的最大公约数, 那么N - M 号位开始枚举起始位,直到 N - M + d - 1 位结束 (枚举一个最大公约数的范围)
      • 例如 N = 8M = 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 1n 范围内的全部素数,如果对 1 ∼ n 1\sim n 1n 进行枚举,判断每个数是否为素数,那么复杂度为 O ( n n ) O(n\sqrt n) O(nn )
    • 这个复杂度对 n ≤ 1 0 5 n\leq10^5 n105 的大小是没有问题的, 考试时大部分涉及素数表的题目都不会超过这个范围

  • 下面学习一个更高效的算法, 它的时间复杂度为 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 就可以了
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 1n 范围内的所有质因子 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)
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 直接作为结果的高位
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;
}
  • 注意:如果 ab 中存在负数, 需要先记录下其负号, 然后取它们的绝对值代入函数

高精度与低精度的除法

在这里插入图片描述

  • 归纳其中某一步的步骤: 上一步的余数乘以 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;
}

猜你喜欢

转载自blog.csdn.net/weixin_42437114/article/details/114819230
今日推荐