CSUOJ 题目选讲配套资料

写在前面

C 语言是同汉语言般精美绝伦的造物。
————Dr.L.b.

6-M

已知以下三个信息:

  1. 出生月份和出生日子的最大公约数;
  2. 出生月份和出生日子的最小公倍数;
  3. 出生年份;

大致思路?

  • 年份已知,闰年平年确定。考虑暴力枚举每个月份和每个月份的每一天,检查是否存在满足条件 1、2 的日期即可。

怎么检查?

  • 辗转相除法求最大公约数:
复制
int gcd(int x, int y) {
return y ? gcd(y, x % y) : x;
}
  • 最小公倍数:

    lcm(a,b)=a×bgcd(a,b)

int lcm(int x, int y) {
return x / gcd(x, y) * y;
}

如何处理多组解?

  • 枚举过程中出现第一组解时把解记录下来,之后再枚举到解时检查之前有没有记录,如果记录了,则标记多解。
if (gcd(i, j) == x && lcm(i, j) == y) {
if (ans1) ans1 = -1; //如果 ans1 之前记录了一组解,则标记为多解
else ans1 = i, ans2 = j; //如果 ans1 之前没有记录解,则将这一组解 (i, j) 记录下来。
}

一些编写时降低编写难度的小细节:

  • 可以把平年、闰年每个月的天数,将月份作为数组下标,分别存入数组。判断平年闰年之后直接读取对应数组的天数即可。
  • 将求 gcdlcm 的部分作为函数独立编写。

完整代码

#include <stdio.h>
int t, n, x, y, z;
int kDays[2][13] = {{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
int Judge(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int gcd(int x_, int y_) {
return y_ ? gcd(y_, x_ % y_) : x_;
}
int lcm(int x_, int y_) {
return x_ / gcd(x_, y_) * y_;
}
int main() {
scanf("%d", &t);
for (int i = 1; i <= t; ++ i) {
scanf("%d%d%d", &x, &y, &z);
int k = Judge(z), ans1 = 0, ans2 = 0;
for (int i = 1; i <= 12; ++ i) {
for (int j = 1; j <= kDays[k][i]; ++ j) {
if (gcd(i, j) == x && lcm(i, j) == y) {
if (ans1) ans1 = -1;
else ans1 = i, ans2 = j;
}
}
}
printf("Case #%d: ", i);
if (ans1 == 0) printf("-1\n");
else if (ans1 == -1) printf("1\n");
else printf("%.4d/%.2d/%.2d\n", z, ans1, ans2);
}
return 0;
}

4-D

除了有点麻烦之外一点也不麻烦的题。

观察一下图形的结构:

  • 一共有 n 行:第一行和最后一行全部是 -,上下半部分各有 n22 行且除去 * 外上下对称。
  • 第一行和最后一行有 (n1)-
  • 对于从第二行开始上半部分,第 i\ 前面有 i1 个空格。
    \/ 之间有 (n1)2×i 个字符。第 1 行这些字符是空格,其他行这些字符是 *
  • 下半部分对称,倒数第 i\ 前面有 i1 个空格。
    \/ 之间有 (n1)2×i 个字符。但中间的字符为 *,其他字符为空格。

按照上述分析直接编程输出对应数量的对应字符即可。

#include <stdio.h>
#include <string.h>
int main() {
int T, n, i, j;
scanf("%d", &T);
while (T --) {
scanf("%d", &n);
//第一行的 -
for (i = 1; i < n; ++ i) printf("-");
printf("\n");
//第二行的 \ /
printf("\\"); //字符 '\',无法直接输出,需要使用转义字符
for (i = 2; i <= n - 2; ++ i) printf(" ");
printf("/\n");
//上半部分
for (i = 2; i <= n / 2 - 1; ++ i) {
for (j = 1; j <= i - 1; ++ j) printf(" "); //先输出空格
printf("\\");
for (j = i; j < n - 1 - i; ++ j) printf("*"); //填满 *
printf("/\n");
}
//下半部分,这里直接倒序枚举了 i,表示输出的是倒数第几行
for (i = n / 2 - 1; i; -- i) {
for (j = 1; j <= i - 1; ++ j) printf(" "); //先输出空格
printf("/");
for (j = n / 2 - 1 - i; j; -- j) printf(" ");
printf("*"); //中间的 *
for (j = n / 2 - 1 - i; j; -- j) printf(" ");
printf("\\\n");
}
//最后一行的 -
for (i = 1; i < n; ++ i) printf("-");
printf("\n\n");
}
}

6-L

全排列是什么?

  • 从一个有 n 个不同元素的集合 s 中取出 n 个元素按照一定顺序排列起来,即得 s 的一个全排列。
  • 全排列的方案数为 Ann=n!

怎么枚举 09 的全排列?

  • 一种最直接的方法是用 10 层循环嵌套,分别枚举全排列每一位的数。注意在枚举过程中判断是否有两个位置的数相同。这里给出枚举 02 的全排列的例子:
#include <stdio.h>
int main() {
int i, j, k, l, m, vis[3];
for (i = 0; i <= 2; ++ i) vis[i] = 0;
for (i = 0; i <= 2; ++ i) { //枚举第一位
vis[i] = 1; //标记第一位的数已经被使用过了 (be visited)
for (j = 0; j <= 2; ++ j) { //枚举第二位
if (vis[j]) continue; //如果第二位的数被使用过,则跳过
vis[j] = 1;
for (k = 0; k <= 2; ++ k) {
if (vis[k]) continue;
printf("%d%d%d\n", i, j, k);
}
vis[j] = 0;
}
vis[i] = 0;
}
return 0;
}
  • 但是写 10 层循环的人脑子肯定有病。怎么不用循环嵌套,又能枚举每一位呢?
  • 能嵌套的东西可不只有循环。
  • 考虑使用递归函数进行枚举:
#include <stdio.h>
int vis[10], ans[10];
//定义在函数外面的全局变量是会自动初始化为 0 的,这里没有给 vis 赋初始值 0
void Dfs(int length_) { //每次调用时枚举 ans 的第 length_ 个位置
int i;
if (length_ == 10) { //0~9 个位置都已经枚举完了,则输出
for (i = 0; i <= 9; ++ i) printf("%d", ans[i]); //输出枚举的结果 ans
printf("\n");
return ;
}
for (i = 0; i <= 9; ++ i) { //枚举填入这个位置的数
if (vis[i]) continue; //这个数在之前的函数调用里被使用过
vis[i] = 1; //标记
ans[length_] = i;
Dfs(length_ + 1); //在标记的基础上开始枚举下一位
vis[i] = 0; //解除标记
}
}
int main() {
Dfs(0);
return 0;
}
  • 这种写法叫做深度优先搜索(Depth-First Search, DFS)算法。在上述的搜索过程中,我们的函数调用过程好似一棵树,叫做搜索树:

    留个坑先。

    发现我们每次进行枚举时,都会一口气向下走到树的分支的末尾,然后再向上回溯,再向下走到末尾……不到达最深的地方不会回溯,因此叫“深度优先”。

回到本题

  • 考虑使用上述 DFS 算法来解决本题中枚举的问题。先随便写一份超级暴力的代码,直接把 abcdefghij 全部枚举出来,分别存入数组 s 的前五位和后五位,再检查是否符合条件:
#include <stdio.h>
#include <math.h>
int n, ans, s[10]; //s[0]~s[4] 存 abcde,s[5]~s[9] 存 fghij
int vis[10];
void judge() {
int x = 0, y = 0;
for (int i = 0; i < 5; ++ i) x = x * 10 + s[i]; //递推求 abcde
for (int i = 5; i < 10; ++ i) y = y * 10 + s[i]; //递推求 fghij
if (n * y == x) { //判断是否满足给出的条件,若满足则输出
++ ans; //记录解的个数
for (int i = 0; i < 5; ++ i) printf("%d", s[i]);
printf(" / ");
for (int i = 5; i < 10; ++ i) printf("%d", s[i]);
printf(" = %d\n", n);
}
}
void Dfs(int lth_) {
if (lth_ == 10) { //0~9 位置都已经被
judge();
return ;
}
for (int i = 0; i < 10; ++ i) {
if (vis[i]) continue;
vis[i] = 1;
s[lth_] = i;
Dfs(lth_ + 1);
vis[i] = 0;
}
}
int main() {
while (scanf("%d", &n) != EOF) {
if (!n) break;
ans = 0; //无解
Dfs(0);
if (!ans) printf("There are no solutions for %d.\n", n);
}
return 0;
}

然后发现时间超限。因为全排列共有 A1010=3,628,800 种,全枚举出来时间复杂度过高。如何优化?

  • 发现只需要枚举出 abcde 即可,因为每一组合法解的 abcdefghij 都是一一对应的,有 fghij=abcden
  • 则可以只枚举 abcde,再检查 abcde 能否整除 n,如果能则求得 fghij=abcden,再检查这两个数的每一位里有没有重复的数即可。

完整代码

#include <stdio.h>
#include <math.h>
int n, ans, s[10];
int vis[10];
void judge() {
int x = 0;
for (int i = 0; i < 5; ++ i) x = x * 10 + s[i]; //求 abcde
if (x % n != 0) return ; //abcde 不能整除 n
int y = x / n, flag = 0, y1 = y; //求得 fghij = abcde / n
int vis1[10];
for (int i = 0; i < 10; ++ i) vis1[i] = vis[i]; //将 abcde 中的数字进行标记
for (int i = 9; i >= 5; -- i) { //标记求得的 fghij 的每一位
if (vis1[y1 % 10] == 1) flag = 1; //发现有重复使用的数
s[i] = y1 % 10; //记录 fghij
vis1[y1 % 10] = 1;
y1 /= 10;
}
//有重复使用的数,不合法
if (flag == 1) {
return ;
}
//输出合法解
++ ans;
for (int i = 0; i < 5; ++ i) printf("%d", s[i]);
printf(" / ");
for (int i = 5; i < 10; ++ i) printf("%d", s[i]);
printf(" = %d\n", n);
}
void Dfs(int lth_) {
if (lth_ == 5) {
judge();
return ;
}
for (int i = 0; i < 10; ++ i) {
if (vis[i] == 1) continue;
vis[i] = 1;
s[lth_] = i;
Dfs(lth_ + 1);
vis[i] = 0;
}
}
int main() {
while (scanf("%d", &n) != EOF) {
if (!n) break;
ans = 0;
Dfs(0);
if (!ans) printf("There are no solutions for %d.\n", n);
printf("\n");
}
return 0;
}

6-H

怎么存每个学生的信息?

  • 昨天刚学的结构体:
struct Student {
int Chinese, Math, English, Sum;
char name[10 + 1];
} a[100 + 1]; //把学生信息读入到 a 数组中
  • 问题变为怎么给结构体排序。

如果不考虑按姓名字典序排序?

  • 只需要考虑每个人某成绩高低即可。

  • 时结构体数组排序和数列排序一样。

  • 先介绍两种简单的排序算法,以数列排序为例。

  • 选择排序!

    将数组 a 分为有序区 1i1 和无序区 in,每次从无序区中选出最大的数,并将它与位置 i 交换,使 i 位置有序。
    每轮循环都使有序区长度加一,显然需要选择 n 次最大值出来,总循环次数为 n22

#include <stdio.h>
int n, i, j, a[100001];
int Min(int x_, int y_) {
return x_ < y_ ? x_ : y_;
}
void Sort() {
//将数组 a 分为有序区 1~i-1 和无序区 i~n
for (i = 1; i <= n; ++ i) {
//每次从无序区中选出最小的数,并将它放在有序区的位置 i 上。
int min_a = i;
for (j = i; j <= n; ++ j) {
if (a[j] > a[min_a]) min_a = j;
}
int temp = a[min_a]; //和位置 i 交换,使 i 位置有序。
a[min_a] = a[i];
a[i] = temp;
}
//显然需要选择 n 次最小值出来,总循环次数为 (n^2) / 2
}
int main() {
scanf("%d", &n);
for (i = 1; i <= n; ++ i) scanf("%d", &a[i]);
Sort();
for (i = 1; i <= n; ++ i) printf("%d ", a[i]);
}
  • 冒泡排序!

    一共循环 n 次,每次循环从前往后比较相邻的两个数的大小关系,如果前一个数更小,则把它与后一个数交换。每一轮循环后都使最小的数,经过一次一次交换后被放到数列最后。

    整个过程好像水下的气泡慢慢飘向水面,从而得名冒泡排序。

#include <stdio.h>
int n, i, j, a[100001];
void Sort() {
for (i = 1; i <= n; ++ i) {
for (j = 1; j < n; ++ j) {
if (a[j] < a[j + 1]) { //比较相邻位置
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
int main() {
scanf("%d", &n);
for (i = 1; i <= n; ++ i) scanf("%d", &a[i]);
Sort();
for (i = 1; i <= n; ++ i) printf("%d ", a[i]);
}

什么是字典序?

  • 用于衡量两个字符串的“大小”关系。
  • 对于两个字符串 s1s2,记它们的长度为 |s1||s2|。定义它们字典序的比较方法为:
    • 比较 s1 的第 1 位和 s2 的第 1 位两个字符对应的 ASCII 码的大小,如果两个字符不同,则 ASCII 码较小的字符所在的字符串字典序小。
    • 如果两个字符相同,则比较 s1 的第 2 位和 s2 的第 2 位两个字符对应的 ASCII 码的大小。
    • 以此类推,直到比较到某个字符串的末尾。此时如果 |s1||s2|,则长度较小的字符串字典序小。
    • 否则两个字符串完全相同,字典序相同。
  • 如果写成代码:
//比较字符串 first_ 和 second_ 的大小,如果 first_ 的字典序较大则返回 1,否则返回 0。
int CompareName(char *first_, char *second_) {
int Length1 = strlen(first_), i; //长度
int Length2 = strlen(second_);
int MinLength = Min(Length1, Length2);
for (i = 0; i < MinLength; ++ i) { //按位比较
if (first_[i] > second_[i]) return 1;
if (first_[i] < second_[i]) return 0;
}
return Length1 > Length2; //按位比较完后比较长度
}

字典序这东西有什么用?

  • 我们是怎么比较两个有相同位数的数字的大小的?
  • 998244353
  • 998243514
  • 如果把这两个数字看做字符串,比较大小实质上就是在比较他们的字典序大小。
  • 如何使用计算机储存,并比较两个 10100000 量级的数的大小?
  • 字符串储存+字典序比较。

怎么实现多关键字的排序?

  • 本题中需要以某科成绩为排序第一关键字,第一关键字相同时以姓名字典序为第二关键字排序。
  • 这个条件本质上是两个元素“大小关系”的一种定义.
  • 我们可以模拟上述条件,定义一个比较两个元素大小关系的函数出来,以下以求总分为例:
int CompareName(int first_, int second_) {
int Length1 = strlen(a[first_].name), i;
int Length2 = strlen(a[second_].name);
int MinLength = Min(Length1, Length2);
for (i = 0; i < MinLength; ++ i) {
if (a[first_].name[i] > a[second_].name[i]) return 1;
if (a[first_].name[i] < a[second_].name[i]) return 0;
}
return Length1 > Length2;
}
//如果学生 a[first_] 排名比 a[second_] 靠后,则返回 1,否则返回 0。
int Compare(int first_, int second_) {
if (a[first_].Sum == a[second_].Sum) { //第一关键字相同比第二关键字
return CompareName(first_, second_);
}
return a[first_].Sum < a[second_].Sum; //否则返回第一关键字的比较结果
}
  • 这个函数要怎么用呢?
  • 直接用它替换掉上面整数列降序排序中的小于号就可以了,正确性显然。
void Sort(int L_, int R_, int type_) { //将 a[L_] 到 a[R_] 排序
int i, j;
for (i = L_; i <= R_; ++ i) {
for (j = L_; j < R_; ++ j) {
//如果 a[j] 排名比 a[j+1] 靠后,则交换他们的位置
if (Compare(j, j + 1) == 1) Swap(j, j + 1);
}
}
}
  • 本题中需要将学生按照四种分数各排一次序,只需要写四种比较函数即可。

完整代码:

#include <stdio.h>
#include <string.h>
struct Student {
int Chinese, Math, English, Sum;
char name[10 + 1];
} a[100 + 1], b;
void Swap(int x_, int y_) {
b = a[x_];
a[x_] = a[y_];
a[y_] = b;
}
int Min(int x_, int y_) {
return x_ < y_ ? x_ : y_;
}
int CompareName(int first_, int second_) {
int Length1 = strlen(a[first_].name), i;
int Length2 = strlen(a[second_].name);
int MinLength = Min(Length1, Length2);
for (i = 0; i < MinLength; ++ i) {
if (a[first_].name[i] > a[second_].name[i]) return 1;
if (a[first_].name[i] < a[second_].name[i]) return 0;
}
return Length1 > Length2;
}
int Compare(int first_, int second_, int type_) {
if (type_ == 1) {
if (a[first_].Sum == a[second_].Sum) {
return CompareName(first_, second_);
}
return a[first_].Sum < a[second_].Sum;
} else if (type_ == 2) {
if (a[first_].Chinese == a[second_].Chinese) {
return CompareName(first_, second_);
}
return a[first_].Chinese < a[second_].Chinese;
} else if (type_ == 3) {
if (a[first_].Math == a[second_].Math) {
return CompareName(first_, second_);
}
return a[first_].Math < a[second_].Math;
} else if (type_ == 4) {
if (a[first_].English == a[second_].English) {
return CompareName(first_, second_);
}
return a[first_].English < a[second_].English;
}
}
void Sort(int L_, int R_, int type_) {
int i, j;
for (i = L_; i <= R_; ++ i) {
for (j = L_; j < R_; ++ j) {
if (Compare(j, j + 1, type_) == 1) Swap(j, j + 1);
}
}
}
int main() {
int n, i;
while (scanf("%d", &n) != EOF) {
for (i = 1; i <= n; ++ i) {
scanf("%s", a[i].name);
scanf("%d%d%d", &a[i].Chinese, &a[i].Math, &a[i].English);
a[i].Sum = a[i].Chinese + a[i].Math + a[i].English;
}
Sort(1, n, 1);
for (i = 1; i <= 3; ++ i) {
printf("%s %d\n", a[i].name, a[i].Sum);
}
printf("\n");
Sort(1, n, 2);
for (i = 1; i <= 3; ++ i) {
printf("%s %d\n", a[i].name, a[i].Chinese);
}
printf("\n");
Sort(1, n, 3);
for (i = 1; i <= 3; ++ i) {
printf("%s %d\n", a[i].name, a[i].Math);
}
printf("\n");
Sort(1, n, 4);
for (i = 1; i <= 3; ++ i) {
printf("%s %d\n", a[i].name, a[i].English);
}
printf("\n");
}
return 0;
}

6-N

什么是栈?

  • 一种数据结构,支持以下两种操作:
    • Push:将一个新的元素加入到栈顶。
    • Pop:从栈顶取出一个元素。
  • 这是比较抽象的定义。可以形象地将栈理解为一个桶。可以对这个桶干两件事情:
    • 一是把一件物品扔进桶里,原来桶中的物品的顺序没有被影响,但是被新的物品压在了底下。
    • 二是从桶中取出最顶上的物品。

就这?看起来好弱智啊,有什么用呢?

  • 一些问题可以抽象成栈模型。
  • 比如括号匹配问题。

人类是怎么进行括号匹配的?

  • 以这个合法括号序列为例:()(()())
  1. 人会首先看到第一位的 (记在心里,然后尝试向右找到与它匹配的 )
  2. 在第二位发现了 ),于是把它与第一位的 ( 进行匹配,然后把一二位的 () 抛之脑后,再考虑后面的部分。
  3. 看到第三位的 (记在心里,尝试向右找到与它匹配的 )
  4. 看到第四位的 (记在心里,把第三位的 ( 暂时忘记,尝试向右找到与当前 ( 匹配的 )
  5. 在第五位发现了 ),于是把它与第四位的 ( 进行匹配,然后把四五位的 () 抛之脑后,再考虑是否有与第三位的 ( 匹配的 )
  6. 看到第六位的 (记在心里,把第三位的 ( 暂时忘记,尝试向右找到与当前 ( 匹配的 )
  7. 在第七位发现了 ),于是把它与第六位的 ( 进行匹配,然后把六七位的 () 抛之脑后,再考虑是否有与第三位的 ( 匹配的 )
  8. 在第八位发现了 ),于是把它与第三位的 ( 进行匹配,然后把三八位的 () 抛之脑后
  9. 到达序列尾部,发现没有剩下待匹配的 (,说明该序列为合法括号序列。
  • 发现这个过程与栈的操作类似。
  • 每当有一个新的待匹配的左括号出现,我们都会暂时放下之前的左括号,与把元素加入到栈顶类似。
  • 匹配时我们仅关注最靠右的待匹配的左括号,与只能访问栈顶的元素类似。
  • 匹配成功时把最靠右的待匹配的左括号抛之脑后,与删除栈顶元素类似。

然后我们就可以把括号匹配的过程用栈进行模拟了。直接上代码。

顺带一提,这题的数据有问题。虽然题干中指出仅存在合法括号序列,但是经测试数据中存在不合法的括号序列。我们输出答案时需要只输出匹配的左右括号才可通过。

#include <stdio.h>
char s[100010];
int n, top, st[100010]; //栈
void Push(int val_) { //入栈
st[++ top] = val_;
}
int Pop() { //出栈
return st[top --];
}
int main() {
while (scanf("%s", s) != EOF) {
n = strlen(s);
top = 0; //栈清空
int ans[100010] = {0}; //如果 s[i] 为左括号,则 ans[i] 表示与 i 匹配的右括号
for (int i = 0; i < n; ++ i) {
if (s[i] == '(') { //如果是左括号,则加入栈顶
Push(i);
} else if (s[i] == ')') { //如果是右括号,则把它和栈顶的左括号匹配,并记录下栈顶的左括号与它匹配成功
ans[Pop()] = i;
}
}
for (int i = 0; i < n; ++ i) { //输出成功匹配的左右括号
if (ans[i]) printf("%d %d\n", i + 1, ans[i] + 1);
}
}
return 0;
}

4-B

  • 对于每一个人的工资,为了最小化使用的人民币张数,应尽量使用较大面值的纸币,然后用更小面值的纸币补齐剩余部分。
  • 剩余部分即工资除以某种纸币面值的余数,使用这种纸币的张数即工资除以面值的商。
  • 将工资依次除以 100、50、……、2、1,将商累加即可。
#include<stdio.h>
#include <string.h>
int Calc(int x_) {
int ret = 0, y = x_;
ret += y / 100; y %= 100;
ret += y / 50; y %= 50;
ret += y / 10; y %= 10;
ret += y / 5; y %= 5;
ret += y / 2; y %= 2;
ret += y;
return ret;
}
int main() {
int n;
while (scanf("%d", &n) != EOF) {
int x, sum = 0;
for (int i = 1; i <= n; ++ i) {
scanf("%d", &x);
sum += Calc(x);
}
printf("%d\n", sum);
}
}

4-C

  • 关键在于怎么实现十进制数转二进制。
  • 众所周知的算法是令这个十进制数除以 2,记录余数,再令商除以 2,再记录余数……直至商为 0。
  • 将顺次记录的余数倒序输出即为这个数的二进制表示。
  • 仅展示这个关键部分的代码:
#include <stdio.h>
int main() {
int x, i, length = 0, s[1000];
scanf("%d", &x);
while (x != 0) {
++ length;
s[length] = x % 2;
x /= 2; //不断除以 2
}
//s 数组中 1~length 位的倒序即为 x 的二进制表示。
for (i = length; i >= 1; -- i) {
printf("%d", s[i]);
}
}

4-G

  • 预处理出前 106 个幸运数,询问时直接回答即可。
  • 看代码注释。
#include <stdio.h>
#define kN 1000010
int n, p, a[kN];
int main() {
for (int i = 1; p <= 1000000; ++ i) { //注意循环的终止条件是找到第 10^6 个幸运数
if (i % 6 == 0) { //i 为幸运数
a[++ p] = i; //记录下来
continue;
}
int x = i % 1000, flag = 0; //x 是 i 的后 3 位,flag 是一个标记变量
while (x) { //按位分解 x
if (x % 10 == 6) flag = 1; //出现 6,,则标记 i。
x /= 10;
}
if (flag) a[++ p] = i; //打过标记则记录下 i
}
while (scanf("%d", &n) != EOF) {
printf("%d\n", a[n]);
}
}

5-D

  • 小学奥数。
  • 发现:8 公里 18 元,12 公里 18+9.6 元,13 公里 18+12 元,16 公里 37.2 元。
  • 但是:8+4 公里 18 + 10 元,8+5 公里 18+12 元,8+8 公里 36 元。
  • 连续坐 13 公里和坐 8+5 公里等价,连续坐更多则不如先坐 8 公里后下车。
  • 这个规律可以扩展到总路程更高的情况,那么有:
  1. 如果总路程小于等于 8,则一坐到底。
  2. 如果总路程大于 8,先求得最多能坐多少段 8 公里,再考虑剩余的不足 8 公里的部分。
    1. 如果这一部分大于 4 公里,则坐这一段时换乘。
    2. 如果这一段不大于 4 公里,则坐这一段时接着上一段 8 公里坐。
  • 注意本题的输出要求,如果结果没有小数部分则保留 0 位小数,否则保留一位小数。发现仅在上述情况 2-2 中会使最终花费出现小数,这种情况下特判即可。
#include <stdio.h>
int main() {
int n;
while (1) {
scanf("%d", &n);
if (!n) break;
double ans = 0;
if (n <= 8) {
if (n > 4) ans = 10 + 2 * (n - 4);
else ans = 10;
printf("%.0lf\n", ans);
} else {
ans = (n / 8) * 18.0;
n %= 8;
if (n == 0) {
printf("%.0lf\n", ans);
} else if (n > 4) {
ans += 10 + 2 * (n - 4);
printf("%.0lf\n", ans);
} else {
ans += 2.4 * n;
printf("%.1lf\n", ans);
}
}
}
return 0;
}

5-F

  • 小学奥数。
  • 求的是至多多少次能把假币分出来。
  • 三分法称硬币,首先尝试把一堆硬币均分成三堆。如果不能均分,也至少有两堆数量相同。如:100=34+34+33,5=2+2+1,4=1+1+2。
  • 把两堆数量相同的放到天平上称。如果天平平衡,则假币在另一堆里。否则假币在轻的一堆里。
  • 求至多多少次分出来,应当假定假币在数量较多的一堆里。
  • 再对有假币的一堆重复上述操作,直至称到含假币的一堆数量为 1 为止。

模拟上述过程即可。

#include <stdio.h>
int main() {
int n;
while (1) {
scanf("%d", &n);
if (!n) break;
int ans = 0;
while (n > 1) {
++ ans; //称一次
if (n % 3 == 0) n = n / 3;
else n = n / 3 + 1;
}
printf("%d\n", ans);
}
return 0;
}

6-I

  • 枚举 + 判断素数 + 判断回文数。
  • 一个缝合怪而已。
  • 看代码吧。
#include <stdio.h>
#include <math.h>
int n, ans, l, r;
int Prime(int x_) { //判断 x_ 是否为素数,是素数返回 1,否则返回 0
if (x_ == 1) return 0;
if (x_ == 2) return 1;
for (int i = 2; i <= sqrt(x_); ++ i) { //用 2~sqrt(x) 试除
if (x_ % i == 0) return 0;
}
return 1;
}
int Manacher(int x_) { //判断 x 是否为回文数,把每一位拆分出来,看正序倒序对应位置的数是否相同,是回文数返回 1,否则返回 0。
int p = 0, s[10];
while (x_) { //按位分解
s[++ p] = x_ % 10;
x_ /= 10;
}
for (int i = 1, j = p; i < j; ++ i, -- j) {
if (s[i] != s[j]) return 0;
}
return 1;
}
int main() {
while (scanf("%d%d", &l, &r) != EOF) {
ans = 0;
for (int i = l; i <= r; ++ i) {
ans += Prime(i) && Manacher(i);
}
printf("%d\n", ans);
}
return 0;
}

6-J

  • 缝合怪罢了。
  • 看代码。
#include <stdio.h>
#include <math.h>
int n, ans1[31][31]; //ans1 为杨辉三角
int ans2[10010]; //ans2 为素数
int main() {
for (int i = 1; i <= 30; ++ i) { //预处理杨辉三角
ans1[i][1] = 1;
for (int j = 2; j < i; ++ j) {
ans1[i][j] = ans1[i - 1][j - 1] + ans1[i - 1][j];
}
ans1[i][i] = 1;
}
ans2[1] = 2; //预处理素数
for (int i = 3, p = 1; i <= 10000; ++ i) {
int flag = 1; //标记变量
for (int j = 2; j <= sqrt(i); ++ j) { //判断素数
if (i % j == 0) {
flag = 0; //发现 i 可以被 j 整除,标记 i 不是素数
break;
}
}
if (!flag) continue;//不是素数,跳过 i。
ans2[++ p] = i; //填入
}
while (scanf ("%d", &n) != EOF) {
int p = 0;
for (int i = 1; i <= n; ++ i) { //输出 n 行
for (int j = 1; j <= i; ++ j) { //先输出每一行的杨辉三角
printf("%d ", ans1[i][j]);
}
for (int j = i + 1; j <= n; ++ j) { //再输出每一行的素数
printf("%d ", ans2[++ p]);
}
printf("\n");
}
printf("\n");
}
return 0;
}

写在最后

Q:C 语言程序设计课考什么?
A:数据类型、选择结构、字符串、循环结构、函数、指针……?

虽然这样说也可以,不过这样是否有些太过急功近利?

回顾 “C 语言程序设计” 这门课的名字:在把它用于设计程序之前,我们需要首先认识到,C 语言是一种语言
倘若以一种理性的视角来认识世界,世界可以被抽象成由无数种参数相互作用形成的统一整体。利用计算机,我们便可以把这种抽象化的世界以数据的方式在计算机中模拟出来。利用编程语言,我们便可以将对现实世界的分析与改造的过程通过对数据的操作在计算机里复现出来。借助计算机高超的计算能力,我们便可以解决许多单凭人力心有余而力不足的问题。

“如何学好 C 语言”,这个问题的关键不在于掌握了多少语法、看了多少别人的代码、会多少种厉害的写法。而在于把具体问题进行简化和分解,再一步步地解决子问题的能力。C 语言不过是同没有感情的计算机交流的工具,仅仅追求浮于表面的代码,而怠慢了分析问题能力养成,实属舍本逐末之举。

那么要怎么才能培养这种能力呢?

当我们拿到一道题的时候,先别急着上手开冲:

  • 先将对应问题简化考虑,思考怎么在现实生活中解决这样的问题。
  • 再考虑上述解决问题的过程能否分解为几个解决较小问题的过程。
  • 经过简化和分解后,再考虑如何把自己的思路转化成代码的形式。

看起来很麻烦?其实多做几次便能养成一种思维习惯。

以上。

posted @   Rainycolor  阅读(257)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示