Skip to content
This repository was archived by the owner on May 2, 2026. It is now read-only.

RenAhsAcme/Remajor-Programming-Test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

程序设计(转专业考核备考版)

我在还有2天就参加机试的情况下,在1:4的神奇报录比之下,怀着忐忑不安的心情,以及近乎疲惫不堪的神经,但又充满着对网安的无限美好憧憬,将我这一个月来付出的一些努力进行汇总。(写完发现这是一个病句,但是这句话无足轻重,为了不重新组织这句话,我又写下了这句废话来搪塞。)

From C to C++

C 语言是每个理工科学生,或者说在当今中大交叉学科风气大盛行的时代下的每一个学生,如呼吸空气一般的简单技能,不必浪费笔墨(键盘)在这里赘述。

C++ 可视作是 C 语言的扩充,主要引入面向对象的特性。过多的理论并不能支撑在机试中取得良好表现,所以我只罗列重要的变化。

  • 使用 iostream 简易处理输入输出

    #include <iostream>
    using namespace std;
    cin >> a;
    cout << b;

    在 C 语言中,有时为了安全处理 I/O,我们会手动解析用户输入,但这过于繁琐。使用 iostream 的一些方法,可以根据对象的数据类型自动解析,非常省事。iostream 同时实现了传统的 cstdio 的相关特性,因此可以放心弃用 cstdio

  • 使用 algorithm 高级方法

  • 使用 C++ 样式的实现

    我的原则是“优雅地实现”,因此在 C++ 环境里,应使用 C++ 样式的实现,即使兼容 C 语法样式。比如显式强制类型转换:

    a = (int) b;

    在 C++ 中应写作:

    a = static_cast<int>(b);

    有时看来 scanf 会很方便(至少比手动解析强),但也仅限于 C 中,且它的不安全特性在任何一个 IDE 中都会被 Clang-Tidy 人人喊打。输入 % 和 & 也是一件令人痛苦地事情,所以又有什么理由不选择强大的 C++ Stream 语法呢?

  • 使用 vector 来更加现代化地管理一组数据

    如果不想被栈、堆内存管理折磨,请直接使用 vector,它是一个能让你在任何地方定义、以任何方法使用而无需过分担心内存管理的好东西。

  • 无论什么时候都在变量声明后为它初始化一个合适的值

    如果使用了变长数组等不支持初始化特性的变量,我的建议是:请立即转向 C++ 更现代化的实现方法,比如 new 一个对象。

  • 不要使用 switch 这种低级易出错的用法,使用 if-else 代替

  • 最好不要超过三层循环嵌套

    数据量不大的情况下,三层嵌套是安全的,数据量较大的情况下,两层嵌套通常是安全的,数据量极大的情况下,请不要暴力枚举。

  • 不要写未定义行为

    不要写有歧义的代码,不要在同一行里写依赖于特定顺序才能正确执行的代码(如滥用运算符,括号不明确),不要写冗余代码,不要写不明意义的代码,不要想当然。想当然、不明意义的拍脑袋代码,在本地 debug 是无法被发现的,OJ 应用 O2 优化以后,会出现不明错误。

  • 数组等作为循环条件时先检查边界

    请勿越界访问,会报 RE。

  • 使用现代化的 string 类型存入字符串

  • 无论什么时候都尽可能避免非现代化的实现

    它们通常很麻烦,而且我们在考试时没有精力处理这些奇怪的错误。

  • 使用迭代器

    stringvector 中最好使用迭代器,它们在大多数情况下更加灵活,且不容易引起错误,语义更清晰。区别是迭代器应当使用前导运算符,且需要在适时的情况下使用解引用、距离计算等语法。将迭代器赋值给变量,使用 auto 类型。

  • 不要在 C 和 C++ 中互相转换 C++ 独有的数据类型

    我也不知道这会不会出现一些问题。根据我秉持的“优雅实现”的理念,这一点也不够现代和优雅,在具有更新、更加安全、更加优雅、更加标准的实现方法下,我极度抗拒这些白痴用法,所以我完全忽略了所谓将 string 转为字符数组等诸如此类抽象用法的知识。

ASCII 码表

以经验来看,需记住以下:

0 - 48,A - 65, a - 97

运算符优先级

由于我的榆木脑袋瓜总是记不住运算符优先级的细节,故选择单开一节重申它。

优先级高 优先级低
(、) !、-(负号)、++、-- *、/、% +、- <<、>>(左右位移) <、>、>=、<= ==、!= && ||

string 对象操作

string 对象可以使用以下操作:

  • s.append(a):将字符串 a 追加在原字符串的后面;

  • s.substr(l, r):返回从 l 起的 r 个字符;

  • s.insert(l, a):使索引为 l 的位置变为字符串 a, 同时后移其他元素;(请勿在循环遍历枚举等操作中反复调用该方法,该方法时空复杂度较高,易导致 TLE,请考虑优化算法。)

  • s.find(a, ptr):从索引 ptr 的位置开始查找 a 第一次出现的位置,若找到,返回 unsigned long long, 若未找到,返回 std::string::npos。若要使用未找到情况下的值,请直接使用而不是应用强制转换,这在一些编译器中属于未定义行为。

算法:模拟

从狭义来讲,在我看来,模拟不能称作一个算法。但在实际考试过程中,出现这类题基本就是一个炸弹。一个完全告诉你所有方法的题目,但题面足够的长,这一定会给你带来很大的震撼,甚至可以作为压轴题的存在。我总结了一些技巧:

  • 输入:以恰当的形式组织需要输入到程序的内容,尤其要注意状态机思维,即利用变量对状态进行跟踪,需要搞清楚跟踪哪些内容才能维持程序在环境发生变化时改变行为,常见的状态有:

    • 当前指针/位置/索引;
    • 当前值/累计量
    • 标记数组/条件数组
  • 处理:这是难点。

    • 先区分题面提到的多项规则,它们的优先级是什么;

    • 在适当的时候拆分规则,否则,为每个规则准备一个变量,一行代码,甚至一个函数;

    • 模拟题的禁忌在于在 main 函数里深度耦合相关规则,考虑为它们单独使用函数;

    • 循环结构可用于以下情况:

      • 顺序扫描
      • 条件循环
      • 回合制多轮模拟
    • 主要流程的通用思维架构

      初始化所有变量
      
      while (没有结束) {
          // 1. 判断当前情况
          if (情况A) {
              执行操作A
          }
          else if (情况B) {
              执行操作B
          }
          else {
              执行默认操作
          }
          // 2. 更新状态
          更新变量
      
          // 3. 判断是否结束
          if (满足结束条件) break
      }
      
      输出结果
      
    • 只要没有出现复杂的遍历、枚举等导致时空复杂度爆炸的题目,尽管翻译题目就可以,不要尝试在考场上引入进阶算法等,把精力留到更需要的题目上;

    • 除了需要预防越界访问导致 RE 的情况下,不要在判断语句中同时连接多个条件,这将降低可读性,且出错概率大幅提升。

算法:高精度

直接上模板。根据我的观察,大部分题目可能会要求使用高精度实现加法和乘法,经过我的折腾和各路 AI 神仙的帮助,总结出这套易理解且好写的高精度。在题目未指明,且出现以下情况,无需多余思考,立即着手实现高精度:

  • 斐波那契特性(具有斐波那契数列特征的数据);
  • 会对大量数据进行加和运算,尤其是连续乘法运算,且单个数据仅保证在 int 范围内;
  • 将代码中的关键变量由 int 替换为 long long 后,OJ 的 WA 点报错信息发生改变;
  • 涉及到 17 位及以上的数字。

C++ 实现高精度模板

#include <iostream>
#include <vector>
#include <string>
using namespace std;
typedef vector<int> bigint;

bigint read(string s) {
    bigint a;
    for (int i = s.size() - 1; i >= 0; i--) a.push_back(s[i] - '0');
    return a;
}

void printf(bigint a) {
    for (int i = a.size() - 1; i >= 0; i--) cout << a[i];
    cout << endl;
}

bigint add(bigint a, bigint b) {
    bigint c;
    int carry = 0;
    for (int i = 0; i < a.size() || i < b.size(); i++) {
        int sum = carry;
        if (i < a.size()) sum += a[i];
        if (i < b.size()) sum += b[i]; // 一种更加优雅的逐位相加的实现方式,还能有效防止越界访问。
        c.push_back(sum % 10);
        carry = sum / 10;
    }
    if (carry) c.push_back(carry);
    return c;
}

bigint mul(bigint a, bigint b) {
    bigint c(a.size() + b.size(), 0); // 两个数相乘结果的位数一定不超过这两个数的位数的和。
    for (int i = 0; i < a.size(); i++) for (int j = 0; j < b.size(); j++) c[i + j] += a[i] * b[j];
    // 乘法不同于加法,需要全部乘完再逐个处理进位。(本质上还是在模拟人类竖式计算过程)
    int carry = 0;
    for (int i = 0; i < c.size(); i++) {
        int sum = c[i] + carry;
        c[i] = sum % 10, carry = sum / 10;
    }
    while (c.size() > 1 && c.back() == 0) c.pop_back(); // 去掉潜在的前导零。
    return c;
}

// 调用示例
int main() {
    string s1, s2;
    cin >> s1 >> s2; // 先以字符串的形式读入
    bigint a = read(s1);
    bigint b = read(s2); // 调用存入函数
    
    bigint c1 = add(a, b); // 调用加法
    bigint c2 = mul(a, b); // 调用乘法
    
    print(c1);
    print(c2); // 调用打印
    
    return 0;
}

算法:排序(ALL IN 快排)

由于排序可以通过 algorithmsort 实现,在这里阐述是极其不合时宜的。我在这里简要写一下手动实现快排的模板,重点放在 cmp 函数的编写上(因为我们有时需要自定义排序规则)。

快排的传统简易模板

void quick_sort(vector<int> a, int l, int r) { // 直接传入需要操作的容器
    if (l >= r) return;
    int i = l, j = r, pivot = a[(l + r) / 2];
    while (i <= j) {
        while (a[i] < pivot) i++; // 升序写法,写好的 cmp 函数也是放到这里进行判断
        while (b[i] > pivot) j--;
        if (i <= j) {
            int tmp = a[i];
            a[i] = a[j], a[j] = tmp, i++, j--;
        }
    }
    quick_sort(a, l, j), quick_sort(a, i, r);
}

我们将重心放在 cmp 函数上:

bool cmp(const T &a, const T &b) {
	if (第一个关键字不相等) return 第一个关键字比较
	if (第二个关键字不相等) return 第二个关键字比较
	return 第三个关键字比较
}

注意:T 是类型占位符,不要背模板背傻了;升序的比较是小于,降序的比较是大于;该 cmp 函数可以给 algorithmsort 函数的第三参数使用。

lower_bound, upper_bound 记得待会写

算法:暴力枚举

循环枚举

这是最粗暴的解决方法,一些简易问题可使用该方法迅速枚举并得出期望的答案,但仅适用于以下情况:

  • 数据量不大,且循环不超过三层;数据量较大,且循环不超过两层;
  • 循环执行的是简易操作而非耗时操作。

如果必须使用循环枚举,考虑枚举一些简单的元素,而非直接对着答案疯狂枚举。

该情况过于简单,不多描述。

子集枚举

子集枚举仅在部分题目(如无明显贪心或动态规划策略)中可用,因为它的时间复杂度极高。

它的核心思路是将集合模拟成一个二进制数,用该位置上的 0 和 1 代表该位置选还是不选。我们上代码:

for (int mask = 0; mask < (1 << n); mask++) {
    // 当前正在枚举子集 mask
    cout << "{";
    for (int i = 0; i < n; i++) {
        if (mask & (1 << i)) {
            // 第 i 个元素已被选中
            cout << a[i] << " ";
        }
    }
    cout << "}";
}

排列枚举

这里就是 algorithm 的主场了,我们引入函数 next_permutation(start, end),这是 STL 的一个标准写法,它可以直接操作 [start, end) 区间内存产生下一个字典序列。使用时需注意以下事项:

  • 数组传递指针(即直接把名字传给参数),vector 传递迭代器;
  • 该函数直接操作原内存区域,因此必须事先对其进行排序;
  • 返回值类型是布尔值,当仍有可产生的新序列,返回 true,否则返回 false
  • 常和 do-while 搭配;
  • 可选第三个参数,即放入 cmp 函数。

注意:do-whilewhile 不能直接等价替换,do-while 的逻辑正如字面,会先执行再判断,而 while 会先判断再执行。前者至少还能执行一次,后者可能完全不执行。

算法:递推与递归

算法:贪心

贪心,即“目光短浅”,始终保持当前决策在当前看来是最优的。在部分可用贪心解决的问题中,必须证明局部最优=整体最优。但通常正着证明很难,我们可以反过来考虑:是否有反例证明当目光短浅时,我们得不到最优答案?如果这个问题证明不出来,就知道贪心是可用的。贪心的使用还应满足当前的决策不会对未来决策产生影响,若先排序后决策,往往也是贪心的征兆。贪心使用起来应该具有线性函数那样顺畅的感觉,当尝试贪心时发现阻碍重重(比如怎样选择贪心?不断更换贪心方向?)这些征兆往往提示:在这里使用贪心是错误的。

贪心可能有这样几类(贪心的样式很多很多,极为玄学,我总结的是我遇到的典型的,不一定完全全面正确)

  • 区间贪心:如最多选不重叠区间、最少覆盖区间,这种问题对右端点贪心;
  • 排序贪心:如背包问题,往包里怎么装物品可使收益最大,这种问题对性价比进行贪心;
  • 哈夫曼贪心:如怎样安排取水使得耗时最短,这种问题对最小的两个贪心(这里可能涉及到队列的知识点,但我并没有仔细研究。)

贪心最忌讳的就是陷入泥潭,导致意外调用大量耗时操作,比如反复移动指针,导致在某些情况下,指针没有按照预期移动;反复调用耗时操作,导致 TLE。写贪心时一定要细心仔细。

算法:二分查找与二分答案

二分查找是小学生都能理解的东西,因为我小学翻字典就是这么翻的,所以我不想说了。

这里引入了两个比较重要的函数 lower_boundupper_bound。前者返回第一个大于等于 x 的位置,后者返回第二个大于 x 的位置。用法示例如下:

例如:a = [0, 1, 2, 2, 2, 4, 5]

lower_bound(a, 2) 指向第一个 2;upper_bound(a, 2) 指向 4。

常见用途有:

  • 统计某个数出现的次数,如 count = upper_bound - lower_bound
  • 判断是否存在;
  • 寻找插入位置以保证容器内元素有序。

该函数返回类型为迭代器。

接下来是重头戏:二分查找

它的核心思想就是把求最值问题转化为判断某个答案 x 是否可行

识别该类型题目可依赖下述关键词:

  • 最大值最小/最小值最大
  • 尽量……同时满足……
  • 在限制条件下求最优

该题目必须满足要求:具备单调性。单调性是指答案 x 在可能的范围内变化时,满足条件和不满足条件是具备单调性函数变化特征的。例如 x 越大越容易满足条件,而 x 越小越不能满足条件,这就说明答案具有单调性,可以考虑使用二分答案。

解题的关键步骤

  • 使用 lr 确定答案范围,越小越准确越好;

  • check 函数(后文写如何写到 check 函数,本质还是用贪心/模拟判断)

  • 二分答案,以下是控制流的二分标准模板

    while (l < r) {
        mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }

    此模板是找最小可行解(lower_bound 型),如果找最大可行值,把 l + r 改成 l + r + 1if-elselr 互换位置即可。

    奇技淫巧:当你发现循环卡死时,不妨把原来的 l + r 改成 l + r + 1

check 函数的写法

本质:在当前答案 x 下,检查构造出来的条件是否满足

常见套路:贪心、模拟、计数;

例如,以分割数组类型的题目作为例子:

bool check(int x) {
    cnt = 1;
    sum = 0;
    for (int i = 0; i < n; i++) { // 强调一件事,考场千万不要过分紧张,导致这里变量不统一,或者分号变成逗号
        if (sum + a[i] > x) cnt++, sum = a[i];
        else sum += a[i];
    }
    return cnt <= k; // 看是否满足题目条件
}

注意这里一定要弄清楚返回的布尔值代表什么,它影响着单调性判断。

算法:搜索

搜索界两大恐怖巨头:DFS - 深度优先搜索;BFS - 广度优先搜索

DFS 常伴随着回溯法而出现,例如下列的伪代码:

function dfs(当前状态):
	if (达到目标):
		记录答案
		return
	
	for 每一种选择:
		if (合法):
			标记
			dfs(新状态)
			取消标记(回溯)

BFS 需要引入队列知识,所以不做展开。原因见最后一节。

数据结构

吾听闻,原则上不考数据结构,好吧,我并没有准备。

如果没有原则了,我直接在考场上嚎啕大哭,我是窝囊派。

About

这是一个分享我在准备转专业程序设计考试的相关笔记的仓库。 This is a repository where I share my notes from preparing for the programming exam, which is related to my remajor.

Resources

License

Security policy

Stars

Watchers

Forks

Contributors