(v4 更新)OI+ACM 笔记:0x10 基础算法
0x11 基础算法 语法基础
C++ 小知识
空间大小
- bit(位,简写 b),是计算机中的最小数据单位。
- Byte(字节,简写 B),是计算机文件大小的基本计算单位。1 个 Byte 由 8 bit 组成,可代表一个字母、数字或符号。中文字符则需要 2 Byte。
- KB(千字节)、MB(兆字节)、GB(吉字节、千兆)、TB(太字节)。
- 一般来说,
short占用 \(2 \ \mathrm{Byte}\),int占用 \(4 \ \mathrm{Byte}\)、long long占用 \(8 \ \mathrm{Byte}\)、float占用 \(4 \ \mathrm{Byte}\)、double占用 \(8 \ \mathrm{Byte}\)。由此可以估算一个数组的空间。当值域在short范围内时,可以使用short卡空间。 sizeof()的功能是返回指定数据类型或表达式值在内存中占用的字节数(单位:B),需要强转double后除以 \(2^{20}\) 转化为以 MB 为单位。
(double)sizeof(a) / (1 << 20) // 用于计算 a 的内存占用(单位:MB)
进制
- 在数学中,通常使用下标标记法 \(\mathrm{number}_\mathrm{base}\) 表示进制。
- 在 C++ 中,使用
0b表示二进制,0表示八进制,0x表示十六进制。
std::cout << 0b10 << '\n'; // 输出 2
std::cout << 024 << '\n'; // 输出 20
std::cout << 0x3f << '\n'; // 输出 63
memset(a, value, sizeof(a))的功能是初始化一个数组 \(a\),将数值 \(\mathrm{value}(\mathrm{0x00\sim 0xFF})\) 填充到数组 \(a\) 的每一个字节上。
而一个int占用 \(4\) 个字节,故memset()只能赋值出每 \(8\) 位都相同的int。
原码:32 位有符号整数(int)的最高位为符号位。\(0\) 表示非负数,\(1\) 表示负数。
反码:非负数的反码与原码相同;负数的反码符号位不变,其余位取反。
补码:非负数的补码与原码相同;负数的补码符号位不变,其余位取反后再 \(+1\)。
- 计算机中的有符号数码用补码表示。解决了 \(0\) 的编码不唯一性。
数值类型
数值范围:
| 变量类型 | 数值范围 | 数值上界 |
|---|---|---|
int |
\([-2^{31}, 2^{31})\) | 2'147'483'647 |
unsigned int |
\([0, 2^{32})\) | 4'294'967'296 |
long long |
\([-2^{63}, 2^{63})\) | 9'223'372'036'854'775'807 |
unsigned long long |
\([0, 2^{64})\) | 18'446'744'073'709'551'616 |
__int128 |
\([-2^{127}, 2^{127})\) | 1.7014...e+38 |
__uint128_t |
\([0, 2^{128})\) | 3.4028...e+38 |
自然溢出:
unsigned int自然溢出:对 \(2^{32}\) 取模。unsigned long long自然溢出:对 \(2^{64}\) 取模。__uint128_t自然溢出:对 \(2^{128}\) 取模。
*运算符优先级
优先级:运算符优先级决定了在没有括号明确指定顺序的情况下,哪一个运算符会优先执行。
| 运算符类型 | 运算符符号 |
|---|---|
| 1. 作用域解析 | :: |
| 2. 后缀 | ++ -- () [] . -> |
| 3. 单目 | ++ -- + - ! ~ * & sizeof new delete |
| 4. 成员指针 | .* ->* |
| 5. 乘除模 | * / % |
| 6. 加减 | + - |
| 7. 位移 | << >> |
| 8. 比较 | < <= > >= |
| 9. 判等 | == != |
| 10. 按位与 | & |
| 11. 按位异或 | ^ |
| 12. 按位或 | ` |
| 13. 逻辑与 | && |
| 14. 逻辑或 | ` |
| 15. 条件 | ?: |
| 16. 赋值 | = += -= *= /= ... |
| 17. 逗号 | , |
*运算符结合性
结合性:当具有相同优先级的运算符出现在同一表达式中时,结合性决定了运算的方向。
- 左结合:从左到右计算。
- 右结合:从右到左计算。only 单目运算符(
++)、条件运算符(?:)、赋值运算符(=、+=)。
缓冲区
注意!在交互题中,不论是 "进行询问" 还是 "给出答案",都需要刷新缓冲区!
缓冲区(buffer):是内存空间的一部分。内存空间中预留了一定的存储空间,用来缓冲输入输出的数据。
*缓冲区的作用:
- 解除高速设备与低速设备的制约关系。数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率。
- 减少数据的读写次数。将数据送往缓冲区,当缓冲区满后再进行传送。会大大减少读写次数,节省很多时间。
缓冲区的刷新:以下情况会引发缓冲区的刷新。
- 缓冲区满时。
- (C++)执行
std::fflush(stdout)或std::cout.flush()。 - (C++)执行
std::endl(std::endl即为\n与强制刷新缓冲区)。 - 关闭文件时。
关闭同步 / 解除绑定 / 减少 std::endl 的使用
可以显著加快程序输入输出的速度。
注意!关流以后不能混用 std::cin 与 scanf,以及 std::cout 与 printf!
同步:默认情况下,C++ 的 iostream(
std::cin/std::cout...)与 C 语言的 stdio(scanf/printf/stdin/stdout...)共享或协调它们的缓冲状态,使得混用 iostream 和 stdio 时的输入输出顺序是可预测的(例如std::cout << "a"; printf("b");就会按照先"a"后"b"的顺序输出)。但会产生额外开销。绑定:默认情况下,
std::cin会和std::cout绑在一起,即每次从std::cin读取之前,都会先调用std::cout.flush(),以确保之前std::cout输出的内容会在输入之前显示出来。但会产生额外开销。
关闭同步:std::ios::sync_with_stdio(0) 取消了 C++ 的 iostream 与 C 语言的 stdio 之间的同步。
解除绑定:std::cin.tie(0) 解除了 std::cin 与 std::cout 之间的绑定。这意味着在进行读入操作时,不需要强行刷新输出缓冲区。
解除绑定后,在使用控制台调试多组测试数据的样例时,可以使得每组测试数据的输出,集中到最后输出。
减少 std::endl 的使用:每次使用 std::endl 都会刷新缓冲区,从而产生不必要的刷新开销。替换为 '\n' 即可。
int main() {
std::ios::sync_with_stdio(0); // 关闭同步
std::cin.tie(0); // 解除绑定
std::cout << ans << '\n'; // 将 std::endl 替换为 '\n'
}
使用 std::cout 输出保留指定位数的浮点数
- 固定格式(
std::fixed):将浮点数以定点表示法输出。多余部分四舍五入,不足则补零。 - 设定位数(
std::setpresicion(n)):设定输出浮点数的小数点后位数个数为 \(n\)。
std::cout << std::fixed << std::setprecision(n);
/*
std::fixed 设定以“定点表示法”输出浮点数(而非“科学计数法”)
std::setprecision(n) 设定输出浮点数的小数点后位数个数
*/
C++ 语法糖
auto 类型占位符
auto:自 C++11 起,auto 被重新定义为类型占位符,用于让编译器根据初始化表达式自动推导变量类型。
Lambda 表达式
C++ STL(容器)
C++ STL(操作)
常见库函数重载
快读
getchar() 用来读入 1 byte 的数据,并将其转化为 char 类型,且速度很快。
使用 getchar() 读入字符,再转换成整型。从而代替缓慢地输入整型。
0x11 快读.cpp:
/* 快读 */
template <class T>
inline void read(T &x) {
static char s;
static bool opt;
while (s = getchar(), (s < '0' || s > '9') && s != '-');
x = (opt = s == '-') ? 0 : s - '0';
while (s = getchar(), s >= '0' && s <= '9') x = x * 10 + s - '0';
if (opt) x = ~x + 1;
}
超级快读
fread() 与 fwrite() 原型:
// fread 从指定的 stream 中读入每个占 size 字节,至多 count 个的对象,存入由 ptr 指向的缓冲区
// 返回实际成功读取的对象数量
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
// fwrite 向指定的 stream 中写入每个占 size 字节,至多 count 个的对象,数据来源于 ptr 指向的内存区域
// 返回实际成功写入的对象数量
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
(gc() 与 pc() 的实现细节见下方代码)
gc() 用于读入一个字符。实现原理:
- 首先,检查 "输入缓冲区" 是否处理完。如果是,则通过
fread()从标准输入重新填充输入缓冲区,重置p1指针并调整p2指针。 - 此时若输入缓冲区仍为空,则文件已经输入完毕,返回
EOF;否则,返回*p1 ++作为读入的字符。
pc() 用于输出一个字符。实现原理:
- 首先,检查 "输出缓冲区" 是否已满。如果是,则通过
fwrite()将输出缓冲区内容写入标准输出,重置pp指针。 - 然后,将字符
c写入输出缓冲区(*pp ++ = c)。
0x11 超级快读.cpp:
/*
注意,此代码的所有输出类函数,均不带“空格”与“换行”。
注意,虽然此代码使用了缓冲区,但不需要你手动清空,fastio 的析构函数会自动清空。
注意,此快读快写不能与 C/C++ 风格的 IO 混用,否则会导致 IO 的顺序混乱。
注意,如果你需要使用控制台调试,请在输入的最后使用 Ctrl+Z 手动输入 EOF 来结束输入。
*/
struct fastio {
static const int N = 1 << 20;
/* 输入缓冲区,p1 表示当前处理到的读入位置,p2 表示已读入的末尾位置 */
char buf[N], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, N, stdin), p1 == p2) ? EOF : *p1 ++)
/* 读入一个整数,无需在意类型 */
template <class T>
void read(T &x) {
static char s;
static bool opt;
while (s = gc(), (s < '0' || s > '9') && s != '-');
x = (opt = s == '-') ? 0 : s - '0';
while (s = gc(), s >= '0' && s <= '9') x = x * 10 + s - '0';
if (opt) x = ~x + 1;
}
/* 输出缓冲区,pp 表示当前处理到的输出位置 */
char pbuf[N], *pp = pbuf;
void pc(const char &c) {
if (pp - pbuf == N) fwrite(pbuf, 1, N, stdout), pp = pbuf;
*pp ++ = c;
}
/* 输出一个字符串,char 数组类型 */
void puts(const char *s) {
while (*s) pc(*s), s ++;
}
/* 输出一个字符串,std::string 类型 */
void puts(const std::string &s) {
for (char ch : s) pc(ch);
}
/* 输出一个整数,无需在意类型 */
template <class T>
void print(T x) {
int top = 0; static int stk[40];
if (x < 0) x = ~x + 1, pc('-');
do stk[++ top] = x % 10, x /= 10; while (x);
while (top) pc(stk[top --] + '0');
}
/* 清空输出缓冲区 */
~fastio() {
fwrite(pbuf, pp - pbuf, 1, stdout);
}
} io;
__int128
重载了 __int128 的标准输入输出流,即可以使用 std::cin 与 std::cout 来输入输出 __int128。
0x11 __int128.cpp:
using s128 = __int128;
/* 重载 __int128 标准输入流 */
std::istream &operator >> (std::istream &is, s128 &x) {
std::string s;
is >> s;
int n = s.length();
int f = s[0] == '-';
x = 0;
for (int i = f; i < n; i ++) {
x = x * 10 + s[i] - '0';
}
if (f) x = ~x + 1;
return is;
}
/* 重载 __int128 标准输出流 */
std::ostream &operator << (std::ostream &os, s128 x) {
int top = 0; static int stk[40];
if (x < 0) os << '-', x = ~x + 1;
do {
stk[++ top] = x % 10, x /= 10;
} while(x);
while (top) os << stk[top --];
return os;
}
整数除法上下取整
取整小性质:
- \(\left\lfloor -x \right\rfloor = -\left\lceil x \right\rceil\),\(\left\lceil -x \right\rceil = -\left\lfloor x \right\rfloor\)。
- 对于整数 \(n, m(m > 0)\),有:
- \(\left\lceil \frac{n}{m} \right\rceil = \left\lfloor \frac{n + m - 1}{m} \right\rfloor = \left\lfloor \frac{n - 1}{m} \right\rfloor + 1\)。
- \(\left\lfloor \frac{n}{m} \right\rfloor = \left\lceil \frac{n - m + 1}{m} \right\rceil = \left\lceil \frac{n + 1}{m} \right\rceil - 1\)。
C++ 整数除法结果向零取整:q = a / b 向零方向舍入(即去掉小数部分),而并非向下取整或向上取整。
C++ 整数取模结果与被除数同号:r = a % b 符号与被除数 \(a\) 同号,绝对值等于 \(|a| \bmod |b|\)。
C++ 中的恒等式:a == (a / b) * b + (a % b)。
被除数 a |
除数 b |
商 q(在 C++ 中) |
余数 r(在 C++ 中) |
|---|---|---|---|
7 |
3 |
2 |
1 |
-7 |
3 |
-2 |
-1 |
7 |
-3 |
-2 |
1 |
-7 |
-3 |
2 |
-1 |
double 的有效位数是 \(15\sim 16\) 位,使用 floor(1.0 * n / m) 难以应对 long long 范围内的整数除法取整问题。
考虑分类讨论,以整数除法下取整为例:
- 当 \(m < 0\) 时,先令 \(n, m\) 变成自己的相反数。转化为 \(m > 0\) 的情况。
- 对 \(n\) 的正负进行分类讨论:
- 当 \(n \geq 0\) 时,
n / m为向下取整。直接计算即可。 - 当 \(n < 0\) 时,
n / m为向上取整,此时有恒等式 \(\left\lfloor \frac{n}{m} \right\rfloor = \left\lceil \frac{n - m + 1}{m} \right\rceil\)。使用(n - m + 1) / m计算即可。
- 当 \(n \geq 0\) 时,
0x11 整数除法上下取整.cpp:
/* 整数除法上取整 */
s64 ceilDiv(s64 n, s64 m) {
assert(m);
if (m < 0) n = -n, m = -m;
if (n >= 0) {
return (n + m - 1) / m;
} else {
return n / m;
}
}
/* 整数除法下取整 */
s64 floorDiv(s64 n, s64 m) {
assert(m);
if (m < 0) n = -n, m = -m;
if (n >= 0) {
return n / m;
} else {
return (n - m + 1) / m;
}
}
字符串转数字
0x11 字符串转数字.cpp:常常配合 std::string 中的 substr 来提取一段子串所代表的数字(暂不考虑负数)。
/* 字符串转数字 */
int digitize(const std::string &str) {
int num = 0;
for (char ch : str) {
num = num * 10 + ch - '0';
}
return num;
}
精确开根号
0x11 精确开根号.cpp:
/* 精确开根号:找到最大整数 d,使得 d * d <= n */
s64 isqrt(s64 n) {
s64 d = sqrt(n);
while (d * d > n) {
d --;
}
while ((d + 1) * (d + 1) <= n) {
d ++;
}
return d;
}
精确求对数
0x11 精确求对数.cpp:
/* 精确求对数(下取整):找到最大整数 t,使得 a ^ t <= b */
int ilog(s64 a, s64 b) {
int t = 0;
s64 v = 1;
while (v <= b / a) {
t ++;
v *= a;
}
return t;
}
/* 精确求对数(上取整):找到最小整数 t,使得 a ^ t >= b */
int iLog(s64 a, s64 b) {
int t = 0;
s64 v = 1;
while (v < b) {
t ++;
if (v > b / a) break;
v *= a;
}
return t;
}
取最值
0x11 取最值.cpp:
/* 取 min */
template <class T>
inline void chmin(T &x, const T &y) {
if (x > y) {
x = y;
}
}
/* 取 max */
template <class T>
inline void chmax(T &x, const T &y) {
if (x < y) {
x = y;
}
}
取模
0x11 取模.cpp:
const int mod = 998244353; // 模数需根据实际问题调整
/* 模意义下 加法 */
inline void add(int &x, const int &y) {
x += y;
if (x >= mod) {
x -= mod;
}
}
/* 模意义下 减法 */
inline void dec(int &x, const int &y) {
x -= y;
if (x < 0) {
x += mod;
}
}
/* 模意义下 取反 */
inline void neg(int &x) {
if (x) {
x = mod - x;
}
}
/* 模意义下 乘法 */
inline void mul(int &x, const int &y) {
x = 1ll * x * y % mod;
}
/* 模意义下 修正 */
inline int norm(int x) {
x %= mod;
return x < 0 ? x + mod : x;
}
/* 快速幂 */
constexpr int qpow(int a, int b, int p) {
int ans = 1;
for (; b; b >>= 1) {
if (b & 1) ans = 1ll * ans * a % p;
a = 1ll * a * a % p;
}
return ans;
}
随机数据生成与对拍
std::mt19937(Mersenne Twister)一种广泛使用的伪随机数生成算法。std::mt19937生成出的随机数的类型为unsigned int。std::mt19937_64生成出的随机数的类型为unsigned long long。
std::random_device:通常作为真随机种子来源,为其他伪随机数引擎(如std::mt19937)提供初始化值。std::uniform_int_distribution<IntType>:离散均匀分布,用于生成指定闭区间内的均匀分布整数。std::uniform_real_distribution<RealType>:连续均匀分布,用于生成指定左闭右开区间内的均匀分布浮点数。
生成区间 \([l, r]\) 中的随机整数
0x11 生成区间 \([l, r]\) 中的随机整数.cpp:
std::mt19937_64 mtrand{std::random_device{}()}; // 若使用 testlib.h,则种子需要更换为 rnd.next()
/* 生成区间 [l, r] 中的随机整数 */
inline int rand(int l, int r) {
std::uniform_int_distribution<int> range(l, r);
return range(mtrand);
}
生成区间 \([l, r)\) 中的随机浮点数
0x11 生成区间 \([l, r)\) 中的随机浮点数.cpp:
std::mt19937_64 mtrand{std::random_device{}()}; // 若使用 testlib.h,则种子需要更换为 rnd.next()
/* 生成区间 [l, r) 中的随机浮点数 */
double rand(double l, double r) {
std::uniform_real_distribution<double> range(l, r);
return range(mtrand);
}
生成若干个互不相同的随机整数
- (非均匀随机)生成 \(n\) 个在 \([L, R]\) 范围内的互不相同的随机整数:
- 随机生成 \(n\) 个在 \([L, R - n + 1]\) 范围内的(可重复)整数,并将其升序排序记作 \(b_0 \leq b_1 \leq \cdots \leq b_{n - 1}\)。
- 生成序列 \(a_i = b_i + i\)(其中 \(0 \leq i < n\)),然后将序列 \(\{a_i\}\) 随机打乱。
0x11 生成若干个互不相同的随机整数.cpp:
std::mt19937_64 mtrand{std::random_device{}()}; // 若使用 testlib.h,则种子需要更换为 rnd.next()
/* 生成区间 [l, r] 中的随机整数 */
inline int rand(int l, int r) {
std::uniform_int_distribution<int> range(l, r);
return range(mtrand);
}
/* 生成 n 个在 [L, R] 范围内的互不相同的随机整数 */
std::vector<int> GenSequence(int n, int L, int R) {
assert(n <= R - L + 1);
std::vector<int> res(n);
for (int i = 0; i < n; i ++) {
res[i] = rand(L, R - n + 1);
}
std::sort(res.begin(), res.end());
for (int i = 0; i < n; i ++) {
res[i] += i;
}
std::shuffle(res.begin(), res.end(), mtrand);
return res;
}
生成随机区间列
0x11 生成随机区间列.cpp:
std::mt19937_64 mtrand{std::random_device{}()}; // 若使用 testlib.h,则种子需要更换为 rnd.next()
/* 生成区间 [l, r] 中的随机整数 */
inline int rand(int l, int r) {
std::uniform_int_distribution<int> range(l, r);
return range(mtrand);
}
/* 生成 m 个端点在 [L, R] 范围内的随机区间列 */
std::vector< std::pair<int, int> > GenRanges(int m, int L, int R) {
std::vector< std::pair<int, int> > res;
for (int i = 1; i <= m; i ++) {
int l = rand(L, R), r = rand(L, R);
if (l > r) {
std::swap(l, r);
}
res.push_back({l, r});
}
return res;
}
生成随机父向树
参考:https://blog.csdn.net/EI_Captain/article/details/109910307
- 随机父节点树的期望树高为 \(\mathcal{O}(\log n)\) 级别。
0x11 生成随机父向树.cpp:
std::mt19937_64 mtrand{std::random_device{}()}; // 若使用 testlib.h,则种子需要更换为 rnd.next()
/* 生成区间 [l, r] 中的随机整数 */
inline int rand(int l, int r) {
std::uniform_int_distribution<int> range(l, r);
return range(mtrand);
}
/* 生成 n 个点的随机父向树 */
std::vector< std::pair<int, int> > GenTree(int n) {
std::vector< std::pair<int, int> > res;
for (int i = 2; i <= n; i ++) {
res.push_back({rand(1, i - 1), i});
}
return res;
}
生成随机树
- 生成一个长度为 \(n - 2\) 值域为 \([1, n]\) 的随机序列作为 prufer 序列,再将 prufer 序列转成树。
- 随机树的期望直径为 \(\mathcal{O}(\sqrt{n})\) 级别。
0x11 生成随机树.cpp:
std::mt19937_64 mtrand{std::random_device{}()}; // 若使用 testlib.h,则种子需要更换为 rnd.next()
/* 生成区间 [l, r] 中的随机整数 */
inline int rand(int l, int r) {
std::uniform_int_distribution<int> range(l, r);
return range(mtrand);
}
/* 生成 n 个点的随机树 */
std::vector< std::pair<int, int> > GenTree(int n) {
if (n == 1) {
return {};
}
std::vector< std::pair<int, int> > res;
std::vector<int> prufer(n - 1);
std::vector<int> deg(n + 1, 1);
for (int i = 1; i <= n - 2; i ++) {
prufer[i] = rand(1, n);
deg[prufer[i]] ++;
}
int leaf = 0, p = 0;
for (int i = 1; i <= n; i ++) {
if (deg[i] == 1) {
leaf = p = i;
break;
}
}
for (int i = 1; i <= n - 2; i ++) {
int x = prufer[i];
res.push_back({leaf, x});
if (-- deg[x] == 1 && x < p) {
leaf = x;
} else {
p ++;
while (deg[p] != 1) p ++;
leaf = p;
}
}
res.push_back({leaf, n});
return res;
}
生成随机连通图(稀疏图)
- 先生成一颗 \(n - 1\) 条边的随机父向树,保证连通。再生成剩下的 \(m - n + 1\) 条边。
0x11 生成随机连通图(稀疏图).cpp:
std::mt19937_64 mtrand{std::random_device{}()}; // 若使用 testlib.h,则种子需要更换为 rnd.next()
/* 生成区间 [l, r] 中的随机整数 */
inline int rand(int l, int r) {
std::uniform_int_distribution<int> range(l, r);
return range(mtrand);
}
/* 生成 n 个点 m 条边的随机连通图(稀疏图) */
std::vector< std::pair<int, int> > GenGraph(int n, int m) {
std::vector< std::pair<int, int> > res;
std::map< std::pair<int, int>, int > exist;
for (int i = 2; i <= n; i ++) {
int x = rand(1, i - 1), y = i;
res.push_back({x, y});
exist[{x, y}] = 1;
}
for (int i = n; i <= m; i ++) {
int x, y;
do {
x = rand(1, n), y = rand(1, n);
if (x > y) {
std::swap(x, y);
}
} while (x == y || exist[{x, y}]);
res.push_back({x, y});
exist[{x, y}] = 1;
}
std::shuffle(res.begin(), res.end(), mtrand);
return res;
}
对拍
0x11 对拍(Windows).cpp:
/* Windows 环境下的对拍 */
void CrossTesting(int Q) {
for (int T = 1; T <= Q; T ++) {
std::cout << "Test #" << T << ":\n";
system("gen.exe >data.in");
system("P1.exe <data.in >data1.out");
system("P2.exe <data.in >data2.out");
if (system("fc data1.out data2.out > diff.log")) {
std::cout << "WA\n";
return;
} else {
std::cout << "AC\n";
}
}
}
0x11 对拍(Linux).cpp:
/* Linux 环境下的对拍 */
void CrossTesting(int Q) {
for (int T = 1; T <= Q; T ++) {
std::cout << "Test #" << T << ":\n";
system("gen >data.in");
system("P1 <data.in >data1.out");
system("P2 <data.in >data2.out");
if (system("diff data1.out data2.out > diff.log")) {
std::cout << "WA\n";
return;
} else {
std::cout << "AC\n";
}
}
}
0x12 基础算法 算法基础
主定理
主定理:用于求解分治递归形的渐进时间复杂度问题。具体地,对于一个规模为 \(n\) 的问题,将其划分为 \(a\) 个规模为 \(\frac{n}{b}\) 的子问题,附带 合并/分割 的开销 \(f(n)\)。
以 \(n^{\log_b a}\) 为处理问题用时的基准线。
- 若 \(f(n) = O(n^{\log_b a - \epsilon})\)
直觉:总工作量由 "叶子层" 主导。
- 若 \(f(n) = \Theta(n^{\log_b a} \log^k n)\) 且 \(k \geq 0\)
直觉:每一层工作量相近,共 \(\log_b n\) 层。
- 若 \(f(n) = \Omega(n^{\log_b a + \epsilon})\) 且满足正则性条件 \(af\left(\frac{n}{b}\right) \leq cf(n)\),其中 \(c < 1\)
直觉:总工作量由顶层的 合并/分割 主导。
一些例子:
- 情形 1:遍历满二叉树 \(T(n) = 2T\left( \frac{n}{2} \right) + O(1) = O(n)\)。
- 情形 2:归并排序 \(T(n) = 2T\left( \frac{n}{2} \right) + \mathcal{O}(n) = \mathcal{O}(n \log n)\)。
- 情形 3:多项式牛顿迭代法 \(T(n) = T(\frac{n}{2}) + \mathcal{O}(n \log n) = \mathcal{O}(n \log n)\)。
一个主定理处理不了的例子:\(T(n) = 2T\left(\frac{n}{2}\right) + \mathcal{O}\left(\frac{n}{\log n}\right) = \mathcal{O}(n \log \log n)\)。
快速幂
求解 \(a^b \bmod p\)。
注意考虑 \(a, b, p\) 的数据范围(决定是否开 long long,是否将乘法替换为 "快速乘")。
注意特判 \(b = 0\) 且 \(p = 1\) 的情况。
0x12 快速幂.cpp:
/* 快速幂 */
constexpr int qpow(int a, int b, int p) {
int ans = 1;
for (; b; b >>= 1) {
if (b & 1) ans = 1ll * ans * a % p;
a = 1ll * a * a % p;
}
return ans;
}
快速乘
求解 \(a \times b \bmod p\),其中 \(a, b, p\) 均为 long long 范围。
法 1:__int128
直接使用 __int128 强行转化!
0x12 快速幂(__int128).cpp:
/* 快速乘(__int128) */
constexpr s64 qmul(s64 a, s64 b, s64 p) {
return static_cast<__int128>(a) * b % p;
}
法 2:倍增
类似快速幂,将 \(b\) 二进制拆分。
注意模数的两倍不能超过 long long 范围,否则还需要修改。
0x12 快速乘(倍增).cpp:
/* 快速乘(倍增) */
constexpr s64 qmul(s64 a, s64 b, s64 p) {
s64 ans = 0;
for (; b; b >>= 1) {
if (b & 1) { ans += a; if (ans >= p) ans -= p; }
a += a; if (a >= p) a -= p;
}
return ans;
}
邪修:long double
注意到 \(a \times b \bmod p = a \times b - \lfloor \frac{a \times b}{p} \rfloor \times p\),利用 long double 来处理 \(\lfloor \frac{a \times b}{p} \rfloor\)。
模数较大时可能会出锅。
0x12 快速幂(long double).cpp:
/* 快速乘(long double) */
constexpr s64 qmul(s64 a, s64 b, s64 p) {
s64 c = static_cast<long double>(a) * b / p + 1e-8;
s64 ans = a * b - c * p;
if (ans < 0) ans += p;
if (ans >= p) ans -= p;
return ans;
}
光速幂
对于一个固定的底数 \(a\),多次询问,每次给定一个 \(i(0 \leq i \leq n)\),我们想要快速地求出 \(a^i \bmod p\)。
取参数 \(B = \lceil \sqrt{n} \rceil\)。先预处理出 \(a^1, a^2, \cdots, a^i, \cdots, a^B\)。再预处理出 \(a^B, a^{2B}, \cdots, a^{iB}, \cdots, a^{\lfloor n / B \rfloor B}\)。
每次只需利用 \(a^i = a^{\lfloor i / B \rfloor B} \times a^{i \bmod B}\) 回答询问即可。
时间复杂度:预处理 \(\mathcal{O}(\sqrt{n})\),查询 \(\mathcal{O}(1)\)。
\(k\) 进制快速幂:利用 \(a^i = \left( a^k \right)^{\lfloor i / k \rfloor} \times a^{i \bmod k}\),右侧预处理,左侧递归。
时间复杂度:预处理 \(\mathcal{O}(k \log_k n)\),查询 \(\mathcal{O}(\log_k n)\)。
空间复杂度:\(\mathcal{O}(k \log_k n)\)。
0x12 光速幂.cpp:
/* 光速幂 */
struct flashPower {
int b;
std::vector<int> w1, w2;
flashPower() {}
flashPower(int a, int n) {
init(a, n);
}
void init(int a, int n) {
b = sqrt(n) + 1;
w1.resize(b + 1), w2.resize(b + 1);
w1[0] = 1;
for (int i = 1; i <= b; i ++) {
w1[i] = 1ll * w1[i - 1] * a % mod;
}
w2[0] = 1;
for (int i = 1; i <= b; i ++) {
w2[i] = 1ll * w2[i - 1] * w1[b] % mod;
}
}
int pow(int n) {
return 1ll * w2[n / b] * w1[n % b] % mod;
}
} fp;
高精度
0x13 基础算法 环境相关
不论是 NOI 还是 ICPC,都需要接触到 Linux。作为算法竞赛选手,掌握一点基础的 Linux 知识是很有必要的。
本章内容绝大部分篇幅都在描述 Linux 相关环境(部分内容在 Windows 下也可能兼容,还请读者自行确认)。如果你是 Windows 系统用户:
- 考虑使用 WSL(Windows Subsystem for Linux)。
- 考虑安装虚拟机。
- 考虑安装双系统。
Linux 基础
文件系统
文件路径分隔符:用于在文件路径中,分隔不同的目录名称。从而清晰地表示文件或目录在文件系统层次结构中的位置。
- Windows:默认使用反斜杠
\作为路径分隔符。但内核支持两种分隔符(\与/均可)。 - Linux/Unix/macOS:一律使用正斜杠
/作为路径分隔符(反斜杠\在 Linux Shell 里是转义字符,不会被识别成路径分隔符)。
绝对路径:从根目录 / 开始,完整写出的路径。
相对路径:从当前工作目录来描述相对位置的路径。
.表示当前目录。在很多情况下可以省略。..表示上一级目录。例如../表示当前目录的父目录。~表示当前用户的主目录(~的展开由 Shell 本身完成,并非文件系统特性)。- root 用户的主目录为
/root。 - 普通用户的主目录为
/home/username,其中username为用户名。
- root 用户的主目录为
重定向机制
输入重定向:命令 < file,将文件 file 作为命令的标准输入。
覆盖输出重定向:命令 > file,将命令的标准输出覆盖文件 file。
追加输出重定向:命令 >> file,将命令的标准输出追加到文件 file 末尾。
管道:命令1 | 命令2,将前一个命令的标准输出作为后一个命令的标准输入。
命令格式
Linux 终端命令的基本格式:命令 <选项> <参数>
- 命令(command):要执行的程序或内置指令。
- 选项(options):改变命令的行为。
- 短选项:以
-开头,通常后接单个字符。可以组合使用。
例如:ls -l -a等价于ls -la - 长选项:以
--开头。
例如:ls --all
- 短选项:以
- 参数(arguments):命令要处理的对象。可以有零个或多个。
- 命令选项参数之间使用空格分隔,如果你想在选项或参数中使用空格,请使用双引号
""将选项或参数括起来,或使用\将空格转义。
Linux 查看命令帮助文档:
命令 --help:快速查看简要说明。man 命令:查看更详细的说明文档(manual)
常见命令
echo:输出内容
echo <内容> # 输出内容,行尾自动添加一个换行符
echo -e <内容> # 启用转义字符(例如可以使用 \n 表示换行)
date:输出当前时间
date # 输出当前时间
clear:清除终端屏幕
clear # 清除终端屏幕
快捷键 ctrl+L 也具有相同的效果。
ls:列出目录内容
ls # 列出目录内容
ls -l # 使用长列表信息
ls -a # 显示所有文件(将隐藏文件也显示出来)
ls -h # 以人类可读的格式显示文件大小(如 KB、MB)
ls -l:举例:drwxr-xr-x 2 ubuntu ubuntu 4096 Aug 20 16:23 Environment。
- 有关权限、属主、属组的概念,参考下文的权限管理。
- 第一列,第一个字符表示类型:
-表示普通文件,d表示目录,l表示链接文件。 - 第一列,接下来九个字符表示文件或目录的权限,每三个字符构成一组。
- 每一组依次代表:文件所有者的权限(user),同组用户的权限(group),其他用户的权限(other)。
- 每一组的三个字符
rwx,分别代表可读、可写、可执行。若相应的位置上没有权限,就会使用-显示。可以使用chmod命令修改权限。
- 第二列,表示该文件或目录的硬链接数。
- 第三列,表示该文件或目录的属主。
- 第四列,表示该文件或目录的属组。
- 第五列,表示该文件或目录的大小(默认单位
B)。 - 第六列~第八列,表示该文件或目录的修改时间。
- 最后一列,表示该文件或目录的名字。
cd:切换目录
cd <路径> # 切换目录
cd .. # 返回上一级目录
cd ~ # 返回当前用户的主目录
cd - # 返回上一次切换目录时,所在的目录
pwd:查看当前路径
pwd # 查看当前路径
cat:查看文件内容
cat <文件名> # 查看文件内容
cat -n <文件名> # 添加行号
cat 文件1 文件2 # 查看多个文件内容
cat 文件1 文件2 ... > 新文件 # 整合文件内容 "覆盖" 到新文件
cat 文件1 文件2 ... >> 新文件 # 整合文件内容 "追加" 到新文件
若没有输入文件,则是从键盘输入,使用 ctrl+D 中止输入。
wc:统计文件行数、单词数、字节数
wc <文件名> # 统计文件,依次输出 行数、单词数、字节数 以及文件名
wc -l <文件名> # 只输出行数
wc -w <文件名> # 只输出单词数
wc -c <文件名> # 只输出字节数
wc -L <文件名> # 只输出文件中最长一行的长度(字节数)
grep:搜索文本内容
grep <文本内容> <文件名> # 查找文件中,与给定模式串正则匹配的文本,并将匹配到的"行"打印出来
grep -n <文本内容> <文件名> # 显示行号
diff:比较文件差异
diff 文件1 文件2 # 比较文件差异
diff 命令的返回值(常用于对拍):\(0\) 表示文件相同,\(1\) 表示文件不同,\(2\) 表示发生错误。
若文件 1 与文件 2 没有差异,则 diff 无输出;否则 diff 的输出由一系列 "更改命令" 组成,这些命令指示如何将文件 1 转换成文件 2,每个命令形如:
<行号范围><操作符><行号范围>
< 文件1 的内容
---
> 文件2 的内容
其中操作符有三种:a 表示追加(add),c 表示更改(change),d 表示删除(delete)。
time:测量命令用时
time <命令> # 测量命令耗时
time ./program # 测量可执行文件 program 的耗时
time 命令一般会输出三个用时:实际时间 real 常用于测量程序运行用时,可以使用文件 I/O 来排除手动 I/O 的用时。
- 实际时间
real:从开始到结束的用时(包含等待 I/O,被抢占,睡眠 ...)。 - 用户时间
user:CPU 执行用户代码的用时。 - 系统时间
sys:CPU 执行内核代码的用时。
*其余文件与目录操作
| 命令 | 用途 |
|---|---|
mkdir |
创建目录 |
rmdir |
删除目录 |
touch |
创建空文件(或更新时间戳) |
cp |
复制文件或目录 |
mv |
移动文件或目录 / 重命名文件或目录 |
rm |
删除文件或目录 |
命令行编译
执行程序:运行当前目录下的可执行文件 program(例如 program.cpp 编译生成的可执行文件 program)。因为当前目录并不在命令行的默认搜索范围内,所以需要加上 ./ 定位到当前目录
./program # 运行 program
./program <data.in # 文件输入
./program >data.out # 文件输出
./program <data.in >data.out # 从文件 data.in 中读入数据,输出到文件 data.out 中
手动命令行编译
g++ program.cpp # 编译 program.cpp,默认生成可执行文件 a.out
g++ program.cpp -o <文件名> # 编译 program.cpp,生成指定文件名的可执行文件
g++ program.cpp -o program # 编译 program.cpp,生成可执行文件 program
常用选项:
-std=c++23:启用 C++23 标准。-O2:开启 O2 优化-Wall:显示所有编译警告信息。-Wconversion:对隐式类型转换(例如double转int)发出警告。-fsanitize=<name>:Sanitizers 通过在编译运行时插入代码,动态地检测某些错误。但会严重增加代码的时间与空间。-fsanitize=address:检测对堆、栈、全局变量的越界访问(性能开销 \(\approx 200\%\))。-fsanitize=undefined:检测未定义行为(性能开销通常 \(< 150\%\))。
Code Runner(VSCode 插件)
Code Runner 是一个 VSCode 的插件,可以一键编译运行多种编程语言的代码。
- 使用方法:
- 点击 VSCode 右上角的播放按钮▶️(Run Code),或者使用默认快捷键
Ctrl+Alt+N。 - 此时 VSCode 的终端就会自动生成命令,用于编译运行当前的程序。
- 点击 VSCode 右上角的播放按钮▶️(Run Code),或者使用默认快捷键
- 更改配置:在 Code Runner 设置中的 Executor Map 下的 Edit in settings.json 来修改 settings.json 文件。在 settings.json 文件的
"code-runner.executorMap"更改"cpp"的编译选项。
{
// ...
"code-runner.executorMap": {
// ..
"cpp": "cd $dir && g++ $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt", // 默认配置,可以自行修改
// ...
},
// ...
}
优点:编译运行相当快捷。
GNU Make
GNU Make 是 Linux 下的一个自动化构建工具,可以一键编译运行多种语言的代码。
由于 GNU Make 相当强大,在这里我们只讨论他的简单用法。
注:只有文件 program.cpp 最近有修改过,make program 才会执行。
- 默认使用方法:若要编译
program.cpp,直接在终端中运行make program即可。 - 添加编译选项:若要添加额外的编译选项,可以在当前目录下建立一个
makefile(无后缀名)的文本文件,输入如下指令,然后在终端中运行make program。
CXXFLAGS = -std=c++23 -O2 -fsanitize=address
优点:方便临时修改编译选项(例如添加 -fsanitize=address 来针对 RE 进行 debug)。
*Linux 权限管理
Linux 是多用户系统。Linux 允许不同的用户同时登陆主机,同时使用主机的资源。
用户:包含用户名(Username)与用户编号(UID)。
对于服务器环境,"用户" 的概念是明确的:服务器的管理员可以为不同的使用者创建用户,分配不同的权限,保障系统的正常运行;也可以为网络服务创建用户(此时,用户就不再是一个有血有肉的人),通过权限限制,以减小服务器被攻击时对系统安全的破坏。
对于个人用户,他们的设备不会有第二个人在使用。此时,现代操作系统一般区分使用者用户与 "系统用户",并且划分权限,以尽可能保证系统的完整性不会因为用户的误操作或恶意程序而遭到破坏。
| UID | 特性 | |
|---|---|---|
| root 用户 | \(0\) | 在 Linux 操作系统中拥有最高的权限,可以对系统做任何操作 |
| 系统用户 | 通常为 \(1 \sim 999\) | 用于执行服务等系统人物,有系统或相关程序创建 |
| 普通用户 | 通常从 \(1000\) 开始 | 用于真人用户登录和使用系统。 |
切换用户:
sudo [命令] # 以 root 的身份执行命令
sudo -u <用户名> [命令] # 以某个用户的身份执行命令
su <用户名> # 切换至某个用户,不加载登录环境
su - <用户名> # 切换至某个用户(登录式切换)
用户组:用户组是用户的集合,包含用户组名(Groupname)与用户组编号(GID)。通过用户组机制,可以为一批用户设置权限。
三种用户群体:在 Linux 中,每个文件和目录都关联着三种不同的用户群体。
| 指代 | |
|---|---|
所有者 u(User) |
通常是该文件或目录的创建者(但可以被 chown 命令改变) |
所属组 g(Group) |
该文件或目录所属的用户组,该用户组成员共享此权限 |
其他人 o(Others) |
除所有者与所属组成员以外的其他用户 |
三种权限:在 Linux 中,每个文件和目录都有三种不同的权限
| 对文件 | 对目录 | |
|---|---|---|
可读 r(read) |
表示用户可以查看文件的内容 | 允许用户列出目录中的文件名 |
可写 w(write) |
表示用户可以修改文件的内容 | 允许用户在目录下创建、删除、重命名文件 |
可执行 x(execute) |
表示文件可以作为 "程序" 来运行 | 允许用户进入目录 |
修改权限:
# 修改权限:以符号的形式
chmod [用户群体] [操作符] [权限] <文件名>
chmod ugo +rwx example.txt
# 用户群体:
# u 表示所有者,g 表示所属组,o 表示其他人,a 表示所有用户(All)
# 操作符:
# + 表示添加权限,- 表示移除权限,= 表示精确设置权限(覆盖原有的)
# 权限:
# r 表示可读,w 表示可写,x 表示可执行

浙公网安备 33010602011771号