目录
第二题:a smple math problem(矩阵快速幂)
板块一:快速幂
前言:如何计算呢?
本来我们要算b次乘法,但是,如果b是偶数呢?那么我们可以这样,现在我们乘法就就只用做一半的次数,大大降低了运算量。为了凑出偶数,如果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(矩阵快速幂)
闲聊:博主第一次接触这种类型的题目(虽然久闻大名,而且知道大概想法),这里引用别人的代码(写上自己的注解,并修改部分)。快速幂的模板不熟,而且不知道如何将二维数组引用到乘法函数内,没想到结构体居然还有这种妙用。
下面代码:
#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(矩阵快速幂)
闲聊:1,我们要构造出递推公式的变换矩阵,作为底数radix
2,PA=B我通过分块矩阵的方法,将原来的矩阵边上放一个n阶的单位矩阵,进行初等行变换,使得A变成B,然后就得到了目标矩阵P
3,A的形式是而B的形式是,我们只要知道线性递推的公式就不难求出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
闲聊:这题找到递推是关键,我们可以求反面。求不存在连续的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);
根据初等矩阵变换,可以得到矩阵。
下面是代码:
#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;
}
板块三:素数筛法
引入:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
质数的是不被比自己小的除1外的所有数整除的数,合数可以分解为若干个比自身小的质数的积,只需要所有的比自己小的质数都无法整除即可,所谓筛法,就是筛掉所有的合数,只需要遍历所有比它小的质数即可。这里运用的递推的思想。
枚举2,筛选掉所有含2的合数,3前面比它小的质数就是2,而且没有被2整除所以3是质数。下一个没有被划掉的数必然是质数,因为前面被划的都是合数,前面没有被划的都是已经枚举过的质数。
实现:时间复杂度
for (int i = 2; i <= n; i++) {
if (!book[i]) {//是否被划掉
for (int j = 2 * i; j <= n; j += i) {//从两倍开始都是合数
book[j] = 1;//划掉
}
}
}
但是,有些因子众多的合数会被重复划掉,浪费一些时间。
优化实现:时间复杂度接近
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。大功告成。