c++模板大全

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=505÷2=212÷2=101÷2=01

接着将余数倒序存储,就是转换结果了。因为十进制转换后的数可能比较长,或含有字母,所以用数组存,再转为字符串存储,这样就可以直接输入输出了。转换部分:

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 (anan1an2...a1)10=ankn1+an1kn2+...a1k0
那么,我们举个例子: ( 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} (020+121+022+123)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=a1k1+a2k2+...ankn

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]
a3210
b7320
c0630

3+7=10,需要进位,c[1]保留0c[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]
这是一个典型的多位数乘多位数的竖式。它的计算顺序大概是这样的:

  1. 计算a[i]*b[j]

  2. c[i+j-1]加上结果;

  3. 计算c[i+j-1]进的位数;

  4. 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 高精度除以高精度

高精除以高精就是 模拟竖式除法 的过程,过程如下:

  1. 计算上一位的余数除以除数的值(向下取整);

  2. 将余数保留至下一位。

但这样就会有个问题, 有一堆前导零在前面站位 ,所以,就需要去前导零。代码如下:

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 114514110000=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} 45141100=341434141100=231423141100=121412141100=114
商4余114,除数缩小,继续求商:
114 − 110 = 4 114-110=4 114110=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 归并排序

归并排序就是利用分治的特性: 将一个问题分解为多个子问题 ,来进行排序。具体步骤如下:

  1. 分解 需要排序的序列直到每个序列 只剩一个数
  2. 一个数的序列一定是有序的。
  3. 合并,使用 双指针法 进行合并,这样能够保证顺序不乱。

我们举个例子比如现在有一列数:5 3 2 6 1 4,我们模拟一下归并排序的过程:

  1. 拆分为5 3 26 1 4两个序列。
  2. 拆分为5 326 14这 4 个序列。
  3. 拆分为532614这 6 个序列。
  4. 合并第 1、2 个序列,同时合并第 4、5 个序列,此时数列中的顺序是3 521 64
  5. 合并第 1、2 和第 3、4 两个区间,得到2 3 51 4 6两个序列。
  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=1nin)=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=1i=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=1i=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=1i=m(j=0j=cipij)
这个式子叫做 正整数的所有约数和表达式


0x720 约数

我们知道了一个正整数的约数和和约数个数,但是我们还是不能知道这个正整数的约数到底是什么。想要知道具体的约数,需要枚举实现。

但是约数有一个性质:它是成对出现的(完全平方数除外)。

假如一个正整数 a a a N N N 的约数,且 a ≤ N a\le \sqrt N aN ,那么必然会有一个正整数 b b b N N N 的约数,且 b ≥ N b\ge \sqrt N bN

而且根据约数的性质,我们完全可以断定,这个 b b b 就是 N a \frac{N}{a} aN

因此,和质数的判定对应地,我们使用试除法。同样只需要枚举 1 − N 1-\sqrt N 1N 的所有数,每枚举到一个可被 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是否在同一个集合
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值