跳转至

第六章 函数

基础数学知识

  • 因子、完全数与亲和数:本章求因子和时不含数本身;完全数的因子和等于自身,亲和数要求两个不同的数互为因子和。对应 1150、1154。
  • 质数与成对因子:质数大于 \(1\),只有 \(1\) 和自身两个正因子;若合数有一对因子,至少一个不超过平方根。对应 1151、1153、1155、1157、1403、1405、1407—1411。
  • 分式与代入:先分别求分子、分母,再做除法,分母不能为零。对应 1152、1167。
  • 平方根与嵌套根式:\(\sqrt a\) 表示平方为 \(a\) 的非负数;多层根式要从内往外理解。对应 1156、1166。
  • 正负交替的求和式与近似:观察每项的幂、分母和符号;截断求和得到近似值,输出位数不等于实际精度。对应 1156。
  • 等差数列求和与方程变形:\(1+2+\cdots+y=\frac{y(y+1)}2\);将门牌关系化为 \(\frac{y(y+1)}2-3x=n\)。对应 1404、1158。
  • 向上取整:不足一条的短信也要按一条计费,条数为 \(\lceil x/70\rceil\)。对应 1398。
  • 进位制与位权:例如 \((42)_{13}=4\times13+2\),每个数位必须小于进制;二进制除以 \(2\) 可依次取出低位。对应 1412、1413、1161。
  • 余数与循环:循环使用长度为 \(k\) 的密钥可用 i % k;字母偏移按 \(26\) 个字母绕回。对应 1402。
  • 数列、递推与递归定义:先给起点,再用较小项定义后续项,例如 \(F_n=F_{n-1}+F_{n-2}\);调用函数时要逐步靠近起点。对应 1158、1159、1163、1165—1167。

第一节 函数

1150:求正整数 2 和 n 之间的完全数

提示 1:先只检查一个数

6的真因子是1、2、3,相加正好是6。

先管好一件事,判断传进来的x。

哪些数要输出,等函数写好再说。

提示 2:函数把判断结果交回来

在bool check(int x)里,sum从0开始,把小于x的正因子加进去,最后:

return sum == x;

check(6)得到true,check(8)应得到什么?

提示 3:主函数决定是否输出

让i从2到n,只输出通过检查的数:

if (check(i)) cout << i << '\n';

判断函数里不输出,这样主函数才好决定怎样使用结果。

知识补充

本题 \(n\le5000\),总和用 int 足够。

第二种方法利用因子成对出现,只查到平方根。

常见错误

不包含数本身,但要包含因子 \(1\)。

成对找因子时,平方根不能重复加。

自测

输入:7 → 预期输出:6

输入:2 → 预期输出:不输出任何字符

输入:5000,预期输出:

6
28
496

自己试试:自己选一个完全数,分别把它和比它小 \(1\) 的数作为上限。

两次输出应相差哪一个数?

方法一:逐个检查因子
#include <bits/stdc++.h>

using namespace std;

bool check(int x) {
    int sum = 0;
    for (int i = 1; i < x; i++)
        if (x % i == 0) sum += i;

    return sum == x;
}

int main() {
    int n;
    cin >> n;

    for (int i = 2; i <= n; i++)
        if (check(i)) cout << i << '\n';

    return 0;
}
方法二:成对寻找因子
#include <bits/stdc++.h>

using namespace std;

bool check(int x) {
    int sum = 1;
    for (int i = 2; i <= x / i; i++) {
        if (x % i != 0) continue;
        sum += i;
        if (i != x / i) sum += x / i;
    }

    return sum == x;
}

int main() {
    int n;
    cin >> n;

    for (int i = 2; i <= n; i++)
        if (check(i)) cout << i << '\n';

    return 0;
}

1151:素数个数

提示 1:函数只负责回答是不是质数

is_prime不负责计数,也不负责输出;主函数还需要自己累加答案。

提示 2:找到一个因数就能否定

在bool is_prime(int x)中先排除小于2的数:

if (x < 2) return false;
for (int i = 2; i <= x / i; i++)
    if (x % i == 0) return false;

全部试完才返回true,为什么不能第一次没除尽就返回true?

提示 3:把判断接进计数循环

主函数cnt从0开始,i从2到n:

if (is_prime(i)) cnt++;

最后输出cnt,不是把每个质数都输出。

知识补充

本题 \(2\le n\le50000\)。

is_prime 找到约数就返回 false,全部试完才返回 true。

常见错误

\(1\) 不是质数,试除要包含平方根。

用 \(4\)、\(9\) 检查循环是否漏掉端点。

自测

输入:10 → 预期输出:4

输入:2 → 预期输出:1

输入:50000 → 预期输出:5133

参考代码
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    int n;
    cin >> n;

    int cnt = 0;
    for (int i = 2; i <= n; i++)
        if (is_prime(i)) cnt++;

    cout << cnt;

    return 0;
}

1152:最大数 max (x,y,z)

提示 1:先把三次调用圈出来

分子求一次最大值,分母有两次;对照原式,三组参数哪里不同?

提示 2:先完成三个数取最大值

在double maxn(double a, double b, double c)中:

return max(a, max(b, c));

主函数用它分别算x、y、z,最后代入x / (y * z),别把乘积漏成只除一个数。

知识补充

题目中的关系是

\[ m=\frac{\max(a,b,c)}{\max(a+b,b,c)\times\max(a,b,b+c)}. \]

题目要求分别用函数和过程实现,第二种用 void 函数表示过程。

double &ans 让函数能够修改调用处传入的变量,不是再复制一份。

分母为零时分式没有定义,题面未给出此时的输出规则,不另编输出。

常见错误

不要把 maxn(a + b, b, c) 看成 maxn(a, b, c)。

用 double 计算,保留三位小数。

自测

输入:1 2 3 → 预期输出:0.200

输入:2 4 6 → 预期输出:0.100

方法一:用返回值带回最大值
#include <bits/stdc++.h>

using namespace std;

double maxn(double a, double b, double c) {
    return max(a, max(b, c));
}

int main() {
    double a, b, c;
    cin >> a >> b >> c;

    double x = maxn(a, b, c);
    double y = maxn(a + b, b, c);
    double z = maxn(a, b, b + c);

    printf("%.3lf", x / (y * z));

    return 0;
}
方法二:用引用参数带回最大值
#include <bits/stdc++.h>

using namespace std;

void maxn(double a, double b, double c, double &ans) {
    ans = max(a, max(b, c));
}

int main() {
    double a, b, c;
    cin >> a >> b >> c;

    double x, y, z;
    maxn(a, b, c, x);
    maxn(a + b, b, c, y);
    maxn(a, b, b + c, z);

    printf("%.3lf", x / (y * z));

    return 0;
}

1153:绝对素数

提示 1:把13倒过来再检查

13和31都是质数,而19倒过来是91,能通过吗?

提示 2:交换两位后复用同一个判断

两位候选i:

int x = i % 10 * 10 + i / 10;

自己写is_prime(i) && is_prime(x),放进10到99的枚举中。

常见错误

“绝对素数”在本题中只要求两位交换后仍是质数。

不要补成其他题里的“任意排列都为质数”。

自测

无需输入,预期输出:

11
13
17
31
37
71
73
79
97
参考代码
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    for (int i = 10; i <= 99; i++) {
        int x = i % 10 * 10 + i / 10;
        if (is_prime(i) && is_prime(x)) cout << i << '\n';
    }

    return 0;
}

1154:亲和数

提示 1:一个因子和还不够

设a的真因子和为b,还要检查b的真因子和等于谁?

完全数的因子和等于自身,但这里要求两数不同。

提示 2:函数返回因子和

写factor_sum(x),只加小于x的正因子;成对求因子时,平方根只加一次。

if (i != x / i) sum += x / i;

这句放在i为因数、且已加i之后。

提示 3:按小的成员从前往后找

枚举a,先算b = factor_sum(a):

if (a < b && factor_sum(b) == a) {
    cout << a << ' ' << b;
    break;
}

为什么a等于b时不能进入?

知识补充

用 \(16\) 检查求和函数:因子是 \(1,2,4,8\),和为 \(15\)。

如果把 \(4\) 加了两次,成对求因子的写法就有问题。

常见错误

不能把完全数当成亲和数,题目要求两个数不同。

因子和不含自身,平方根因子只加一次。

自测

无需输入,预期输出:220 284

参考代码
#include <bits/stdc++.h>

using namespace std;

int factor_sum(int x) {
    if (x <= 1) return 0;
    int sum = 1;
    for (int i = 2; i <= x / i; i++) {
        if (x % i != 0) continue;
        sum += i;
        if (i != x / i) sum += x / i;
    }

    return sum;
}

int main() {
    for (int a = 2; ; a++) {
        int b = factor_sum(a);
        if (a < b && factor_sum(b) == a) {
            cout << a << ' ' << b;
            break;
        }
    }

    return 0;
}

1155:回文三位数

提示 1:两道检查分开过

121左右相同,但11乘11等于121,为什么不能输出?

提示 2:三位回文只比较首尾

在bool is_palindrome(int x)中:

return x / 100 == x % 10;

主函数枚举100到999,还要用&&接上is_prime;这里的回文函数只针对三位数。

常见错误

\(121\) 是回文数,却不是质数。

三位数要从 \(100\) 检查到 \(999\)。

自测

无需输入,预期输出:

101
131
151
181
191
313
353
373
383
727
757
787
797
919
929
参考代码
#include <bits/stdc++.h>

using namespace std;

bool is_palindrome(int x) {
    return x / 100 == x % 10;
}

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    for (int i = 100; i <= 999; i++)
        if (is_palindrome(i) && is_prime(i)) cout << i << '\n';

    return 0;
}

1156:求 π 的值

提示 1:先把项的变化列出来

题面给了求和式,不要求先学反三角函数。

分子是x、三次方、五次方,分母是1、3、5,符号怎样轮换?

提示 2:当前项加完后再更新

在arctanx中,分子numerator初值x、分母denominator初值1、sign初值1,sum从0开始:

sum += sign * numerator / denominator;
numerator *= x * x;
denominator += 2;
sign = -sign;

自己用正负两项跟踪,看符号是否轮换。

提示 3:停止看项,不是看总和

用fabs(numerator / denominator)与eps比较,不足阈值时停止加入。

函数返回sum,主函数再乘6;十位小数是输出格式,不代表截断误差小于十位小数。

知识补充

按题目给出的求和式计算,不需要先学会反三角函数。

\[ \arctan x=x-\frac{x^3}{3}+\frac{x^5}{5}-\cdots, \qquad \pi=6\arctan\frac1{\sqrt3}. \]

这是无限求和式,不能称作有限项多项式。

参考代码沿用原有截断写法,首个绝对值不足 \(10^{-6}\) 的项不再加入。

常见错误

负数项不能直接与正的阈值比较。

输出十位小数是格式要求,不代表这个截断结果有十位精度。

自测

无需输入,预期输出:3.1415905109

参考代码
#include <bits/stdc++.h>

using namespace std;

const double eps = 1e-6;

double arctanx(double x) {
    double sum = 0, numerator = x;
    int denominator = 1, sign = 1;
    while (fabs(numerator / denominator) >= eps) {
        sum += sign * numerator / denominator;
        numerator *= x * x;
        denominator += 2;
        sign = -sign;
    }

    return sum;
}

int main() {
    printf("%.10lf", 6 * arctanx(1.0 / sqrt(3)));

    return 0;
}

1157:哥德巴赫猜想

提示 1:确定一个加数就确定另一个

要把偶数x拆成两个数,先选a,另一个只能是多少?

从小a往大试,找到的第一组有什么特点?

提示 2:两个加数都交给质数函数
int b = x - a;
if (is_prime(a) && is_prime(b)) {
    // 按题面输出一条等式
    break;
}

放在枚举a的内层循环;外层x从6到100每次加2,break应结束哪一层?

知识补充

这里只验证题目指定的有限区间,不是证明哥德巴赫猜想对所有偶数成立。

常见错误

每个偶数只输出一次,不是输出全部拆分。

输出等号、加号时不加空格。

自测

无需输入,预期输出:

6=3+3
8=3+5
10=3+7
12=5+7
14=3+11
16=3+13
18=5+13
20=3+17
22=3+19
24=5+19
26=3+23
28=5+23
30=7+23
32=3+29
34=3+31
36=5+31
38=7+31
40=3+37
42=5+37
44=3+41
46=3+43
48=5+43
50=3+47
52=5+47
54=7+47
56=3+53
58=5+53
60=7+53
62=3+59
64=3+61
66=5+61
68=7+61
70=3+67
72=5+67
74=3+71
76=3+73
78=5+73
80=7+73
82=3+79
84=5+79
86=3+83
88=5+83
90=7+83
92=3+89
94=5+89
96=7+89
98=19+79
100=3+97
参考代码
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    for (int x = 6; x <= 100; x += 2) {
        for (int a = 2; a <= x / 2; a++) {
            int b = x - a;
            if (is_prime(a) && is_prime(b)) {
                cout << x << '=' << a << '+' << b << '\n';
                break;
            }
        }
    }

    return 0;
}

1397:简单算术表达式求值

提示 1:先让输入按类型自动分开

表达式12 +34含空格,也可以拆成整数、字符、整数。

提示 2:字符前的空白由提取操作跳过
int a, b;
char op;
cin >> a >> op >> b;

整数读到加号前停止,char提取会跳过空白;先写加法分支,再补其余四种运算。

提示 3:空格变化不应改变结果

把12+34、12 +34、12+ 34分别运行。

若用scanf,格式串"%d %c %d"中的空格也能跳过空白;%c自己不会跳过。

知识补充

scanf 的 %c 不会自动跳过空白,%d 会。

原来的 "%2d%c%2d" 可用于观察读取过程,但不满足本题全部输入条件。

例如 12 +34 会把空格读进运算符,不能因为某次测评通过就认定写法完整。

格式串 "%d %c %d" 中的空格会跳过空白,也允许那里没有空格。

%2d 只是限制最多读取两个字符宽度,不是读整数的必需写法。

常见错误

题面明确允许运算符前后有空格。

两位正整数相除是整数除法,输出结果不一定是两位数。

自测

输入:32+64 → 预期输出:96

输入:12 + 34 → 预期输出:46

输入:12+ 34 → 预期输出:46

输入:25 / 12 → 预期输出:2

输入:25 % 12 → 预期输出:1

输入:10-99 → 预期输出:-89

自己试试:自己写一个两位正整数的表达式,先算出结果。

只改变运算符两侧空格的数量,结果应不应该变?

方法一:cin 依次读取
#include <bits/stdc++.h>

using namespace std;

int main() {
    int a, b;
    char op;
    cin >> a >> op >> b;

    if (op == '+') cout << a + b;
    else if (op == '-') cout << a - b;
    else if (op == '*') cout << a * b;
    else if (op == '/') cout << a / b;
    else if (op == '%') cout << a % b;

    return 0;
}
方法二:scanf 跳过空白
#include <bits/stdc++.h>

using namespace std;

int main() {
    int a, b;
    char op;
    scanf("%d %c %d", &a, &op, &b);

    if (op == '+') cout << a + b;
    else if (op == '-') cout << a - b;
    else if (op == '*') cout << a * b;
    else if (op == '/') cout << a / b;
    else if (op == '%') cout << a % b;

    return 0;
}
方法三:字符串拆分(拓展)
#include <bits/stdc++.h>

using namespace std;

int to_number(string s) {
    int value = 0;
    for (int i = 0, len = s.size(); i < len; i++) value = value * 10 + s[i] - '0';

    return value;
}

int main() {
    string s, left, right;
    getline(cin, s);

    int pos = 0, len = (int)s.size();
    while (pos < len && s[pos] == ' ') pos++;
    while (pos < len && s[pos] >= '0' && s[pos] <= '9') left += s[pos++];
    while (pos < len && s[pos] == ' ') pos++;
    char op = s[pos++];
    while (pos < len && s[pos] == ' ') pos++;
    while (pos < len && s[pos] >= '0' && s[pos] <= '9') right += s[pos++];

    int a = to_number(left), b = to_number(right);

    if (op == '+') cout << a + b;
    else if (op == '-') cout << a - b;
    else if (op == '*') cout << a * b;
    else if (op == '/') cout << a / b;
    else if (op == '%') cout << a % b;

    return 0;
}

1398:短信计费

提示 1:70字与71字差一条

每次发送要单独拆分,两次各35字是否只收一条的钱?

提示 2:让函数先返回整数条数

在int pieces(int x)中:

return x / 70 + (x % 70 != 0);

前半算整份,后半有剩余就加1;主函数先累计条数,最后除10.0换算元。

知识补充

每条 \(0.1\) 元,相当于把总条数除以 \(10\)。

pieces 把不足 \(70\) 字的剩余部分也算一条,不把不同次发送的内容合并。

常见错误

不是把所有次数的字数合起来再拆分。

避免反复累加 0.1,先统计整数条数更清楚。

自测

输入:3 70 71 140 → 预期输出:0.5

输入:1 1 → 预期输出:0.1

自己试试:自己造两次发送,让它们都刚好不超过一条短信。

如果把两次字数合并后计算,会不会得到不同费用?

参考代码
#include <bits/stdc++.h>

using namespace std;

int pieces(int x) {
    return x / 70 + (x % 70 != 0);
}

int main() {
    int n;
    cin >> n;

    long long sum = 0;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        sum += pieces(x);
    }

    printf("%.1lf", sum / 10.0);

    return 0;
}

1399:甲流病人初筛

提示 1:温度和咳嗽要同时符合

体温38度但不咳嗽,能通过初筛吗?

再试恰好37.5且咳嗽的情况。

提示 2:让判断函数只返回真假

在bool check(double t, int cough)中:

return t >= 37.5 && cough == 1;

主函数逐个读姓名与信息,通过时输出姓名并计数;人数最后再输出。

常见错误

没有人符合时,仍要输出人数 \(0\)。

用温度恰好 \(37.5\)、咳嗽标志为 \(0\) 的数据,检查“并且”有没有写错。

自测

输入:

5
Zhang 38.3 0
Li 37.5 1
Wang 37.1 1
Zhao 39.0 1
Liu 38.2 1

预期输出:

Li
Zhao
Liu
3

输入:1 A 37.4 1 → 预期输出:0

参考代码
#include <bits/stdc++.h>

using namespace std;

bool check(double t, int cough) {
    return t >= 37.5 && cough == 1;
}

int main() {
    int n;
    cin >> n;

    int cnt = 0;
    for (int i = 0; i < n; i++) {
        string s;
        double t;
        int cough;
        cin >> s >> t >> cough;
        if (check(t, cough)) {
            cout << s << '\n';
            cnt++;
        }
    }

    cout << cnt;

    return 0;
}

1400:统计单词数

提示 1:先用一句短话分清整词

找to,在today to TO里应找到两次;先统一大小写。

位置从0数,空格也得占一格。

所以原句的空格别删,删了位置就变了。

提示 2:先找出一个词的两端

沿方法一写:i遇到空格就向后走,走到字母时用j找词尾。

len是原文长度,j从i开始:

while (j < len && text[j] != ' ') j++;

此时text.substr(i, j - i)取出这个词;为什么先检查j < len?

提示 3:匹配一次,做两件事

word和text都已转成小写,cnt = 0、first = -1:

if (text.substr(i, j - i) == word) {
    cnt++;
    if (first == -1) first = i;
}

随后让i = j,回到循环继续找;后面再匹配时,只加次数,不改首次位置。

知识补充

get_lower 用引用参数修改原字符串。

find 找不到时返回 string::npos,不要先转成 int 再与 -1 比较。

方法二在两端补空格,把单词边界一起匹配;补入的首空格正好抵消匹配起点在单词前一个位置的偏移。

下一次从 pos + 1 查找,让相邻匹配仍能共用中间的空格。

常见错误

to 出现在 today 里不算一次匹配。

不能删掉原文空格再找位置,位置从 \(0\) 开始。

自测

输入:

To
to be or not to be is a question

预期输出:2 0

输入:

to
Today TO to

预期输出:2 6

输入:

to
  today  

预期输出:-1

输入:

a
 a  a

预期输出:2 1

自己试试:自己造一句话,让目标单词连续出现两次,中间只隔一个空格。

只在句首加几个空格,次数和首次位置应该分别怎样变化?

方法一:逐个取出单词
#include <bits/stdc++.h>

using namespace std;

void get_lower(string &s) {
    for (int i = 0, len = s.size(); i < len; i++)
        if (s[i] >= 'A' && s[i] <= 'Z') s[i] += 'a' - 'A';
}

int main() {
    string word, text;
    getline(cin, word);
    getline(cin, text);

    get_lower(word);
    get_lower(text);

    int cnt = 0, first = -1;
    int i = 0, len = (int)text.size();
    while (i < len) {
        if (text[i] == ' ') {
            i++;
            continue;
        }
        int j = i;
        while (j < len && text[j] != ' ') j++;
        if (text.substr(i, j - i) == word) {
            cnt++;
            if (first == -1) first = i;
        }
        i = j;
    }

    if (cnt == 0) cout << -1;
    else cout << cnt << ' ' << first;

    return 0;
}
方法二:补边界后使用 find
#include <bits/stdc++.h>

using namespace std;

void get_lower(string &s) {
    for (int i = 0, len = s.size(); i < len; i++)
        if (s[i] >= 'A' && s[i] <= 'Z') s[i] += 'a' - 'A';
}

int main() {
    string word, text;
    getline(cin, word);
    getline(cin, text);

    get_lower(word);
    get_lower(text);
    word = ' ' + word + ' ';
    text = ' ' + text + ' ';

    int cnt = 0;
    string::size_type pos = text.find(word), first = pos;
    while (pos != string::npos) {
        cnt++;
        pos = text.find(word, pos + 1);
    }

    if (cnt == 0) cout << -1;
    else cout << cnt << ' ' << first;

    return 0;
}

1401:机器翻译

提示 1:命中不会重新排队

容量2,依次处理1、2、1、3,最后该淘汰1还是2?

规则是最早进入内存的先出,不是最久没使用的先出。

提示 2:先检查是否命中

队列q记录进入顺序,vis记录当前是否在内存:

if (vis[x]) continue;
cnt++;

x已读入,cnt初值0;命中时为何不能再push一次?

提示 3:满了先腾出一个位置

容量m为正,未命中且队列已满时:

vis[q.front()] = false;
q.pop();

随后入队x并设vis[x]为true,两个标记都要与队列同步。

知识补充

全局 vis 默认是 false,这里约定它表示“还不在内存中”。

这是人为建立的对应关系,不是 bool 自带的含义。

队列只在已满时取队头,题目给定容量为正数。

常见错误

命中也重新入队,会把先进先出误写成别的规则。

单词编号允许为 \(0\),标记数组也要覆盖到 \(1000\)。

自测

输入:3 7 1 2 1 5 4 4 1 → 预期输出:5

输入:1 4 0 0 1 0 → 预期输出:3

输入:2 4 1 2 1 3 → 预期输出:3

自己试试:自己造一组单词,让最早进入内存的词刚被再次使用,接着就要淘汰一个词。

命中后如果错误地调整队列顺序,会淘汰谁?

参考代码
#include <bits/stdc++.h>

using namespace std;

queue<int> q;
bool vis[1010];

int main() {
    int m, n;
    cin >> m >> n;

    int cnt = 0;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        if (vis[x]) continue;
        cnt++;
        if ((int)q.size() == m) {
            vis[q.front()] = false;
            q.pop();
        }
        q.push(x);
        vis[x] = true;
    }

    cout << cnt;

    return 0;
}

1402:Vigenère 密码

提示 1:先试一个字母解密

密钥B代表偏移1,密文A往左移1要绕到哪个字母?

密钥大小写不改变偏移,明文大小写跟密文一致。

提示 2:用余数循环取密钥

key长为k,shift把密钥字母转成0到25:

int d = shift(key[i % k]);

为什么不能直接用key[i]?先用长度2的密钥配长度5的密文试。

提示 3:解密减偏移,越界绕回

base是当前密文大小写对应的A或a:

ans += (char)(base + (cipher[i] - base - d + 26) % 26);

先手算最左端减1的情况,检查加26的作用。

知识补充

密钥长度不超过 \(100\),密文长度不超过 \(1000\),输入只含英文字母。

i % k 使密钥下标循环回到开头。

这里的条件表达式 条件 ? 值一 : 值二 用于选择大写或小写起点。

常见错误

密钥大小写不影响偏移,明文大小写跟随密文。

求余前加 \(26\),避免负余数导致字符越界。

自测

输入:CompleteVictory Yvqgpxaimmklongnzfwpvxmniytm → 预期输出:Wherethereisawillthereisaway

输入:abc Hfnlpyosnd → 预期输出:Helloworld

输入:B Aa → 预期输出:Zz

自己试试:自己选一个带大小写字母的密文,用同一个密钥解密。

只改变密钥的大小写,结果应不应该变?

参考代码
#include <bits/stdc++.h>

using namespace std;

int shift(char c) {
    if (c >= 'a' && c <= 'z') return c - 'a';

    return c - 'A';
}

int main() {
    string key, cipher;
    cin >> key >> cipher;

    string ans;
    int k = (int)key.size();
    for (int i = 0, len = cipher.size(); i < len; i++) {
        int d = shift(key[i % k]);
        char base = cipher[i] >= 'a' && cipher[i] <= 'z' ? 'a' : 'A';
        ans += (char)(base + (cipher[i] - base - d + 26) % 26);
    }

    cout << ans;

    return 0;
}

1403:素数对

提示 1:相差2就不必再枚举第二个

较小数i确定,较大数是i加2,两个都不能超过n。

提示 2:复用两次质数判断

先写好is_prime,主函数循环:

for (int i = 2; i + 2 <= n; i++) {
    // 检查i与i+2是否都为质数
}

找到时输出并做标记,但不要break,3与5、5与7都应保留。

提示 3:最后才决定是否empty

标记初值false,有一对就设true;循环结束仍为false,才输出empty。

知识补充

本题 \(n\le10000\),数组容量要包含下标 \(10000\)。

筛法中的 true 约定为“不是质数”,所以要把 \(0\)、\(1\) 单独标记。

常见错误

两个数都必须不超过 \(n\)。

\(3,5\) 与 \(5,7\) 是不同的两对,不能跳过共享的质数。

自测

输入:7,预期输出:

3 5
5 7

输入:4 → 预期输出:empty

方法一:调用质数判断函数
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    int n;
    cin >> n;

    bool found = false;
    for (int i = 2; i + 2 <= n; i++) {
        if (is_prime(i) && is_prime(i + 2)) {
            cout << i << ' ' << i + 2 << '\n';
            found = true;
        }
    }

    if (!found) cout << "empty";

    return 0;
}
方法二:筛法预处理
#include <bits/stdc++.h>

using namespace std;

bool st[10010];

void get_primes(int n) {
    st[0] = st[1] = true;
    for (int i = 2; i <= n; i++) {
        if (st[i]) continue;
        for (int j = i + i; j <= n; j += i) st[j] = true;
    }
}

int main() {
    int n;
    cin >> n;

    get_primes(n);
    bool found = false;
    for (int i = 2; i + 2 <= n; i++) {
        if (!st[i] && !st[i + 2]) {
            cout << i << ' ' << i + 2 << '\n';
            found = true;
        }
    }

    if (!found) cout << "empty";

    return 0;
}

1404:我家的门牌号

提示 1:先假设有4家

门牌是1、2、3、4,总和为10。

假设我家是3号,其余各家合计7,再减去我家门牌号的两倍,得到\(7-2\times3=1\)。

先按题意算一次,不急着把式子合并。

提示 2:户数和门牌号,分两层试

沿方法一写:外层用y枚举总户数,内层用x枚举我家门牌号,从1试到y。

sum记录1到y的门牌总和,y每增加1,就给sum加上这个新门牌号。

固定一组x、y后,先算其余各家的总和:

int other = sum - x;

接着检查other - 2 * x是否等于输入的n,把这句判断补进去。

提示 3:找到后,两层都要停

题目保证唯一解,找到就按“我家门牌号、总户数”的顺序输出。

这里写在main中,输出后用return 0;结束程序;只写break会退出哪一层?

知识补充:为什么不需要很大的范围

若有450家,门牌总和为101475,即使我家是最大的450号,差值也有\(101475-3\times450=100125\)。

户数继续增加,这个最小差值还会增加,已超过题目的\(n<100000\)。

所以合法解的总户数不到450,两层枚举最多检查约十万次,int也足够。

常见错误

sum包含我家的门牌号,先减掉一次,才是“其余各家”。

内层的x只能从1到y,不能试一个胡同里不存在的门牌号。

sum在外层每增加一家时更新,别在尝试每个x时重复累加。

自测

输入:100 → 预期输出:12 16

输入:1 → 预期输出:3 4

输入:99997 → 预期输出:193 448

自己试试:用输出的两个数,还原“门牌总和、其余各家总和、减去两倍我家”的过程。

看最后能否回到输入的n。

方法一:枚举户数和我家门牌号

先用两层循环按题意检查,不需要先反求x。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    cin >> n;

    int sum = 0;
    for (int y = 1; ; y++) {
        sum += y;
        for (int x = 1; x <= y; x++) {
            int other = sum - x;
            if (other - 2 * x == n) {
                cout << x << ' ' << y;

                return 0;
            }
        }
    }
}
方法二:列式后只枚举户数(拓展)

sum - x - 2 * x = n,合并后是sum - 3 * x = n。

因此固定y后,x = (sum - n) / 3,不用再试每个门牌号。

要先确认能被3整除,再检查x在1到y之间;下面用t保存sum - n。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;
    cin >> n;

    for (int y = 1; ; y++) {
        int sum = y * (y + 1) / 2;
        int t = sum - n;
        if (t % 3 != 0) continue;
        int x = t / 3;
        if (x >= 1 && x <= y) {
            cout << x << ' ' << y;
            break;
        }
    }

    return 0;
}

1405:质数的和与积

提示 1:和固定,但两数还必须是质数

和为10时,2与8虽然能相加到10,能用吗?

提示 2:确定a,再检查剩下的数

输入和s,ans从0开始,a从2试到s / 2:

if (is_prime(a) && is_prime(s - a))
    ans = max(ans, a * (s - a));

自己补循环;为什么要允许两数相等,例如2与2?

知识补充

\(s\le10000\),最大乘积不超过 \(25000000\),int 足够。

常见错误

两个质数可以相同,例如 \(4=2+2\)。

本题保证有解,不需要自定义无解输出。

自测

输入:50 → 预期输出:589

输入:4 → 预期输出:4

输入:10 → 预期输出:25

参考代码
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    int s;
    cin >> s;

    int ans = 0;
    for (int a = 2; a <= s / 2; a++)
        if (is_prime(a) && is_prime(s - a)) ans = max(ans, a * (s - a));

    cout << ans;

    return 0;
}

1406:单词替换

提示 1:先分词,不直接替换片段

句子cat scatter cat,只换独立单词cat,scatter里面要动吗?

提示 2:找到词尾再比较

s是原句,i指词首,j从i走到空格或末尾,目标为a,新词为b:

string word = s.substr(i, j - i);
if (word == a) cout << b;
else cout << word;

输出词后处理分隔空格,再移到下一词;用只有一个词的句子检查末尾。

知识补充

题面规定单词间恰好一个空格,三行输入首尾没有空格。

这里先整行读取句子,再读两个单词。

常见错误

You 和 you 不相同。

循环先检查下标,再访问字符,最后一个单词也要处理。

自测

输入:

You want someone to help you
You
I

预期输出:I want someone to help you

输入:

cat scatter cat
cat
dog

预期输出:dog scatter dog

输入:

a
a
b

预期输出:b

自己试试:自己造一句话,让目标词既独立出现,也藏在别的单词里。

只改一个独立单词的大小写,替换结果应怎样变化?

参考代码
#include <bits/stdc++.h>

using namespace std;

int main() {
    string s, a, b;
    getline(cin, s);
    cin >> a >> b;

    for (int i = 0, len = s.size(); i < len; ) {
        int j = i;
        while (j < len && s[j] != ' ') j++;
        string word = s.substr(i, j - i);
        if (word == a) cout << b;
        else cout << word;
        if (j < len) cout << ' ';
        i = j + 1;
    }

    return 0;
}

1407:笨小猴

提示 1:没出现的字母不参加最少次数

error里e、r、o各出现几次?

其他字母的0次不能拿来当最少。

提示 2:用计数格统计后再找两端

全局cnt初值0,逐个小写字符:

cnt[s[i] - 'a']++;

遍历26格时跳过0,更新maxn、minn;然后把差交给is_prime判断。

提示 3:不是质数时输出的不是差

差为0或1都不是质数,第二行要输出0。

分别用error、a检查两类输出文字。

常见错误

差为 \(0\) 或 \(1\) 时都不是质数。

不是幸运单词时,第二行输出 \(0\),不是直接输出差值。

自测

输入:error,预期输出:

Lucky Word
2

输入:olympic,预期输出:

No Answer
0

输入:a,预期输出:

No Answer
0
参考代码
#include <bits/stdc++.h>

using namespace std;

int cnt[26];

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    string s;
    cin >> s;

    for (int i = 0, len = s.size(); i < len; i++) cnt[s[i] - 'a']++;
    int maxn = 0, minn = 100;
    for (int i = 0; i < 26; i++) {
        if (cnt[i] == 0) continue;
        maxn = max(maxn, cnt[i]);
        minn = min(minn, cnt[i]);
    }

    int d = maxn - minn;
    if (is_prime(d)) cout << "Lucky Word" << '\n' << d;
    else cout << "No Answer" << '\n' << 0;

    return 0;
}

1408:系数回文数的个数

提示 1:范围与结果都和前题不同

只检查从11到n的数,这次输出个数而不是名单。

提示 2:两个函数给出两道检查

cnt从0开始,已有is_prime和reverse_num:

if (is_prime(i) && reverse_num(i) == i) cnt++;

放进11到n的循环;别把单个数字的质数也加进去。

知识补充

输入满足 \(11<n<1000\)。

标题沿用原页,实际统计对象以题面为准:既是质数又是回文数。

常见错误

不能把个位质数也算进去。

不要用“跳过前几个质数”代替清楚的数值范围。

自测

输入:23 → 预期输出:1

输入:12 → 预期输出:1

输入:999 → 预期输出:16

参考代码
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int reverse_num(int x) {
    int t = 0;
    while (x) {
        t = t * 10 + x % 10;
        x /= 10;
    }

    return t;
}

int main() {
    int n;
    cin >> n;

    int cnt = 0;
    for (int i = 11; i <= n; i++)
        if (is_prime(i) && reverse_num(i) == i) cnt++;

    cout << cnt;

    return 0;
}

1409:判决素数个数

提示 1:两个端点可能反着给

输入10和2时,与输入2和10的区间是不是一样?

提示 2:先排端点,再统计
if (x > y) swap(x, y);

cnt从0,枚举x到y,用is_prime通过一次加1;端点为1时判断函数能否排除它?

知识补充

两种方法都覆盖 \(1\le X,Y\le10^5\)。

筛法先处理到较大端点,之后每个数只查一次标记。

方法三保留线性筛的写法,每个合数只由最小质因子筛掉一次,可以学过筛法后再看。

常见错误

两个端点都包含,\(1\) 不能计为质数。

数组下标必须小于容量,不能对容量本身做筛选。

自测

输入:1 100 → 预期输出:25

输入:10 2 → 预期输出:4

输入:1 1 → 预期输出:0

输入:99991 100000 → 预期输出:1

自己试试:自己选两个不同的合法端点,先数出区间里的质数。

只交换输入顺序,答案应不应该变?

方法一:逐个试除判断
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int main() {
    int x, y;
    cin >> x >> y;
    if (x > y) swap(x, y);

    int cnt = 0;
    for (int i = x; i <= y; i++)
        if (is_prime(i)) cnt++;

    cout << cnt;

    return 0;
}
方法二:筛法预处理
#include <bits/stdc++.h>

using namespace std;

const int N = 100000 + 10;
bool st[N];

void get_primes(int n) {
    st[0] = st[1] = true;
    for (int i = 2; i <= n; i++) {
        if (st[i]) continue;
        for (int j = i + i; j <= n; j += i) st[j] = true;
    }
}

int main() {
    int x, y;
    cin >> x >> y;
    if (x > y) swap(x, y);

    get_primes(y);
    int cnt = 0;
    for (int i = x; i <= y; i++)
        if (!st[i]) cnt++;

    cout << cnt;

    return 0;
}
方法三:线性筛预处理(拓展)
#include <bits/stdc++.h>

using namespace std;

const int N = 100000 + 10;
bool st[N];
int primes[N], idx;

void get_primes(int n) {
    st[0] = st[1] = true;
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[idx++] = i;
        for (int j = 0; j < idx && primes[j] <= n / i; j++) {
            st[i * primes[j]] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

int main() {
    int x, y;
    cin >> x >> y;
    if (x > y) swap(x, y);

    get_primes(y);
    int cnt = 0;
    for (int i = x; i <= y; i++)
        if (!st[i]) cnt++;

    cout << cnt;

    return 0;
}

1410:最大质因子序列

提示 1:一个因数可能要除很多遍

把72按从小到大试除,2要除几次才除尽?

用函数largest_factor单独求一个数的最大质因子。

提示 2:除掉当前因数再接着试

函数参数x是副本,候选i从2开始,ans初值1:

while (x % i == 0) {
    ans = i;
    x /= i;
}

i只需试到i <= x / i;最后x还大于1时,它也是剩下的质因子。

提示 3:主函数负责范围和逗号

逐个调用函数;第二项起才在前面加逗号,别在末尾补逗号。

知识补充

本题 \(1<m<n\le5000\)。

质数输入时没有小因子能除掉它,最后返回它本身。

常见错误

不能只除一次,例如 \(8\) 要连续除去三个 \(2\)。

逗号只放在两项之间,这里需要分隔控制。

自测

输入:5 10 → 预期输出:5,3,7,2,3,5

输入:2 4 → 预期输出:2,3,2

输入:48 49 → 预期输出:3,7

参考代码
#include <bits/stdc++.h>

using namespace std;

int largest_factor(int x) {
    int ans = 1;
    for (int i = 2; i <= x / i; i++) {
        while (x % i == 0) {
            ans = i;
            x /= i;
        }
    }
    if (x > 1) ans = x;

    return ans;
}

int main() {
    int m, n;
    cin >> m >> n;

    for (int i = m; i <= n; i++) {
        if (i > m) cout << ',';
        cout << largest_factor(i);
    }

    return 0;
}

1411:区间内的真素数

提示 1:反过来仍要是质数

13在区间内,反序31不在区间内,是否影响13成为真素数?

这里范围管的是原来的数,倒过来的数,只检查它是不是质数。

提示 2:拼起两个现成函数

i在m到n之间:

if (is_prime(i) && is_prime(reverse_num(i))) {
    // 输出i,并记录已经找到结果
}

保留逗号分隔控制;循环全部结束仍没找到才输出No。

知识补充

原数不超过 \(100000\),反序结果也不会超过 \(100000\),筛表可覆盖两者。

筛表中的 true 表示“不是质数”,包含 \(0\) 和 \(1\)。

常见错误

反序时可以丢掉前导零,但 \(1\) 仍不是质数。

没有符合项时输出 No,有结果时用逗号分隔。

自测

输入:10 35 → 预期输出:11,13,17,31

输入:1 1 → 预期输出:No

输入:13 13 → 预期输出:13

输入:100000 100000 → 预期输出:No

方法一:逐个试除判断
#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++)
        if (x % i == 0) return false;

    return true;
}

int reverse_num(int x) {
    int t = 0;
    while (x) {
        t = t * 10 + x % 10;
        x /= 10;
    }

    return t;
}

int main() {
    int m, n;
    cin >> m >> n;

    bool found = false;
    for (int i = m; i <= n; i++) {
        if (is_prime(i) && is_prime(reverse_num(i))) {
            if (found) cout << ',';
            cout << i;
            found = true;
        }
    }

    if (!found) cout << "No";

    return 0;
}
方法二:筛法预处理
#include <bits/stdc++.h>

using namespace std;

bool st[100000 + 10];

void get_primes(int n) {
    st[0] = st[1] = true;
    for (int i = 2; i <= n; i++) {
        if (st[i]) continue;
        for (int j = i + i; j <= n; j += i) st[j] = true;
    }
}

int reverse_num(int x) {
    int t = 0;
    while (x) {
        t = t * 10 + x % 10;
        x /= 10;
    }

    return t;
}

int main() {
    int m, n;
    cin >> m >> n;

    get_primes(100000);
    bool found = false;
    for (int i = m; i <= n; i++) {
        if (!st[i] && !st[reverse_num(i)]) {
            if (found) cout << ',';
            cout << i;
            found = true;
        }
    }

    if (!found) cout << "No";

    return 0;
}

1412:二进制分类

提示 1:只数有效二进制位

10的二进制为1010,1与0一样多,按题面属于哪类?

不能把整数存储时前面补的零也数进去。

提示 2:除2的余数就是当前一位

在check(x)中one、zero从0开始:

while (x) {
    if (x % 2 == 1) one++;
    else zero++;
    x /= 2;
}

函数返回one > zero,主函数枚举1到1000分别计入两类。

常见错误

两种数字一样多时归入 B 类。

不要把固定整数类型里补齐的前导零也统计进去。

自测

无需输入,预期输出:538 462

方法一:除以 2 统计数位
#include <bits/stdc++.h>

using namespace std;

bool check(int x) {
    int one = 0, zero = 0;
    while (x) {
        if (x % 2 == 1) one++;
        else zero++;
        x /= 2;
    }

    return one > zero;
}

int main() {
    int a = 0, b = 0;
    for (int i = 1; i <= 1000; i++) {
        if (check(i)) a++;
        else b++;
    }

    cout << a << ' ' << b;

    return 0;
}
方法二:按位检查(拓展)
#include <bits/stdc++.h>

using namespace std;

bool check(int x) {
    int one = 0, bits = 0;
    for (int j = 0; (1 << j) <= x; j++) {
        if (x & (1 << j)) one++;
        bits++;
    }

    return one > bits - one;
}

int main() {
    int a = 0, b = 0;
    for (int i = 1; i <= 1000; i++) {
        if (check(i)) a++;
        else b++;
    }

    cout << a << ' ' << b;

    return 0;
}

1413:确定进制

提示 1:纸上的42不一定是四十二

在13进制里,42表示4乘13加2,等于多少?

先别直接用输入的p、q、r做十进制乘法。

提示 2:从低位累加各位贡献

value(x, base)中sum从0、weight从1开始,每次取d = x % 10:

if (d >= base) return -1;
sum += d * weight;
weight *= base;
x /= 10;

比如二进制里能不能出现数字2?先用这个想一下。

函数返回值用long long。

提示 3:从小进制试,避免乘积溢出

主函数从2到40试base,三次value得到a、b、c,非法值先跳过。

if (c % a == 0 && c / a == b) {
    ans = base;
    break;
}

三个转换值为正,这样比较不必计算可能溢出的a乘b。

知识补充

输入 \(p,q,r\) 在 \(1\) 到 \(10^6\) 之间,它们的十进制写法用于保存数位。

value 返回 -1,表示当前进制下存在非法数位。

转换结果为正数,可以用 c % a == 0 && c / a == b 代替乘法等式,避免乘积溢出。

拿 \(42\)、进制 \(13\) 跟一遍 x、weight、sum 的变化,再关上提示自己写。

常见错误

某一位不小于进制时,这个进制不合法。

转换后的数能放进 long long,它们的乘积却不一定能。

自测

输入:6 9 42 → 预期输出:13

输入:11 11 121 → 预期输出:3

输入:1 1 1 → 预期输出:2

输入:1 1 2 → 预期输出:0

输入:1000000 1000000 1 → 预期输出:0

自己试试:自己选一个小进制,写出一组只含数字的乘法等式。

这是否就是最小可行进制,还要怎样检查?

参考代码
#include <bits/stdc++.h>

using namespace std;

long long value(int x, int base) {
    long long sum = 0, weight = 1;
    do {
        int d = x % 10;
        if (d >= base) return -1;
        sum += d * weight;
        weight *= base;
        x /= 10;
    } while (x);

    return sum;
}

int main() {
    int p, q, r;
    cin >> p >> q >> r;

    int ans = 0;
    for (int base = 2; base <= 40; base++) {
        long long a = value(p, base), b = value(q, base), c = value(r, base);
        if (a == -1 || b == -1 || c == -1) continue;
        if (c % a == 0 && c / a == b) {
            ans = base;
            break;
        }
    }

    cout << ans;

    return 0;
}

第二节 递归算法

1158:求 1+2+3+…

提示 1:先把最后一个数拿出来

1加到3,可以分成“1加到2的和,再加3”。

函数f(n)负责返回1加到n的和,先写最小情况:

if (n == 1) return 1;
提示 2:调用下去,还要一层层返回

剩下的情况,就写return f(n - 1) + n;。

求f(3)时,先等f(2);f(2)又先等f(1)。

f(1)返回1后,f(2)得到3,f(3)再得到6。

f(4)你来走一遍,每一层只加回自己的那个数,别一下跳到答案。

知识补充

函数返回值用 long long,避免总和先在 int 中溢出。

题面未给出 \(n\) 上界,递归深度随 \(n\) 增长,不能把这种教学写法当作任意大输入的通用实现。

常见错误

没有终止条件,就会一直往下调用。

返回的是子问题的结果加 \(n\),不能忘掉 return。

自测

输入:5 → 预期输出:15

输入:1 → 预期输出:1

输入:100 → 预期输出:5050

参考代码
#include <bits/stdc++.h>

using namespace std;

long long f(int n) {
    if (n == 1) return 1;

    return f(n - 1) + n;
}

int main() {
    int n;
    cin >> n;

    cout << f(n);

    return 0;
}

1159:斐波那契数列

提示 1:先对齐项号

第1项是0,第2项是1;先在纸上写出前五项。

函数f(n)返回的是第n项的值,不是前n项的和。

提示 2:两项能直接回答,其余接着问

在long long f(int n)中先写:

if (n == 1) return 0;
if (n == 2) return 1;

其余返回f(n - 1) + f(n - 2);自己用第3项检查开头是否接对。

提示 3:同一个问题被问了几次

求f(5)要算f(4)和f(3),而f(4)又要算一次f(3)。

你看,f(3)又算了一遍。

在草稿上把这两处圈一下,这就是重复计算。

先拿前几项,把递归走一遍。

记忆化先不着急,等你看出哪儿重复了,再往下看。

知识补充

直接递归会反复计算相同项,项号变大时会很慢。

拓展方法把算过的项存起来,每项只展开一次,可以以后再看。

memo.assign(n + 1, -1) 分配存储空间并填入 -1,这里约定它表示“尚未计算”。

题面未给项号上界;long long 可容纳到第 \(93\) 项,第 \(94\) 项起需更大的整数表示,不表示直接递归能快速算到第 \(93\) 项。

常见错误

第 \(1\) 项是 \(0\),不是 \(1\)。

不能用零表示“没有算过”,因为合法答案本来就可能是零。

自测

输入:3 → 预期输出:1

输入:1 → 预期输出:0

输入:2 → 预期输出:1

输入:30 → 预期输出:514229

自己试试:自己选相邻的三个小项号,先手算结果。

它们应满足什么关系,能帮你检查起点有没有写错?

方法一:按定义递归

先用小的项号观察调用过程;这份写法有重复计算,数值增大后会很慢。

#include <bits/stdc++.h>

using namespace std;

long long f(int n) {
    if (n == 1) return 0;
    if (n == 2) return 1;

    return f(n - 1) + f(n - 2);
}

int main() {
    int n;
    cin >> n;

    cout << f(n);

    return 0;
}
方法二:记忆化(拓展)
#include <bits/stdc++.h>

using namespace std;

vector<long long> memo;

long long f(int n) {
    if (n == 1) return 0;
    if (n == 2) return 1;
    if (memo[n] != -1) return memo[n];

    memo[n] = f(n - 1) + f(n - 2);
    return memo[n];
}

int main() {
    int n;
    cin >> n;
    memo.assign(n + 1, -1);

    cout << f(n);

    return 0;
}

1160:倒数数

提示 1:先输出个位会怎样

123先输出3,再处理12,会得到什么顺序?

函数只负责输出,所以返回类型用void。

提示 2:数位处理完就结束当前调用
if (n == 0) return;
cout << n % 10;
f(n / 10);

把片段放进void f(long long n);输入7时,一共输出几次?

知识补充

void 函数不返回计算值,这里用它完成输出。

return; 表示结束当前这一次调用。

常见错误

本题给定个位不为零,不需要自己添加保留整数前导零的规则。

输出放在递归调用前后,顺序不同。

自测

输入:123 → 预期输出:321

输入:7 → 预期输出:7

输入:10203 → 预期输出:30201

参考代码
#include <bits/stdc++.h>

using namespace std;

void f(long long n) {
    if (n == 0) return;
    cout << n % 10;
    f(n / 10);
}

int main() {
    long long n;
    cin >> n;

    f(n);

    return 0;
}

1161:转进制

提示 1:余数先得到的是低位

把10反复除以2,余数顺序是0、1、0、1,直接按这个顺序输出对吗?

提示 2:先递归处理商,再输出余数

进制保存在base,digit为"0123456789ABCDEF":

if (x == 0) return;
convert(x / base);
cout << digit[x % base];

放进convert函数;从10到0,再返回时逐个写出输出顺序。

提示 3:输入零与递归结束分开

主函数n为0时直接输出一个0;否则调用convert。

为何不能在函数每次x等于0时都输出0?

知识补充

进制按定义至少为 \(2\),本题转换到不超过 \(16\) 的进制。

digit 的下标是余数,对应要输出的数字或字母。

常见错误

十六进制中 \(10\) 到 \(15\) 要输出 A 到 F。

不能把零和“递归已经没有更多高位”混成一种输出。

自测

输入:31 16 → 预期输出:1F

输入:10 2 → 预期输出:1010

输入:0 16 → 预期输出:0

自己试试:自己选一个小整数,先把它写成二进制和十六进制。

分别按位还原成十进制,是否回到原数?

参考代码
#include <bits/stdc++.h>

using namespace std;

int base;
string digit = "0123456789ABCDEF";

void convert(int x) {
    if (x == 0) return;
    convert(x / base);
    cout << digit[x % base];
}

int main() {
    int n;
    cin >> n >> base;

    if (n == 0) cout << 0;
    else convert(n);

    return 0;
}

1162:字符串逆序

提示 1:先把结束符留在字符串外
getline(cin, s, '!');

读到!停止,空格保留,但!没有存入;最后有效下标是len减几?

提示 2:下标每次少一个

在void f(const string &s, int pos)中先处理:

if (pos < 0) return;

再输出s[pos]并调用f(s, pos - 1),先检查再访问;主函数从最后一个下标调用。

知识补充

getline(cin, s, '!') 读到半角 ! 为止,空格也会保留,但 ! 不存入字符串。

因此最后一个字符的下标是 len - 1,不用再跳过结束符。

const string &s 让递归共用原字符串,不在每次调用时复制,也不在函数中修改它。

递归深度随字符数增长,题面未给长度上界,仍需注意调用层数。

常见错误

cin >> s 遇到空格就停止,输入 a b! 时不能读到完整内容。

当前读法没有把 ! 存进字符串,再从 len - 2 开始会漏掉最后一个字符。

先判断 pos < 0,再访问 s[pos],避免下标越界。

自测

输入:abc! → 预期输出:cba

输入:a b! → 预期输出:b a

输入:x! → 预期输出:x

自己试试:自己写一串带空格、以 ! 结束的字符,先在纸上反序。

只在结束符前增加一个空格,输出应在哪个位置多一个空格?

参考代码
#include <bits/stdc++.h>

using namespace std;

void f(const string &s, int pos) {
    if (pos < 0) return;

    cout << s[pos];
    f(s, pos - 1);
}

int main() {
    string s;
    getline(cin, s, '!');

    int len = (int)s.size();
    f(s, len - 1);

    return 0;
}

1163:阿克曼 (Ackermann) 函数

提示 1:先挑能直接算的情况

按定义,\(A(0,2)=3\)。

再算\(A(1,0)=A(0,1)\),用第一条定义得到结果。

提示 2:先把这两种情况写好

放在ack函数的前面:

if (m == 0) return n + 1;
if (n == 0) return ack(m - 1, 1);

用刚才手算的两组数检查,再写后面的情况。

提示 3:先算里面,拿结果交给外面

\(A(1,1)=A(0,A(1,0))\),里面的\(A(1,0)\)等于2,所以外面变成\(A(0,2)\)。

代码对应ack(m - 1, ack(m, n - 1))。

再算一下\(A(1,2)\)。

留意啊,里面算出的结果,是交给外面的函数接着算,不是两个答案相加。

知识补充

题目限定 \(0\le m\le3\)、\(0\le n\le10\)。

\[ A(m,n)=\begin{cases} n+1,&m=0,\\ A(m-1,1),&m>0,\ n=0,\\ A(m-1,A(m,n-1)),&m>0,\ n>0. \end{cases} \]

手算 \(A(1,1)\) 时,先写出最里面的 \(A(1,0)\),不要一口气把所有层都展开。

常见错误

别把嵌套调用改成两个函数值相加。

\(m=0\) 时直接返回,不再进入其他分支。

自测

输入:2 3 → 预期输出:9

输入:0 10 → 预期输出:11

输入:3 0 → 预期输出:5

输入:3 4 → 预期输出:125

参考代码
#include <bits/stdc++.h>

using namespace std;

int ack(int m, int n) {
    if (m == 0) return n + 1;
    if (n == 0) return ack(m - 1, 1);

    return ack(m - 1, ack(m, n - 1));
}

int main() {
    int m, n;
    cin >> m >> n;

    cout << ack(m, n);

    return 0;
}

1164:digit 函数

提示 1:去掉个位后,位置也变了

31859从右数第3位是8,去掉9以后,它是新数的第几位?

提示 2:两个参数一起缩小

在digit(n,k)中先写:

if (k == 1) return n % 10;

其余情况返回digit(n / 10, k - 1);只减少k而不除n,会一直看到哪一位?

常见错误

这里是从右数,不是从左数。

\(n\) 和 \(k\) 要一起更新,只改其中一个会找错位置。

自测

输入:31859 3 → 预期输出:8

输入:10203 2 → 预期输出:0

输入:7 1 → 预期输出:7

自己试试:自己选一个中间带零的正整数,再指定这个零的位置。

把 \(k\) 增加或减少 \(1\),预期应取出哪一个数字?

参考代码
#include <bits/stdc++.h>

using namespace std;

int digit(long long n, int k) {
    if (k == 1) return n % 10;

    return digit(n / 10, k - 1);
}

int main() {
    long long n;
    int k;
    cin >> n >> k;

    cout << digit(n, k);

    return 0;
}

1165:Hermite 多项式

提示 1:先只算第二阶

固定\(x=2\),先算出\(h_0(2)=1\)、\(h_1(2)=4\)。

代入题面的下一条公式,\(h_2(2)=2\times2\times4-2\times1\times1=14\)。

提示 2:先写能直接回答的两阶

在double h(int n, double x)中:

if (n == 0) return 1;
if (n == 1) return 2 * x;

其余把公式里的前两阶换成h(n - 1, x)、h(n - 2, x)。

系数仍是2 * (n - 1),自己补完整表达式,再用刚才的14核对。

提示 3:每层只改变阶数

求\(h_3(2)\)时,仍用同一个\(x=2\),只去求前两阶。

若研究后面的记忆化,先圈出重复求过的阶数;用vis记是否算过,不能拿函数值0作标记。

知识补充

题目给出的定义是

\[ h_n(x)=\begin{cases} 1,&n=0,\\ 2x,&n=1,\\ 2xh_{n-1}(x)-2(n-1)h_{n-2}(x),&n>1. \end{cases} \]

记忆化把已经算过的值保存下来,再次遇到相同问题时直接取用,可以以后再看。

同一次计算中 \(x\) 不变,所以只需按阶数 \(n\) 保存结果。

vis[n] 记录是否计算过,memo[n] 才是函数值。

题面输出说明未写位数,但原题样例为 4.00,这里沿用两位小数格式。

辅助数组放在全局,vis 默认是 false,表示还没有计算。

本实现预留 \(100010\) 个位置,可使用下标 \(0\) 到 \(100009\),这不是题面给出的阶数上界。

题面未给阶数上界,使用时仍需检查数组容量、递归深度和浮点数范围。

常见错误

公式中的系数是 \(2(n-1)\),不是 \(2n\)。

函数值可能为零或负数,要用独立标记表示“算过”。

自测

输入:1 2 → 预期输出:4.00

输入:2 1 → 预期输出:2.00

输入:3 2 → 预期输出:40.00

输入:4 1 → 预期输出:-20.00

方法一:按定义递归

先用小的阶数观察调用过程;这份写法有重复计算,数值增大后会很慢。

#include <bits/stdc++.h>

using namespace std;

double h(int n, double x) {
    if (n == 0) return 1;
    if (n == 1) return 2 * x;

    return 2 * x * h(n - 1, x) - 2 * (n - 1) * h(n - 2, x);
}

int main() {
    int n;
    double x;
    cin >> n >> x;

    printf("%.2lf", h(n, x));

    return 0;
}
方法二:记忆化(拓展)
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

double memo[N];
bool vis[N];

double h(int n, double x) {
    if (n == 0) return 1;
    if (n == 1) return 2 * x;
    if (vis[n]) return memo[n];

    memo[n] = 2 * x * h(n - 1, x) - 2 * (n - 1) * h(n - 2, x);
    vis[n] = true;
    return memo[n];
}

int main() {
    int n;
    double x;
    cin >> n >> x;

    printf("%.2lf", h(n, x));

    return 0;
}

1166:求 f (x,n)

提示 1:从最外层根号往里看

先写n为2的式子,最外面是根号下2加上哪一个更小结果?

提示 2:最里层直接计算

在f(x,n)中:

if (n == 1) return sqrt(1 + x);

其他层用return sqrt(n + f(x, n - 1));;每层的n会变化,x在哪一层真正参与加法?

知识补充

题面中的 \(4.2,10\) 与 \(2.5,15\) 是两组计算示例,程序仍应读取输入。

根式中的每个 \(n\) 对应当前递归层,不是一直使用最初的项号。

常见错误

读入顺序是 \(x,n\)。

不要把 \(x\) 加到每一层,只有最里层含它。

自测

输入:4.2 10 → 预期输出:3.68

输入:2.5 15 → 预期输出:4.39

输入:3 1 → 预期输出:2.00

参考代码
#include <bits/stdc++.h>

using namespace std;

double f(double x, int n) {
    if (n == 1) return sqrt(1 + x);

    return sqrt(n + f(x, n - 1));
}

int main() {
    double x;
    int n;
    cin >> x >> n;

    printf("%.2lf", f(x, n));

    return 0;
}

1167:再求 f (x,n)

提示 1:把分母看成一个整体

先用x为1写出n为1、n为2的连分式,不能把它拆成两项相加。

提示 2:先求内层,再放进分母

在f(x,n)中:

if (n == 1) return x / (1 + x);

其他层返回x / (n + f(x, n - 1));x为1、n为2时,先算二分之一,再代入外层。

知识补充

原题图片中的连分式可写成

\[ f(x,1)=\frac{x}{1+x},\qquad f(x,n)=\frac{x}{n+f(x,n-1)}\quad(n>1). \]

例如 \(x=1,n=2\) 时,先算内层 \(\frac12\),再算 \(\frac1{2+\frac12}=0.4\)。

常见错误

每一层的分子都是 \(x\),分母是当前 \(n\) 加上内层结果。

用 double 计算,按原题样例保留两位小数。

自测

输入:1 2 → 预期输出:0.40

输入:2 2 → 预期输出:0.75

输入:1 1 → 预期输出:0.50

参考代码
#include <bits/stdc++.h>

using namespace std;

double f(double x, int n) {
    if (n == 1) return x / (1 + x);

    return x / (n + f(x, n - 1));
}

int main() {
    double x;
    int n;
    cin >> x >> n;

    printf("%.2lf", f(x, n));

    return 0;
}