【数论相关习题集】(矩阵快速幂,母函数,素数筛法)(更新至8318字)(不定期更新)

目录

板块一:快速幂

第零题:板子题 

第一题:Tr A(矩阵快速幂)

第二题:a smple math problem(矩阵快速幂)

第三题:Lucky Coins Sequence

板块二:母函数

类型一:一般组合(砝码,硬币,诸如此类)

第一题:hdoj1085

类型二:最佳平分(1/3分也可)

第一题:洛谷P2392

第二题:是否可以平分 

板块三:素数筛法


板块一:快速幂

前言:a^{b}如何计算呢?

本来我们要算b次乘法,但是,如果b是偶数呢?那么我们可以这样a^{2 * b / 2} = (a^{2})^{b / 2},现在我们乘法就就只用做一半的次数,大大降低了运算量。为了凑出偶数,如果b是奇数的话,我们先取出一个底数a使得指数变成偶数,这样就可以进行减小运算次数的操作了。一直道所有的数都被取完,也就是指数为0,比如111个a相乘,我们先取一个a,然后扩大底数为a方,相当于两个两个取,次数次数为55,再取一个底数,再扩大。

没有改变数值,但是大大减少了重复的运算。

第零题:板子题 

话不多说看板子:

#include <bits/stdc++.h>
#define int long long//毒瘤写法:P
using namespace std;
int po(int rad, int ind) {
	int res = 1;
	while (ind) {
		if (ind & 1) {
			res *= rad;
			res %= 10;
		}
		ind >>= 1;
		rad *= rad;//这里可能会爆int
		rad %= 10;
	}
	return res;
}
main() {//毒瘤写法
	ios::sync_with_stdio(false);
	cin.tie(0);
	int a, b;
	while (cin >> a >> b) {
		cout << po(a, b) << '\n';
	}
	return 0;
}

第一题:Tr A(矩阵快速幂)

hdoj1575

闲聊:博主第一次接触这种类型的题目(虽然久闻大名,而且知道大概想法),这里引用别人的代码(写上自己的注解,并修改部分)。快速幂的模板不熟,而且不知道如何将二维数组引用到乘法函数内,没想到结构体居然还有这种妙用。

下面代码:

#include <bits/stdc++.h>
#define mod 9973//模
using namespace std;
struct matrix {//矩阵结构体,可以说这是张量吧
	int a[20][20];
};
matrix ori, res;
int n;
void init(int n) {//初始化
	memset(res.a, 0, sizeof(res.a));
	for (int i = 0; i < n; i++) {
		res.a[i][i] = 1;//单位阵E,线性代数知识
		for (int j = 0; j < n; j++) {
			cin >> ori.a[i][j];//ori.a存的底数矩阵
		} 
	}
}
matrix multiple(matrix a, matrix b) {//定义矩阵乘法
	matrix c;
	memset(c.a, 0, sizeof(c.a));//初始化,临时存储结果
	for (int i = 0; i < n; i++) {
		for (int k = 0; k < n; k++) {//k是用来扫描行或者列
			if (a.a[i][k] == 0) {//小小剪枝
				continue;
			}
			for (int j = 0; j < n; j++) {//A左乘B,i行j列为A的i行,B的j列的各元素的乘积之和
				c.a[i][j] = (c.a[i][j] + a.a[i][k] * b.a[k][j] % mod) % mod;//该取模取模
			}//对了,模的运算,可以加,可以乘,可以减,不能除(乘法逆元)
		}
	}
	return c;
}
void matrix_mod(matrix ori, int k) {
	while (k) {
		if (k & 1) {//位运算,如果是奇数的话
			res = multiple(ori, res);//再结果里先取一个底数
		}
		ori = multiple(ori, ori);//底数翻倍
		k >>= 1;//位运算,舍弃最后一位,整除2
	}
	int ans = 0;
	for (int i = 0; i < n; i++) {
		ans = (ans + res.a[i][i]) % mod;//取模得答案
	}
	cout << ans << '\n';
}
int main() {
	int t, k;
	cin >> t;
	while (t--) {
		cin >> n >> k;
		init(n);
		matrix_mod(ori, k);
	}
	return 0;
}

第二题:a smple math problem(矩阵快速幂)

hdoj1757

闲聊:1,我们要构造出递推公式的变换矩阵,作为底数radix

2,PA=B我通过分块矩阵的方法,将原来的矩阵边上放一个n阶的单位矩阵,进行初等行变换,使得A变成B,然后就得到了目标矩阵P

3,A的形式是\binom{f(n)}{f(n -1)}而B的形式是\binom{f(n + 1)}{f(n)},我们只要知道线性递推的公式就不难求出P。

下面是代码,注意,要及时清空矩阵,否则答案会出错。

#include <bits/stdc++.h>
#define ll long long//超大数运算谁也不知道哪里对哪里错 
using namespace std;
int m;
struct mat{
	ll a[15][15];
};
mat rad;
mat mul(mat a, mat b) {
	mat c;
	memset(c.a, 0, sizeof(c.a));//!!!
	for (int i = 1; i <= 10; i++) {
		for (int j = 1; j <= 10; j++) {
			for (int k = 1; k <= 10; k++) {
				c.a[i][j] = (c.a[i][j] + (a.a[i][k] * b.a[k][j]) % m) % m; 
			}
		}
	}
	return c;
}
mat po(int ind) {
	mat ans;
	memset(ans.a, 0, sizeof(ans.a));//!!!
	for (int i = 1; i <= 10; i++) {
		ans.a[i][i] = 1;
	}
	while (ind) {
		if (ind & 1) {
			ans = mul(ans, rad);
		}
		ind >>= 1;
		rad = mul(rad, rad);
	}
	return ans;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int k;
	while (cin >> k >> m) {
		memset(rad.a, 0, sizeof(rad.a));//!!!
	for (int i = 1; i <= 10; i++) {
		cin >> rad.a[1][i];
	}
	rad.a[2][1] = rad.a[3][2] = rad.a[4][3] = rad.a[5][4] = rad.a[6][5] = rad.a[7][6] = rad.a[8][7] = rad.a[9][8] = rad.a[10][9] = 1;
	if (k <= 9) {
		cout << k % m << '\n';
	} else {
		mat ans = po(k - 9);
		cout << (ans.a[1][1] * 9 + ans.a[1][2] * 8 + ans.a[1][3] * 7 + ans.a[1][4] * 6 + ans.a[1][5] * 5 + ans.a[1][6] * 4 + ans.a[1][7] * 3 + ans.a[1][8] * 2 + ans.a[1][9] * 1) % m << '\n';
	}
	} 
	return 0;
}

第三题:Lucky Coins Sequence

hdoj3519

闲聊:这题找到递推是关键,我们可以求反面。求不存在连续的3个及以上的的相同面的硬币。记记为b(n),如果说bn的最后两个字符是相同的,那么,就相当于b(n-2)加上11或00,对于任意的b(n-2)的尾部我们都可以而且唯一可以找到一个11或者00,如果是不相同的,如果仍然找b(n-2)有时候是10可以,有时候是01可以,有时候都可以,这样就没办法确定了。因此我们找b(n-1)这样就能保证找到一个而且唯一找到1或者0使得尾部两个不同。

所以b(n) = b(n - 1) + b(n - 2);

b(n) + a(n) = 2 ^ n;

消掉b(n);

a(n) = a(n - 1)+a(n-2) + 2^{n-2}

\begin{bmatrix} a(n)\\ a(n-1)\\ 2^{n-1} \end{bmatrix}=\begin{bmatrix} 1 & 1 & 1\\ 1 &0 &0 \\ 0& 0 & 2 \end{bmatrix} *\begin{bmatrix} a(n-1)\\ a(n-2)\\ 2^{n-2} \end{bmatrix}

根据初等矩阵变换,可以得到矩阵。

下面是代码:

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int p = 10007;
struct mat {
	ll a[5][5];
};
mat mul(mat a, mat b) {
	mat c;
	memset(c.a, 0, sizeof(c.a));
	for (int i = 1; i <= 3; i++) {
		for (int j = 1; j <= 3; j++) {
			for (int k = 1; k <= 3; k++) {
				if (!a.a[i][k] || !b.a[k][j]) {
					continue;
				}
				c.a[i][j] = (c.a[i][j] + (a.a[i][k] * b.a[k][j]) % p) % p;
			}
		}
	}
	return c;
}
mat po (mat rad, int ind) {
	mat res;
	memset(res.a, 0, sizeof(res.a));
	for (int i = 1; i <= 3; i++) {
		res.a[i][i] = 1;
	}
	while (ind) {
	 	if (ind & 1) {
	    	res = mul(res, rad);
    	}
    	rad = mul(rad, rad);
    	ind >>= 1;
	}
	return res;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	mat rad;
	memset(rad.a, 0, sizeof(rad.a));
	rad.a[1][1] = rad.a[1][2] = rad.a[1][3] = 1;
	rad.a[2][1] = 1;
	rad.a[3][3] = 2;
	int n;
	while (cin >> n) {
		if (n <= 3) {
			if (n <= 2) {
				cout << 0 << '\n';
			} else {
				cout << 2 << '\n';
			}
		} else {
			mat res = po(rad, n - 2);
			cout << res.a[1][3] * 2 % p << '\n';
		}
	}
	return 0;
} 

板块二:母函数

母函数是用来解决组合问题的一大利器。

板子合集:

#include <bits/stdc++.h>//母函数 
using namespace std;
int c1[50], c2[50], v[10], n1[10], n2[10];
int main()
{
	memset(n1, 0, sizeof(n1));//初始化 
	int t;
	scanf("%d", &t);
	while (t--)
	{
		int n, k;
		scanf("%d %d", &n, &k);
		for (int i = 0; i < k; i++)
		{
			scanf("%d %d", &v[i], &n2[i]);//录入数据 
		}
		memset(c1, 0, sizeof(c1));//初始化 
		memset(c2, 0, sizeof(c2));
		c1[0] = 1;//有效乘一次,系数加一,c1存储的是结果多项式的各项系数,标号为次数 开始是一 
		for (int i = 0; i < k; i++)//核心代码 ,循环每个因子 
		{
			for (int j = n1[i]; j <= n2[i] && j * v[i] <= n; j++)//n1是起始次数,n2是终止次数,注意次数不能超过最大数 
			{
				for (int k = 0; k + j * v[i] <= n; k++)//遍历结果多项式,中可以相乘的,并记录 
				{
					c2[k + j * v[i]] += c1[k];//此处,c2是临时数据大小与结果多项式相同,拿一和第一个多项式乘,然后第一个多项式每个系数相应加 
				}// 
			}
			for (int j = 0; j <= n; j++)//这只是一个转存器而已 
			{
				c1[j] = c2[j];
				c2[j] = 0;
			}
		}
		printf("%d\n", c1[n]);
	}
	return 0;
 }

拆解:

核心代码:

		for (int i = 0; i < all; i++)//核心代码 ,循环每个因子 
		{
			for (int j = n1[i]; j <= n2[i] && j * v[i] <= n; j++)//n1是起始次数,n2是终止次数,注意次数不能超过最大数 
			{
				for (int k = 0; k + j * v[i] <= n; k++)//遍历结果多项式,中可以相乘的,并记录 
				{
					c2[k + j * v[i]] += c1[k];//此处,c2是临时数据大小与结果多项式相同,拿一和第一个多项式乘,然后第一个多项式每个系数相应加 
				}// 
			}
			for (int j = 0; j <= n; j++)//这只是一个转存器而已 
			{
				c1[j] = c2[j];
				c2[j] = 0;
			}
		}

注意这里的含义,最外层表示遍历每一个因子,第二层表示遍历所有可能的系数,n1是起始系数数组,表示对于每一个因子最小可以取到的系数,注意这里必须是非负的,一般都是零,n2是终止系数,指的是最大可以取到的系数,注意v[i]数组表示的是价值,系数 X 价值 = 因子中的对应x的幂的指数,前两层模拟出第一个多项式,第三层执行乘法操作,这里c1的含义是存储的当前累积的多项式,注意这里c1[k]表示的是对应指数的项的系数,这里我们要遍历c1的每一项,注意,最开始的时候我们要将c1数组赋值为100000000000000。它表示i的多项式是(1),c2表示的是历史累积乘以当前因子的新积,注意相乘之后的指数变成了k + j * v[i],所以给c2对应的系数得到了增加。然后把新的c2转存到c1内,并把c2把它置零。依次类推。

类型一:一般组合(砝码,硬币,诸如此类)

第一题:hdoj1085

这里又双叒叕把数组开小了,TLE。注意,清空要清空到sum+1,否则会wa。

下面是代码。

#include <bits/stdc++.h>
using namespace std;
const int N = 10005;
int c1[N], c2[N];
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int v[5] = {1, 2, 5}, n2[5];
	while (cin >> n2[0] >> n2[1] >> n2[2], n2[0] + n2[1] + n2[2]) {
		int sum = n2[0] * 1 + n2[1] * 2 + n2[2] * 5;
		for (int i = 0; i <= sum + 1; i++) {
			c1[i] = 0;
			c2[i] = 0;
		}
		c1[0] = 1;
		for (int i = 0; i < 3; i++) {
			for (int j = 0; j <= n2[i]; j++) {
				for (int k = 0; k <= sum; k++) {
					c2[k + v[i] * j] += c1[k];
				}
			}
			for (int j = 0; j <= sum; j++) {
				c1[j] = c2[j];
				c2[j] = 0;
			}
		}
		for (int i = 0; i <= sum + 1; i ++) {
			if (!c1[i]) {
				cout << i << '\n';
				break;
			}
		}
	}
	return 0;
}

类型二:最佳平分(1/3分也可)

题目的要求是平分物品使得,最接近平均数。这是一种思路清奇的做法,先列出所有可能的组合,再从平均数附件查找。

第一题:洛谷P2392

这里从后半找,因为前半的时间是被后半包含的。不过前半也可以,因为两半脑子的总时间是知道的,可以求出。

下面是代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1205;
int c1[N], c2[N], v[25];
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int ans = 0;
	int t[5];
	cin >> t[0] >> t[1] >> t[2] >> t[3];
	for (int i = 0; i < 4; i++) {
		int sum = 0;
		for (int j = 0; j < t[i]; j++) {
			cin >> v[j];
			sum += v[j];
		}
		for (int j = 0; j <= sum; j++) {
			c1[j] = 0;
			c2[j] = 0;
		}
		c1[0] = 1; 
		for (int j = 0; j < t[i]; j++) {
			for (int k = 0; k <= 1; k++) {
				for (int l = 0; l + k * v[j] <= sum; l++) {
					c2[l + k * v[j]] += c1[l];
				} 
			}
			for (int k = 0; k <= sum; k++) {
				c1[k] = c2[k];
				c2[k] = 0;
			}
		}
		for (int j = (sum + 1) / 2; j <= sum; j++) {
			if (c1[j]) {
				ans += j;
				break;
			}
		}
	}
	cout << ans << '\n';
	return 0;
} 

第二题:是否可以平分 

这道题如果直接用母函数去做,会超时,于是考虑取模将重复的问题去掉,mod几呢?看了其他人的题解是mod60,刚好是最小公倍数,取模要保证不改变可能性。

原来如果是可以平分的,加上这六十个仍然可以平分,这是很容易的。

原来如果是不可以平分的话,必然多一个1或者一个2,或者一个3,或者一个4,或者一个5,或者一个6。取出一个1。

如果是60个1,多出一个1,照样不行,多出一个2,不可能,多出一个3,4也不可能,5照样不行

如果是60个2,多出一个1,照样不行,多出一个2,照样不行,不可能多出一个4,多出一个5,照样不行。

如果是3过量,多出一个1,照样不行,等等。。。

mod 30也可,

原来能分的分掉,1~6各最多多一个,有残量,试了一下最小可以取到10,有点玄学还是取最小公倍数稳妥些。

经过考证,以上都是错的,能过纯属数据太弱,建议用多重背包解决。

#include <bits./stdc++.h>
using namespace std;
const int N = 120005;
int n2[10], c1[N], c2[N]; 
int main() {
	int I = 1;
	while (cin >> n2[0] >> n2[1] >> n2[2] >> n2[3] >> n2[4] >> n2[5], accumulate(n2, n2 + 6, 0)) {
		int sum = 0;
		for (int i = 0; i < 6; i++) {
			n2[i] %= 60;
			sum += n2[i] * (i + 1);
		}
		for (int i = 0; i <= sum; i++) {
			c1[i] = 0;
			c2[i] = 0;
		}
		c1[0] = 1;
		if (sum % 2) {
			printf("Collection #%d:\nCan't be divided.\n\n", I++);
			continue;
		}
		sum /= 2;
		for (int i = 0; i < 6; i++) {
			for (int j = 0; j * (i + 1) <= sum && j <= n2[i]; j++) {
				for (int k = 0; k + j * (i + 1) <= sum; k++) {
					c2[k + j * (i + 1)] += c1[k];
				}
			}
			for (int j = 0; j <= sum; j++) {
				c1[j] = c2[j];
				c2[j] = 0;
			}
		}
		if (c1[sum]) {
			printf("Collection #%d:\nCan be divided.\n\n", I++);
		} else {
			printf("Collection #%d:\nCan't be divided.\n\n", I++);
		}
	}
	return 0;
}

板块三:素数筛法

引入:

12345678910
11121314151617181920
21222324252627282930
31323334353637383940
41424344454647484950
51525354555657585960
61626364656667686970
71727374757677787980
81828384858687888990
919293949596979899100

质数的是不被比自己小的除1外的所有数整除的数,合数可以分解为若干个比自身小的质数的积,只需要所有的比自己小的质数都无法整除即可,所谓筛法,就是筛掉所有的合数,只需要遍历所有比它小的质数即可。这里运用的递推的思想。

枚举2,筛选掉所有含2的合数,3前面比它小的质数就是2,而且没有被2整除所以3是质数。下一个没有被划掉的数必然是质数,因为前面被划的都是合数,前面没有被划的都是已经枚举过的质数。

实现:时间复杂度O(nloglogn)

for (int i = 2; i <= n; i++) {
	if (!book[i]) {//是否被划掉
		for (int j = 2 * i; j <= n; j += i) {//从两倍开始都是合数
			book[j] = 1;//划掉
		}
	}
}

但是,有些因子众多的合数会被重复划掉,浪费一些时间。

优化实现:时间复杂度接近O(n)

for (int i = 2; i * i <= n; ++i) {//
		if(!a[i]){
			for(int j = i * i; j <= n;j += i) {
				a[j] = 1;
			}
		}
	}

解释:

j = i * i,例如j = 2 * 2, 2 * 3 2 * 4...

j = 3 * 3 , 3 * 4, 3 * 5

其实任何一个合数可以分解成,一个最小的质数和另一个数的积。为什么这样做可以避免重复枚举合数呢?首先i是一个质数,i * (i + k)是合数,2 * 2,2 * 3,2 * 4 ……3 * 3,3 * 4,3 * 5, 3 * 6,其中虽然有重复,比如 2 * 9 = 3 * 6 但是,我们可以不去计算i * (i - k),i - k都是是已经枚举过的质数的倍数(1倍或几倍),在n的数很大的时候优化的力度很大,如过i * i > n的下面的for循环无法再找到新的质数,因为j > n。大功告成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值