C++ 入門防爆零教程(上冊)
C++ Introductory Explosion Proof Zero Tutorial(Volume \(1\))
編寫者:美公雞(洛谷賬號:beautiful_chicken233,電話:\(155****7747\),如有需要請隨時聯絡)
編寫時間:\(2023.10.5 \sim ?\)
地址:湖南省長沙市雨花區明升異城 \(11\) 棟 \(3902\)
出版社:美公雞出版社
Part \(0\) 目錄
Directory
Part \(1\) 賽時注意事項
\(...\) Part \(1.1\) 檔案讀寫
\(.....\) Part \(1.1.1\) \(\texttt{freopen}\)
\(.....\) Part \(1.1.2\) \(\texttt{fstream}\)
\(.....\) Part \(1.1.3\) \(\texttt{fopen}\)
\(...\) Part \(1.2\) 源程式的存放
\(...\) Part \(1.3\) 惡意程式
\(...\) Part \(1.4\) 卡常
\(.....\) Part \(1.4.1\) 關閉同步流
\(.....\) Part \(1.4.2\) 關閉緩衝區自動重新整理
\(.....\) Part \(1.4.3\) 快讀快寫
\(.....\) Part \(1.4.4\) 底層最佳化
Part \(2\) 演演算法
\(...\) Part \(2.1\) 時間 & 空間複雜度
\(.....\) Part \(2.1.1\) 時間複雜度
\(.....\) Part \(2.1.2\) 空間複雜度
\(...\) Part \(2.2\) 列舉
\(...\) Part \(2.3\) 模擬
\(...\) Part \(2.4\) 排序
\(.....\) Part \(2.4.1\) 選擇排序
Part \(1\) 賽時注意事項
Games-time precautions
Part \(1.1\) 檔案讀寫
在 CSP 等重大比賽中,最重要的就是檔案讀寫,在每一年都有許多競賽選手因為檔案讀寫而爆零。而檔案讀寫都很多類,比如 freopen
等等。下面介紹的是 \(3\) 種常用的檔案讀寫。
Part \(1.1.1\) \(\texttt{freopen}\)
在程式中加上 \(\texttt{freopen("xxx.in/out", "r/w", stdin/stdout);}\) 之後,程式就會從 \(\texttt{xxx.in}\) 中讀取輸入檔案,在 \(\texttt{xxx.out}\) 中輸出。
Part \(1.1.2\) \(\texttt{fstream}\)
首先需要在主函式外面定義 \(\texttt{\#include <fstream>}\) 標頭檔案和 \(\texttt{ifstream fin("xxx.in")}\) 和 \(\texttt{ifstream fout("xxx.out")}\)。然後在程式中要檔案讀寫的寫成 \(\texttt{fin}\) 和 \(\texttt{fout}\) 即可。
Part \(1.1.3\) \(\texttt{fopen}\)
首先要定義 \(\texttt{FILE *file = fopen("xxx.in", "r+/w+")}\)。然後在程式中使用 \(\texttt{fscanf(file, "", ...)}\) 和 \(\texttt{fprintf()}\) 即可。
Part \(1.2\) 源程式的存放
在比賽中,源程式的存放是比較重要的,源程式的錯誤存放會導致失去分數。
一般的存放結構如下:
\(\texttt{|-your name (folder)}\)
\(\texttt{|---T1 name (folder)}\)
\(\texttt{|------T1 name.cpp}\)
\(\texttt{|---T2 name (folder)}\)
\(\texttt{|------T2 name.cpp}\)
\(\texttt{|---T3 name (folder)}\)
\(\texttt{|------T3 name.cpp}\)
\(\texttt{|---T4 name (folder)}\)
\(\texttt{|------T4 name.cpp}\)
Part \(1.3\) 惡意程式
在比賽中,惡意程式的分數會變為 \(0\),且會有相應的懲罰,一定不要做。
惡意程式的殺傷力如下表:
程式碼 | 殺傷力 |
---|---|
\(\texttt{while (1) & Sleep()}\) | 低 |
\(\texttt{system("")}\) | 中 \(\sim\) 高 |
\(\texttt{#include <con>}\) | 極高 |
Part \(1.4\) 卡常
Part \(1.4.1\) 關閉同步流
C++ 的輸入輸出的緩衝區和 C 是同步的,我們可以用 \(\texttt{ios::sync_with_stdio(0)}\) 關閉同步流而加快速度。
Part \(1.4.2\) 關閉緩衝區自動重新整理
程式會在輸入和輸出時重新整理緩衝區(特別是 endl
,想快一點就用 '\n'
),我們可以用 \(\texttt{cin.tie(0), cout.tie(0)}\) 關閉緩衝區自動重新整理而加快速度。
Part \(1.4.3\) 快讀快寫(不推薦,比較麻煩)
模板:
快讀:
int read() {
int s = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
(ch == '-') && (f = -1), ch = getchar();
}
while (ch >= '0' && ch <= '9') {
s = (s << 1 + s << 3) + ch - '0', ch = getchar();
}
return s * f;
}
快寫:
void write(int x) {
(x < 0) && (putchar('-')) && (x = -x);
if (x > 9) {
write(x / 10);
}
putchar(x % 10 + '0');
}
Part \(1.4.4\) 底層最佳化
迴圈展開:
恰當的迴圈展開可以讓 CPU 多執行緒進行併發運算,可以提升速度。但是不恰當的迴圈展開可能會起副作用。
展開前:
\(\texttt{for (int i = 1; i <= 300; ++ i) \{}\)
\(\texttt{ ans += i;}\)
\(\texttt{\}}\)
展開後:
\(\texttt{for (int i = 1; i <= 300; i += 6) \{}\)
\(\texttt{ ans += i;}\)
\(\texttt{ ans += i + 1;}\)
\(\texttt{ ......}\)
\(\texttt{ ans += i + 6;}\)
\(\texttt{\}}\)
火車頭(指令最佳化):
Tips:NOI 禁止。
\(\texttt{#pragma GCC optimize(3)}\)
\(\texttt{#pragma GCC target("avx")}\)
\(\texttt{#pragma GCC optimize("Ofast")}\)
\(\texttt{#pragma GCC optimize("inline")}\)
\(\texttt{#pragma GCC optimize("-fgcse")}\)
\(\texttt{......}\)
完整程式碼:
#pragma GCC optimize(3)
#pragma GCC target("avx")
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fwhole-program")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-fstrict-overflow")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-skip-blocks")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("-funsafe-loop-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")
#pragma GCC optimize(2)
Part \(2\) 演演算法
Algorithm
Part \(2.1\) 時間 & 空間複雜度
Part \(2.1.1\) 時間複雜度
人們將程式執行次數的量級以及空間的量級成為時空複雜度,用大 \(O\) 表示,一般會忽略常數。現代評測機大約每秒能夠執行 \(2 \times 10^7 \sim 10^9\) 次,但是使用了陣列就比較慢了。需要格外注意你的時間複雜度是否都在題目限制以內。
時間複雜度表:
時間複雜度 | 少爺評測機 | 老爺評測機 |
---|---|---|
\(O(\log n)\) | \(2^{10^9}\) | \(2^{2 \times 10^7}\) |
\(O(\sqrt n)\) | \(10^{18}\) | \(4 \times 10^{14}\) |
\(O(\log n\sqrt n)\) | \(5 \times 10^{14}\) | \(3 \times 10^{11}\) |
\(O(n^{\frac{3}{4}})\) | \(10^{12}\) | \(5 \times 10^9\) |
\(O(n)\) | \(10^9\) | \(2 \times 10^7\) |
\(O(n \log \log n)\) | \(4 \times 10^8\) | \(5 \times 10^6\) |
\(O(n \log n)\) | \(4 \times 10^7\) | \(10^6\) |
\(O(n \sqrt n)\) | \(10^6\) | \(8 \times 10^4\) |
\(O(n^2)\) | \(3 \times 10^4\) | \(5000\) |
\(O(n^2 \log n)\) | \(9000\) | \(1500\) |
\(O(n^2 \sqrt n)\) | \(4000\) | \(1000\) |
\(O(n^3)\) | \(1000\) | \(300\) |
\(O(n^4)\) | \(150\) | \(80\) |
\(O(n^5)\) | \(60\) | \(30\) |
\(O(2^n)\) | \(30\) | \(25\) |
\(O(2^n \times n)\) | \(25\) | \(20\) |
\(O(n!)\) | \(12\) | \(10\) |
\(O(n! \times n)\) | \(11\) | \(9\) |
\(O(n^n)\) | \(9\) | \(8\) |
常用時間複雜度排序:
\(O(1) < O(\log n) < O(\sqrt n) < O(n) < O(n \log n) < O(n \sqrt n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)\)
Part \(2.1.2\) 空間複雜度
類似地,演演算法所使用的空間隨輸入規模變化的趨勢可以用空間複雜度來衡量。
如,開一個長度為 \(n\) 的陣列,那麼空間複雜度是 \(O(n)\)。開一個長度為 \(n \times n\) 的二維陣列,那麼空間複雜度是 \(O(n^2)\)。開一個長度為 \(n \times 3\) 的二維陣列或 \(3\) 個長度為 \(n\) 的陣列,那麼空間複雜度是 \(O(3n)\)。開一個長度為 \(n\) 的陣列和一個長度為 \(m\) 的陣列,那麼空間複雜度是 \(O(n + m)\)。
Part \(2.2\) 列舉
列舉的思想是不斷地猜測,從可能的集合中一一嘗試,然後再判斷題目的條件是否成立。但是並非所有的情況都要列舉,有時要適當的進行一些剪枝。(如列舉 \(a + b = c\) 且 \(b > a\) 的個數那麼 \(b\) 要從 \(a + 1\) 開始列舉)。
例題:
給出 \(n\) 個數 \(a_1, a_2, \cdots, a_n\) 和 \(x\),求有多少對 \(i, j\) 滿足 \(a_i + a_j = x\) 且 \(j > i\)。
輸入樣例:
6 12
4 5 7 6 3 5 6
輸出樣例:
2
資料範圍:\(1 \le n \le 10^3\),\(1 \le x, a_i \le 10^9\)。
分析:
我們可以先列舉 \(i\),從 \(1\) 列舉到 \(n\)。每次列舉到一個 \(i\) 時列舉 \(j\),從 \(i + 1\) 列舉到 \(n\)(因為 \(j > i\))。每列舉到一個 \(i, j\) 時判斷條件 \(a_i + a_j = x\),如果滿足把答案 \(+1\)。時間複雜度 \(O(n^2)\)。(準確來說是 \(O\Big(\dfrac{n^2 - n}{2}\Big)\))。
程式碼:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
using ll = long long;
const int kMaxN = 1010, kInf = (((1 << 30) - 1) << 1) + 1;
int n, x, a[kMaxN], ans = 0; // ans 是滿足條件的個數
int main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
cin >> n >> x; // 輸入 n, x
for (int i = 1; i <= n; ++ i) {
cin >> a[i]; // 輸入 ai
}
for (int i = 1; i <= n; ++ i) { // 列舉 i,範圍 1 至 n
for (int j = i + 1; j <= n; ++ j) { // 列舉 j,範圍 i + 1 至 n
(a[i] + a[j] == x) && (++ ans); // 如果 ai + aj = x,那麼答案 +1(if 壓縮)
}
}
cout << ans << '\n'; // 輸出答案
return 0;
}
Part \(2.3\) 模擬
模擬就是用程式碼模擬出題目所要求的操作。雖然本質上比較簡單,但是碼量大,很難調錯。所以做模擬題的時候一定要先構思好再敲程式碼。
例題:
小藍要和朋友合作開發一個時間顯示的網站。在伺服器上,朋友已經獲取了當前的時間,用一個整數表示,值為從 \(1970\) 年 \(1\) 月 \(1\) 日 \(00:00:00\) 到當前時刻經過的毫秒數。
現在,小藍要在客戶端顯示出這個時間。小藍不用顯示出年月日,只需要顯示出時分秒即可,毫秒也不用顯示,直接捨去即可。
給定一個用整數表示的時間,請將這個時間對應的時分秒輸出。
資料範圍:給定的時間不超過 \(10^{18}\)。
分析:
我們直接把輸入的時間 \(t\) 除以 \(1000\) 變成秒(毫秒和秒之間的進率為 \(1000\))。然後再時間轉換,天為 \(t \bmod (60^2 \times 24) \div 60^2\),小時為 \(t \bmod (60^2 \times 24) \bmod 60^2 \div 60\),分鐘為 \(t \bmod (60^2 \times 24) \bmod 60\)。這裡為了方便,我們定義 \(d = 60^2 \times 24\),\(h = 60^2\),\(m = 60\)。時間複雜度 \(O(1)\)。注意,一定要開 long long
!
程式碼:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
using ll = long long;
const int kMaxN = -1, kInf = (((1 << 30) - 1) << 1) + 1;
const ll d = 24 * 60 * 60, h = 60 * 60, m = 60; // 定義常量 d, h, m
int main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
ll t;
cin >> t; // 輸入 t
t /= 1000; // 把 t 除以 1000(把毫秒轉換成秒)
ll day = t % d / h, hour = t % d % h / m, _min_ = t % d % m;
// 計算天,小時,分鐘(注意這裡 min 只能定義成 _min_)
printf("%02d:%02d:%02d", day, hour, _min_); // 輸出,格式為 dd:hh:mm
return 0;
}
Part \(2.4\) 排序
涼心提醒:這一章比較長。
排序就是將一個無序的序列排成有序的演演算法,下面為各個排序的性質:
排序演演算法 | 時間複雜度 | 空間複雜度 | 穩定性 | 特殊性質 |
---|---|---|---|---|
選擇排序 | \(O(n^2)\) | \(O(1)\) | 否 | 可以透過額外的 \(O(n)\) 空間達到穩定 |
氣泡排序 | \(O(n) \sim O(n^2)\) | \(O(1)\) | 是 | 有序時速度快,可以進行剪枝 |
插入排序 | \(O(n) \sim O(n^2)\) | \(O(1)\) | 是 | 當 \(n\) 較小時速度很快 |
快速排序 | \(O(n \log n) \sim O(n^2)\) | \(O(\log n) \sim O(n)\) | 否 | 最快的排序,有序時退化到平方級 |
歸併排序 | \(O(n \log n)\) | \(O(n)\) | 是 | 不易被卡,很穩定 |
希爾排序 | \(O(n) \sim O(n^2)\) | \(O(1)\) | 否 | 是插入排序改進而來的排序 |
堆排序 | \(O(n \log n)\) | \(O(1)\) | 否 | 常數較大 |
桶排序 | \(O(\max^{n}_{i = 1} a_i)\) | \(O(n + m)\) | 是 | 空間比較大 |
基數排序 | \(O(d(r + n))\) | \(O(rd + n)\) | 是 | 非比較排序 |
計數排序 | \(O(n + k)\) | \(O(k)\) | 是 | 非比較排序 |
Part \(2.4.1\) 選擇排序
選擇排序的思想就是在無序區裡找到最大值,然後與無序區的最後一個交換位置,再把無序區的最後一個變成有序區。當有序區有 \(n - 1\) 個元素時,排序完成。
例:(!
代表有序區)
初始:\([5, 7, 2, 8, 4]\)
第 \(1\) 次:\(8\) 最大,與 \(4\) 交換位置,\([5, 7, 2, 8, 4] \gets [5, 7, 2, 4, !8]\)。
第 \(2\) 次:\(7\) 最大,與 \(4\) 交換位置,\([5, 7, 2, 4, !8] \gets [5, 4, 2, !7, !8]\)。
第 \(3\) 次:\(5\) 最大,與 \(2\) 交換位置,\([5, 4, 2, !7, !8] \gets[2, 4, !5, !7, !8]\)。
第 \(4\) 次:\(4\) 最大,與 \(4\) 交換位置,\([2, 4, !5, !7, !8] \gets[2, !4, !5, !7, !8]\)。
排序後:\([2, 4, 5, 7, 8]\)。
程式碼:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
using ll = long long;
const int kMaxN = 5050, kInf = (((1 << 30) - 1) << 1) + 1;
int n, a[kMaxN]; // 定義 n 和 a 陣列
int main() {
cin >> n; // 輸入 n
for (int i = 1; i <= n; ++ i) {
cin >> a[i]; // 輸入 ai
}
int maxa = -1e9, idx = 0; // 存最大值,idx 是下標
for (int i = n; i >= 2; -- i) {
maxa = -1e9; // 每次取最大值前要把 maxa 賦值成極小值
for (int j = 1; j <= i; ++ j) { // 列舉最大值
if (a[j] >= maxa) { // 如果大於等於最大值
maxa = a[j], idx = j; // 更新最大值
}
}
swap(a[idx], a[i]); // 交換位置
}
for (int i = 1; i <= n; ++ i) {
cout << a[i] << ' '; // 輸出
}
cout << '\n';
return 0;
}
未完待續