0x000 前言
这是一堆板子, 欢迎 copy。
0x100 进制转换
0x110 十进制转k进制
0x111 整数
十进制转k进制使用的是 短除法 ,举个例子:
(
10
)
10
=
(
1010
)
2
(10)_{10}=(1010)_2
(10)10=(1010)2,其计算方法如下:
10
÷
2
=
5
…
0
5
÷
2
=
2
…
1
2
÷
2
=
1
…
0
1
÷
2
=
0
…
1
\begin{align} 10÷2=5…0\\ 5÷2=2…1\\ 2÷2=1…0\\ 1÷2=0…1 \end{align}
10÷2=5…05÷2=2…12÷2=1…01÷2=0…1
接着将余数倒序存储,就是转换结果了。因为十进制转换后的数可能比较长,或含有字母,所以用数组存,再转为字符串存储,这样就可以直接输入输出了。转换部分:
string tenTok(int number, int k) {
int a[MAXN] = {};
do {
a[++a[0]] = number % k;
number /= k;
} while (number != 0);
string ans;
for (int i = a[0]; i >= 1; i--) {
if (a[0] > 9) ans += (a[0] - 10 + 'A');
else ans += (a[0] + '0');
}
return ans;
}
0x112 小数
小数采用”乘k取整法“,取出的整数正序存储即可。
string tenTok_lit(double number, int k, int Maxlen) {
int a[MAXN] = {};
while (a[0] <= Maxlen && number != 0) {
number *= k;
a[++a[0]] = int(number);
number -= a[a[0]];
}
string ans;
for (int i = i; i <= a[0]; i++) {
if (a[0] > 9) ans += (a[0] - 10 + 'A');
else ans += (a[0] + '0');
}
return ans;
}
0x120 k进制转十进制
0x121 整数
k进制转十进制使用的是乘权求和法,其公式如下(设这个数为a,使用k进制):
(
a
n
a
n
−
1
a
n
−
2
.
.
.
a
1
‾
)
10
=
a
n
∗
k
n
−
1
+
a
n
−
1
∗
k
n
−
2
+
.
.
.
a
1
∗
k
0
(\overline{a_na_{n-1}a_{n-2}...a_1})_{10}=a_n*k^{n-1}+a_{n-1}*k^{n-2}+...a_1*k^0
(anan−1an−2...a1)10=an∗kn−1+an−1∗kn−2+...a1∗k0
那么,我们举个例子:
(
1010
)
2
=
(
10
)
10
(1010)_2=(10)_{10}
(1010)2=(10)10,其计算方法如下:
(
0
∗
2
0
+
1
∗
2
1
+
0
∗
2
2
+
1
∗
2
3
)
10
=
(
10
)
10
(0*2^0+1*2^1+0*2^2+1*2^3)_{10}=(10)_{10}
(0∗20+1∗21+0∗22+1∗23)10=(10)10
进制转十进制一般不会超过 int 的最大范围,所以直接使用 int 类型即可(如果超过了,可以使用 long long)。
int ktoten_tmp(int n, int k) { //n为该数字的个位位置,输入必须用char或string类型
int power = 1, sum = 0;
for (int i = n - 1; i > -1; i--) {
if (s[i] >= 'A') sum += (s[i] - 55) * power;
else sum += (s[i] - '0') * power;
power *= k;
}
return sum;
}
0x122 小数
依旧是”乘权求和法“,但公式变成了这样:
(
0.
a
1
a
2
.
.
.
a
n
‾
)
10
=
a
1
∗
k
−
1
+
a
2
∗
k
−
2
+
.
.
.
a
n
∗
k
−
n
(0.\overline{a_1a_2...a_n})_{10}=a_1*k^{-1}+a_2*k^{-2}+...a_n*k^{-n}
(0.a1a2...an)10=a1∗k−1+a2∗k−2+...an∗k−n
double ktoten_lit_tmp(int n, int k) { //n为该数字的十分位位置,输入必须用char或string类型
double power = k, sum = 0;
for (int i = n + 1; i < len; i++) {
if (s[i] >= 'A') sum += (s[i] - 55) * (1.0 / power);
else sum += (s[i] - '0') * (1.0 / power);
power *= k;
}
return sum;
}
0x130 总结
进制转换需要记背的公式比较多,但只要记住这些公式,代码还是很好实现的。当然记住了公式也很容易错。
0x200 高精度算法
0x210 前言
高精度算法普遍使用的思路是 模拟竖式计算 的过程。两数计算时会分别倒序存入两个数组 (为了方便最高位的进位) ,一边进位一边计算。加减法的时间复杂度为 O ( n ) O(n) O(n) ,乘法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),除法的时间复杂度为 O ( n ) O(n) O(n)(高精度除以低精度)或 O ( n 3 ) O(n^3) O(n3)(高精度除以高精度)。高精度计算主要分为:
-
逆序存储
-
计算
-
最高位进位
-
去除前导零
各运算具体思路如下。
0x220 加法
0x221 整数加法
加法就是 模拟竖式计算 的过程,举一个例子,123+237=250,将 123 和 127 存进数组后,应该是这样的:
| 下标 | [1] | [2] | [3] | [4] |
|---|---|---|---|---|
| a | 3 | 2 | 1 | 0 |
| b | 7 | 3 | 2 | 0 |
| c | 0 | 6 | 3 | 0 |
3+7=10,需要进位,c[1]保留0,c[2]++ ;2+3+1=6,不用进位,直接放进c[2];1+2=3,不用进位,直接放进c[3]。最后将c数组倒序存入字符串ans,返回ans并输出。实现代码如下: (注意,ans必须使用string类型,不然使用复合运算符会报错) 。
string add(string a1, string b1) {
//预备需要使用的变量及数组
int lena = a1.size(), lenb = b1.size(), i, x = 0;
int a[MAXN] = {}, b[MAXN] = {}, c[MAXN] = {};
//倒序存入数组
for (i = 0; i < lena; i++) a[lena - i] = a1[i] - '0';
for (i = 0; i < lenb; i++) b[lenb - i] = b1[i] - '0';
//计算加法并进位
i = 1;
while (i <= lena || i <= lenb) {
c[i] = a[i] + b[i] + x, x = c[i] / 10;
c[i] %= 10, i++;
}
//特判最高位是否需要进位
c[i] += x;
//去除最高位前导零
if (c[i] == 0) i--;
//转化为string字符串,方便下一步
string ans;
for (int j = 1; j <= i; j++) ans += c[i - j + 1] + '0';
return ans;
}
0x212 小数加法
总体来说,与整数加法相似,但有以下几个难点:
- 小数后需补整数
- 十分位向个位进位需要特判
- 如果小数部分全为0,则删除小数点
解决这些问题,小数加法就解决了。
bool pd(string s) {
int len = s.size() - 1;
while (len--)
if (s[len] == '.') return true;
return false;
}
int find_pit(string s, int len) {
int pit = 0;
for (int i = len - 1; i >= 0; i--)
if (s[i] == '.') {
pit = len - i - 1;
break;
}
return pit;
}
void turn_num(string s, int a[], int len) {
for (int i = len - 1; i >= 0; i--)
if (s[i] != '.') a[++a[0]] = (s[i] - '0');
}
string add(string a1, string b1) {
int a[MAXN] = {}, b[MAXN] = {}, c[MAXN] = {}, i, x = 0;
int lena = a1.size(), lenb = b1.size();
int pit_a = 0, pit_b = 0, pit_c = 0;
pit_a = find_pit(a1, lena);
pit_b = find_pit(b1, lenb);
pit_c = max(pit_a, pit_b);
b[0] = pit_c - pit_b, a[0] = pit_c - pit_a;
turn_num(a1, a, lena);
turn_num(b1, b, lenb);
i = 1;
while (i <= a[0] || i <= b[0]) {
c[i] = (a[i] + b[i] + x), x = c[i] / 10;
c[i] %= 10, i++;
}
(x == 0) ? i-- : c[i] = x;
string ans;
for (int j = i; j > 0; j--) {
if (j == pit_c) {
if (j == i) ans += '0';
ans += '.';
}
ans += (c[j] + '0');
}
if (pd(ans)) while (ans[i] == '0') ans.erase(i--, 1);
if (ans[i] == '.') ans.erase(i--, 1);
return ans;
}
0x220 减法
减法与加法相似,但需要考虑a < b的情况,于是可以做一个特判:当a < b时,交换a,b的值,ans储存一个 “-”,表示 结果为负数 。代码如下所示 (注意,a1和b1必须使用string类型,否则不能直接使用swap( )函数) 。
string sub(string a1, string b1) {
//预备需要使用的变量及数组
int lena = a1.size(), lenb = b1.size(), i;
int a[MAXN] = {}, b[MAXN] = {}, c[MAXN] = {};
string ans;
//特判是否为负数
if (lena < lenb || (lena == lenb && s1 < s2)) swap(a1, b1), swap(lena, lenb), ans += "-";
//倒序存入数组
for (i = 0; i < lena; i++) a[lena - i] = a1[i] - '0';
for (i = 0; i < lenb; i++) b[lenb - i] = b1[i] - '0';
//计算加法并进位
i = 1;
while (i <= lena || i <= lenb) {
if (a[i] < b[i]) a[i + 1]--, a[i] += 10;
c[i] = a[i] - b[i];
i++;
}
//去除最高位前导零
while (c[i] == 0 && i > 1) i--;
//转化为string字符串,方便下一步
for (int j = 1; j <= i; j++)
ans += c[i - j + 1] + '0';
return ans;
}
0x230 乘法
0x231 高精度乘高精度
乘法的竖式涉及到 错位相加 ,所以需要两层循环,分别用b的每个数位乘a的每个数位,举个例子:
a
[
3
]
a
[
2
]
a
[
1
]
∗
b
[
2
]
b
[
1
]
———————————
c
1
[
3
]
c
1
[
2
]
c
1
[
3
]
c
2
[
3
]
c
2
[
2
]
c
2
[
1
]
———————————
c
[
4
]
c
[
3
]
c
[
2
]
c
[
1
]
\begin{align} a[3]\quad a[2]\quad a[1]\\ *\qquad \qquad b[2]\quad b[1]\\ ———————————\\ c1[3]\quad c1[2]\quad c1[3]\\ c2[3]\quad c2[2]\quad c2[1]\qquad \quad \\ ———————————\\ c[4]\qquad c[3]\qquad c[2]\qquad c[1] \end{align}
a[3]a[2]a[1]∗b[2]b[1]———————————c1[3]c1[2]c1[3]c2[3]c2[2]c2[1]———————————c[4]c[3]c[2]c[1]
这是一个典型的多位数乘多位数的竖式。它的计算顺序大概是这样的:
-
计算
a[i]*b[j]; -
将
c[i+j-1]加上结果; -
计算
c[i+j-1]进的位数; -
将
c[i+j]加上c[i+j-1]进的位数。
所以,计算乘法的代码就呼之欲出了:
string mul(string a1, string b1) {
//同上
int lena = a1.size(), lenb = b1.size(), i, x;
int a[MAXN] = {}, b[MAXN] = {}, c[10 * MAXN] = {};
for (i = 0; i < lena; i++) a[lena - i] = a1[i] - '0';
for (i = 0; i < lenb; i++) b[lenb - i] = b1[i] - '0';
//计算乘法,i代表a的第i位,j代表b的第j位
for (i = 1; i <= lena; i++) {
x = 0;
for (int j = 1; j <= lenb; j++) {
//计算每一位相乘的值
c[i + j - 1] += a[i] * b[j] + x;
//计算进位
x = c[i + j - 1] / 10;
//计算c[i+j-1]进位后剩下的部分
c[i + j - 1] %= 10;
}
//最高位加上进位
c[i + lenb] += x;
}
i = lena + lenb;
//去前导零
while (c[i] == 0 && i > 1) i--;
//数组转string
string ans;
for (int j = i; j > 0; j--)
ans += char(c[j] + 48);
return ans;
}
0x232 高精度乘低精度
string div_mul(string a1, int b) {
int lena = a1.size(), i, x = 0;
int a[MAXN] = {}, c[10 * MAXN] = {};
for (i = 0; i < lena; i++) a[lena - i] = a1[i] - '0';
i = 1;
while (i <= lena) {
c[i] = a[i] * b + x;
x = c[i] / 10;
c[i] %= 10;
i++;
}
c[i] += x;
while (c[i] > 9) {
x = c[i] / 10;
c[i] %= 10;
i++;
c[i] += x;
}
while (c[i] == 0 && i > 1) i--;
string ans;
for (int j = i; j > 0; j--) ans += c[j] + '0';
return ans;
}
0x240 除法
0x241 高精度除以高精度
高精除以高精就是 模拟竖式除法 的过程,过程如下:
-
计算上一位的余数除以除数的值(向下取整);
-
将余数保留至下一位。
但这样就会有个问题, 有一堆前导零在前面站位 ,所以,就需要去前导零。代码如下:
bool flag = false;
int lena, lenb, a[MAXN], b[MAXN], ans[MAXN];
bool pd() {
//判断被除数是否大于除数
string a1, b1;
for (int i = lena; i >= 1; i--) a1 += a[i] + '0';
for (int i = lenb; i >= 1; i--) b1 += b[i] + '0';
if (lena > lenb || (lena == lenb && a1 >= b1)) return true;
else return false;
}
void sub() {
//做减法
for (int i = 1; i <= lena && i <= lenb; i++) {
if (a[i] < b[i]) a[i + 1]--, a[i] += 10;
a[i] = a[i] - b[i];
}
while (a[lena] == 0 && lena > 1) lena--;
}
void wy(int f) {
//除数添0或去0
if (f == 1) {
for (int i = lenb; i >= 0; i--)
b[i + 1] = b[i];
lenb++;
} else {
for (int i = 2; i <= lenb; i++)
b[i - 1] = b[i];
lenb--;
}
}
void div_high(string a1, string b1) {
//高精除高精的主体
lena = a1.size(), lenb = b1.size();
for (int i = 0; i < lena; i++) a[lena - i] = a1[i] - '0';
for (int i = 0; i < lenb; i++) b[lenb - i] = b1[i] - '0';
int tmp_lenb = lenb;
while (lena - lenb > 0) wy(1);
while (lenb >= tmp_lenb) {
//num记录上
int num = 0;
while (pd()) sub(), num++;
//去前导零
if (num > 0) flag = true;
//商的存放
if (flag) ans[++ans[0]] = num;
wy(0);
}
//也可以在后面将商转为string类型
}
0x242 高精度除以低精度
高精除以低精实际上是 用减法去模拟除法 ,举个例子,114514÷11,计算过程如下:
首先,给除数11扩大10倍直到等于被除数的位数,接着开始减除数:
114514
−
110000
=
4514
114514-110000=4514
114514−110000=4514
由于只能减一个,所以商1。接着除数缩小10倍,继续减法,但被除数小于除数,商0。继续缩小,减法求商:
4514
−
1100
=
3414
3414
−
1100
=
2314
2314
−
1100
=
1214
1214
−
1100
=
114
\begin{align} 4514-1100=3414\\ 3414-1100=2314\\ 2314-1100=1214\\ 1214-1100=114 \end{align}
4514−1100=34143414−1100=23142314−1100=12141214−1100=114
商4余114,除数缩小,继续求商:
114
−
110
=
4
114-110=4
114−110=4
商1余4。这时,我们发现除数再去一个0还是大于余数,只能商一个0。那么,我们得出结果 114514÷11=10410。思路已有,代码开始:
string div_low(string a1, int b) {
long long lena = a1.size(), x = 0;
int a[MAXN] = {}, c[MAXN] = {};
for (int i = 0; i < lena; i++) a[lena - i] = a1[i] - '0';
//除法主体
for (int i = lena; i >= 1; i--) {
x = 10 * x + a[i];
c[i] = x / b;
x %= b;
}
//去前导零
while (c[lena] == 0 && lena > 1) lena--;
string ans;
for (int i = lena; i >= 1; i--) ans += c[i] + '0';
return ans;
}
0x250 模运算
根据上面的思路,最后余下来的数就是余数。所以,返回一个 x 即可(高精除高精就返回 计算完商后的被除数 )。
long long mod_low(string a1, int b) {
long long lena = a1.size(), x = 0;
int a[MAXN] = {}, c[MAXN] = {};
for (int i = 0; i < lena; i++) a[lena - i] = a1[i] - '0';
for (int i = lena; i >= 1; i--) {
x = 10 * x + a[i];
c[i] = x / b;
x %= b;
}
return x;
}
0x260 总结
高精度算法的思路比较简单明了,主要在于 代码的细节程度 ,稍有不注意,就爆零了。
重庆计算机协会提醒您:高精须注意,爆零两行泪。
0x300 分治模板
关于分治的定义,请移步分治板块查看!
0x310 归并排序
归并排序就是利用分治的特性: 将一个问题分解为多个子问题 ,来进行排序。具体步骤如下:
- 分解 需要排序的序列直到每个序列 只剩一个数 。
- 一个数的序列一定是有序的。
- 合并,使用 双指针法 进行合并,这样能够保证顺序不乱。
我们举个例子比如现在有一列数:5 3 2 6 1 4,我们模拟一下归并排序的过程:
- 拆分为
5 3 2和6 1 4两个序列。 - 拆分为
5 3、2、6 1和4这 4 个序列。 - 拆分为
5、3、2、6、1、4这 6 个序列。 - 合并第 1、2 个序列,同时合并第 4、5 个序列,此时数列中的顺序是
3 5、2、1 6、4。 - 合并第 1、2 和第 3、4 两个区间,得到
2 3 5和1 4 6两个序列。 - 最后进行一次合并,得到
1 2 3 4 5 6,这就是排好序的序列。
具体的代码模板如下:
void merge_sort(int l,int r) {
if (l == r) return;
//拆分序列
int mid = (l + r) / 2;
merge_sort(l, mid);
merge_sort(mid + 1, r);
//双指针法合并两个序列
int i = l, j = mid + 1, num = l - 1;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) b[++num] = a[i++];
else b[++num] = a[j++];
}
//特判两个序列不等长的情况
while (i <= mid) b[++num] = a[i++];
while (j <= r) b[++num] = a[j++];
//复制数组元素到原数组
for (int k = l; k <= r; k++) a[k] = b[k];
}
另外,非常重要的一点:
归并排序是稳定排序!
在 CSP-2022 中有考到, 当时我做错了 。
0x320 快速排序
void quicksort(int l,int r) {
if (l >= r) return;
int key = a[l], L = l, R = r;
while (L < R) {
while (L < R && a[R] >= key) R--;
while (L < R && a[L] <= key) L++;
if (L != R) swap(a[L], a[R]);
}
if (l != L) swap(a[L], a[l]);
quicksort(l, L - 1);
quicksort(L + 1, r);
}
0x330 快速幂
long long qkpow(int y) {
if (y == 1) return 2;
long long tmp = qkpow(y / 2);
if (y % 2) return 2 * tmp * tmp;
else return tmp * tmp;
}
0x400 __int128_t输入输出
小小的科普一下, __int128_t 可以储存
2
128
2^{128}
2128 大小的数字,大概也就是 $10^{38} $ ~
1
0
39
10^{39}
1039 的大小,使用时请慎重。
应该用高精度的还是得用高精度!
0x410 输入
使用快读的思想,检测到 ' ' 或者 \n 时结束输入,时间复杂度比scanf()还快!
__int128_t read128() {
char c;
__int128_t num = 0;
while (1) {
c = getchar();
if (c == ' ' || c == '\n') break;
num = num * 10 + (c - '0');
}
return num;
}
0x420 输出
使用快写的思想,将 __int128_t 转换为 string 后再输出,时间复杂度基本为常数级别,大概是
O
(
19
)
O(19)
O(19) 左右。
void put128(__int128_t num) {
string ans = "";
if (num == 0) {
cout << 0;
return;
}
while (num != 0) ans += (num % 10) + '0', num /= 10;
int len = ans.size();
for (int i = 0; i < len / 2; i++)
swap(ans[i], ans[len - i - 1]);
cout << ans;
}
0x500 素数筛
0x510 调和级数
我们从 2 到
n
n
n 枚举整数
i
i
i ,标记大于
i
i
i 且不大于
n
n
n 的
i
i
i 的倍数。枚举到
i
i
i 时,若
i
i
i 没有被标记过,则
i
i
i 为质数。可以证明其时间复杂度为:
O
(
∑
i
=
1
n
n
i
)
=
O
(
n
log
n
)
O(\sum\limits_{i=1}^n \frac n i)=O(n \log n)
O(i=1∑nin)=O(nlogn)
bool vis[N + 1];
vector<int> p;
void sieve() {
for (int i = 2; i <= N; ++i) {
if (!vis[i]) p.push_back(i);
for (int j = i * 2; j <= N; j += i)
vis[j] = 1;
}
}
0x520 欧拉筛
从 2 到 n n n 枚举整数 i i i ,在从小到大枚举所有 不大于 i i i 的 最小质因子 p 0 p_0 p0 ,标记为 i p 0 ip_0 ip0 。显然,枚举到的 p 0 p_0 p0 是 i p 0 ip_0 ip0 的最小质因子,而更大的 p 0 p_0 p0 不可能 是 i p 0 ip_0 ip0 的最小质因子。同样的,如上,枚举到 i i i 时,如果 i i i 没有被标记过,则 i i i 是质数。因为每个合数只会被其最小质因子枚举时标记,所以其时间复杂度是 O ( n ) O(n) O(n) 。
bool vis[N + 1];
vector<int> p;
void sieve() {
for (int i = 2; i <= N; ++i) {
if (!vis[i]) p.push_back(i);
for (int j = 0; i * p[j] <= N; ++j) {
vis[i * p[j]] = 1;
if (i % p[j] == 0) break;
}
}
}
0x530 埃拉托斯特尼筛法
从 2 到 n n n 枚举整数 i i i ,若 i i i 是质数(即在之前的枚举中没有被标记过),则标记大于 i i i 却不大于 n n n 的 i i i 的倍数。同样的,如果 i i i 没有被标记过,则 i i i 是质数。其时间复杂度是 O ( n log log n ) O(n \log \log n) O(nloglogn) 。
bool vis[N + 1];
vector<int> p;
void sieve() {
for (int i = 2; i <= N; ++i) {
if (!vis[i]) {
p.push_back(i);
for (int j = i * 2; j <= N; j += i)
vis[j] = 1;
}
}
}
0x600 链表
0x610 单链表
这里使用了 class 进行封装,实际用的时候记得把下面这段源码复制到 主函数前面 。
class List {
public:
void insert(int x) {
++nodenum;
a[nodenum].Next = head;
a[nodenum].val = x;
head = nodenum;
}
void erase_by_right(int k) {
if (k == 0) head = a[head].Next;
else a[k].Next = a[a[k].Next].Next;
}
void insert_by_right(int k, int x) {
++nodenum;
a[nodenum].Next = a[k].Next;
a[nodenum].val = x;
a[k].Next = nodenum;
}
void print() {
for (int i = head; i != 0; i = a[i].Next)
printf("%d ", a[i].val);
printf("\n");
}
private:
int nodenum, head;
struct Node {
int Next, val;
} a[100005];
};
使用时可以像其他 STL 一样声明变量。比如 List lis ,就声明了一个单链表数据结构。我们也可以像其他 STL 一样调用函数,比如:
List lis;
lis.insert(2);
insert(x) 的作用就是插入一个节点,但它是 头插法 ,一定要注意。另外的函数的作用如下:
List lis;
lis.insert(x); //在头部插入一个节点
lis.erase_by_right(k); //删除第k次插入操作的下一个元素
lis.insert_by_right(k, x); //在第k次插入的元素的下一个位置插入一个元素
lis.print(); //顺序输出链表,自带换行
0x620 双链表
这里使用了 class 进行封装,实际用的时候记得把下面这段源码复制到 主函数前面 。
class double_list {
public:
void insert_left(int x) {
a[nodenum].r = head;
a[head].l = nodenum;
head = nodenum;
if (tail == 0) tail = nodenum;
a[nodenum].val = x;
}
void insert_right(int x) {
a[nodenum].l = tail;
a[tail].r = nodenum;
tail = nodenum;
if (head == 0) head = nodenum;
a[nodenum].val = x;
}
void erase(int k) {
if (a[k].l == 0 && a[k].r == 0) head = tail = 0;
else if (a[k].l == 0) head = a[k].r, a[a[k].r].l = 0;
else if (a[k].r == 0) tail = a[k].l, a[a[k].l].r = 0;
else {
a[a[k].l].r = a[k].r;
a[a[k].r].l = a[k].l;
}
}
void insert_by_left(int k, int x) {
if (head == k) {
a[nodenum].r = head;
a[head].l = nodenum;
head = nodenum;
} else {
a[nodenum].r = k;
a[nodenum].l = a[k].l;
a[a[k].l].r = nodenum;
a[k].l = nodenum;
}
a[nodenum].val = x;
}
void insert_by_right(int k, int x) {
if (tail == k) {
a[nodenum].l = tail;
a[tail].r = nodenum;
tail = nodenum;
} else {
a[nodenum].l = k;
a[nodenum].r = a[k].r;
a[a[k].r].l = nodenum;
a[k].r = nodenum;
}
a[nodenum].val = x;
}
void print_left(int x) {
for (int i = head; i != 0; i = a[i].r)
printf("%d ", a[i].val);
printf("\n");
}
private:
int nodenum, head, tail;
struct Node {
int l, r, val;
} a[100005];
};
使用时可以像其他 STL 一样声明变量。比如double_list lis,就声明了一个双链表数据结构。我们也可以像其他 STL 一样调用函数,比如:
double_list lis;
lis.insert_left(x);
下面列举已有的函数的功能:
double_list lis;
lis.insert_left(x); //往链表头部插入一个数
lis.insert_right(x); //往链表尾部插入一个数
lis.erase(k); //删除第k次插入的数
lis.insert_by_left(k, x); //在第k次插入的数的下一个位置插入一个数
lis.insert_by_right(k, x); //在第k次插入的数的上一个位置插入一个数
lis.print_left(); //从头部开始输出整个链表
0x700 约数
0x710 分解质因数
整数的唯一分解定理 :任何一个大于 1 的整数都可以表示为若干个 质数 的乘积,表示如下:
N = ∏ i = 1 i = k p i c i N=\prod\limits_{i=1}^{i=k}p_i^{c_i} N=i=1∏i=kpici
实现分解质因数,我们可以从 2 枚举到 N \sqrt N N ,设当前枚举到的数是 i i i ,如果 i i i 能整数 N N N ,就在 N N N 的银子表内加上他,并用 N N N 一直除以 i i i ,每除以一次就在 因子表的数量 上加一,直到除不尽为止。
void divide(int n) {
cnt = 0;
for (int i = 2; i <= sqrt(n); i++)
if (n % i == 0) {
prime[++cnt] = i, c[cnt] = 0;
while (n % i == 0) n /= i, c[cnt]++;
}
if (n > 1) prime[++cnt] = n, c[cnt] = 1;
}
刚才,我们知道了怎么分解一个质因数,接下来就该求
N
N
N 有多少个约数了。根据约数的定义,上面的展开式中所有的元素 都是
N
N
N 的约数 。那么根据乘法原理,
N
N
N 的约数个数
T
T
T 可以表示为:
T
=
(
1
+
c
1
)
×
(
1
+
c
2
)
×
⋅
⋅
⋅
×
(
1
+
c
m
)
T=(1+c_1)\times(1+c_2)\times···\times(1+c_m)
T=(1+c1)×(1+c2)×⋅⋅⋅×(1+cm)
也就是下面这个:
T
=
∏
i
=
1
i
=
m
(
1
+
c
i
)
T=\prod\limits_{i=1}^{i=m}(1+c_i)
T=i=1∏i=m(1+ci)
这个式子叫做 正整数的正约数个数表达式 。
还是根据上面的展开式,我们可以得到,
N
N
N 的所有正约数的和
S
S
S 就是:
S
=
(
1
+
p
1
+
⋅
⋅
⋅
+
p
1
c
1
)
×
⋅
⋅
⋅
×
(
1
+
p
m
+
⋅
⋅
⋅
+
p
m
c
m
)
S=(1+p_1+···+p_1^{c_1})\times···\times(1+p_m+···+p_m^{c_m})
S=(1+p1+⋅⋅⋅+p1c1)×⋅⋅⋅×(1+pm+⋅⋅⋅+pmcm)
即:
S
=
∏
i
=
1
i
=
m
(
∑
j
=
0
j
=
c
i
p
i
j
)
S=\prod\limits_{i=1}^{i=m}(\sum\limits_{j=0}^{j=c_i}{p_i}^j)
S=i=1∏i=m(j=0∑j=cipij)
这个式子叫做 正整数的所有约数和表达式 。
0x720 约数
我们知道了一个正整数的约数和和约数个数,但是我们还是不能知道这个正整数的约数到底是什么。想要知道具体的约数,需要枚举实现。
但是约数有一个性质:它是成对出现的(完全平方数除外)。
假如一个正整数 a a a 是 N N N 的约数,且 a ≤ N a\le \sqrt N a≤N ,那么必然会有一个正整数 b b b 是 N N N 的约数,且 b ≥ N b\ge \sqrt N b≥N。
而且根据约数的性质,我们完全可以断定,这个 b b b 就是 N a \frac{N}{a} aN 。
因此,和质数的判定对应地,我们使用试除法。同样只需要枚举 1 − N 1-\sqrt N 1−N 的所有数,每枚举到一个可被 N N N 整除的数,就把这个数和除 N N N 这个数所得的数一起加入到约数集合中。
当然,在细节实现上要判一下 它是不是完全平方数 。
void factor(int n) {
for (int i = 1; i <= sqrt(n); i++)
if (n % i == 0) {
f[++cnt] = i;
if (i != n / i)
f[++cnt] = n / i;
}
}
通过试除法的原理,我们还可以推导出:
一个数的约数个数上届为 2 N 2\sqrt N 2N 。
0x800 排列组合
0x810 排列数
int A(int n, int m) {
int pw = 1;
for (int i = 0; i < m; i++) pw *= (n - i);
return pw;
}
0x820 组合数
int C(int n, int m) {
int pw1 = A(n, m), pw2 = 1;
for (int i = 1; i <= m; i++) pw2 *= i;
return pw1 / pw2;
}
0x830 第二类斯特林数
long long S(int n, int m) {
long long dp[MAXN][MAXN] = {};
if (n < m) return 0;
for (int i = 0; i <= min(n, m); i++) dp[i][i] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (i > j) dp[i][j] = j * dp[i - 1][j] + dp[i - 1][j - 1];
return dp[n][m];
}
0x900 并查集
class diset {
public:
void make_set(int n) {
for (int i = 1; i <= n; i++) {
a[i].data = i;
a[i].rank = 0;
a[i].parent = i;
}
}
int find_set(int x) {
if (x != a[x].parent) return find_set(a[x].parent);
return x;
}
void union_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (a[x].rank > a[y].rank) a[y].parent = x;
else {
a[x].parent = y;
if (a[x].rank == a[y].rank) a[y].rank++;
}
}
bool check_set(int x, int y) {
x = find_set(x);
y = find_set(y);
if (x == y) return true;
return false;
}
private:
struct Node {
int data, rank, parent;
} a[20005];
};
对于并查集而言,我还是使用class对其进行了封装,目前只有 4 个常用函数,预计未来会添加新的函数的。目前的函数的用法如下:
diset dis;
dis.make_set(n); //初始化并查集,限定长度为n
dis.union_set(x, y);//合并x和y
dis.find_set(x); //返回x所在集合的代表元素
dis.check_set(x, y);//返回x和y是否在同一个集合
1566





