总结:
1、对于素数筛、线性筛,我们需要非常熟悉这些代码,然后知道该如何对其进行更改来解决相对应的问题。
2、这道题,似乎也可以改线性筛的代码来满足要求,但似乎写起来不如素数筛简单;
总结:
1、对于素数筛、线性筛,我们需要非常熟悉这些代码,然后知道该如何对其进行更改来解决相对应的问题。
2、这道题,似乎也可以改线性筛的代码来满足要求,但似乎写起来不如素数筛简单;
总结
1、线性筛开两个数组,是很合适的方式,一个记录第几个素数,另一个可以判断是否为素数;
总结:
1、二分查找也有递归写法,如果只需要返回True或者False的话。
2、我认为有一个更占用空间的写法,就是先减去二倍的平方数,再判断得到的数字是否是素数,这样会更简单也更节省时间,因为每个单词可以只需要判断1次if即可;目前,每个单词都需要被二分查找确认一次;不过确实解题思路有很多。
总结:
1、素勾股数的算法·要记住!
for (int n = 1; n <= 32; n++) {
for (int m = n + 1; m <= 32; m++) {
if (gcd(m, n) - 1) continue; //gcd(m, n) - 1 对偶逻辑 gcd(m, n) != 1
int a = m * m - n * n;
int b = 2 * m * n;
int c = m * m + n * n;
}
}
总结:
1、memset,memcopy,memcmp都是按字节来批量修改,移动,比较的函数。
2、本来是一个开辟二维数组,然后挨个往里面存储;现在是开辟一个一维的指针数组,而储存每个计算后的数字的数组,在计算的时候再开辟出来,计算完以后返回数组地址即可。
3、这是暴力方法,还有整数素因子表示法,筛法思想等写法,可以提升效率;只需要比较素因子分解后,每个素数位上的幂次,是否相等即可,不需要计算出来,这应该要快得多,可能就是整数素因子表示法;
4、result[result_len++] = temp; //这样比用memcpy,memset更快;为什么呀?因为不用复制过去,已经用指针计算过一次了!
总结:
1、memset是对较大的结构体或数组进行清零操作的一种最快方法!
void *memset(void *s, int ch, size_t n);
函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法 [1] 。
2、解题要抓住,最主要的判断函数,这里就是计算循环节的位数,并了解其特点,1/n的循环节不超过其所有余数可能性,因此不超过n-1;
总结:
1、要把每个变量表示的语义信息,清晰地清楚地确认明白!有利于有许多变量进行复杂变化的过程。
例如get_num(int k, int n, int &x)函数中,涉及到
k 表示还有多少个可能状态数需要跳跃?第一百万,还需要按顺序跳跃k = 999999个可能状态数;
n 表示,我当前这个位置数字每向上变化一次,跳跃了多少个可能状态数:是一个阶乘数,fac[i - 1];
step = k / n 我利用当前这个位置数字,最多可以向上变化几次,来最大程度减少需要跳跃的可能状态数。(保证按顺序跳跃,维持字典序)
x 表示当且这个位置上的为数字几,以及最后向上跳跃变化为了数字几?
i和num[i] 这个数字,有没有用过?(num[i] = 1则可用,= 0则不可用;只能用处于后面的数字,前面的数字都根据需要安排好了)
t 用于遍历数字i,看看向上变化,还有哪些数字可以用,没用过,以及变化step次会停在哪个没有用过的数字i上;
2、为什么不是这里写地址&x,上面写指针*x呀?而是这里写变量x,上面写该变量的地址&x?对于更改x而言,是等价的写法吗?
int get_num(int k, int n, int &x) //传一个“引用”进去?
......
k = get_num(k, fac[i - 1], x)
int get_num(int k, int n, int *x)
......
k = get_num(k, fac[i - 1], &x)
#include<stdio.h>
void myswap1(int x, int y)
{
int t;
t=x;
x=y;
y=t;
}
void myswap2(int *p1, int *p2)
{
int t;
t=*p1;
*p1=*p2;
*p2=t;
}
void myswap3(int &x, int &y)
{
int t;
t=x;
x=y;
y=t;
}
int main()
{
int a,b;
a = 2;
b = 3;
myswap1(a,b); //作为对比,直接交换两个整数,显然不行
printf("调用交换函数后的结果是:%d 和 %d\n", a, b);
a = 2;
b = 3;
myswap2(&a,&b); //交换两个整数的地址
printf("调用交换函数后的结果是:%d 和 %d\n", a, b);
a = 2;
b = 3;
myswap3(a,b); //直接以变量a和b作为实参交换
printf("调用交换函数后的结果是:%d 和 %d\n", a, b);
return 0;
}
在第一个程序中,传值不成功的原因是指在形参上改变了数值,没有在实参上改变数值。
在第二个程序中,传地址成功的原因利用指针改变了原来的地址,所以实参就交换了。
在第三个程序中,引用是直接改变两个实参变量a,b的值,所以就交换了。
注:引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名;
【例】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名
说明:
(1)&在此不是求地址运算,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
ra=1; 等价于 a=1;
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
(6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
总结:
1、为什么第四步编写程序,也算一步?
分两种:
一种是正向递推,最后需要回溯回去;
第二种是逆向递推,也叫伪递推,这里体现的是就伪递推,从最后一层往第一层递推;
总结:
1、记忆化搜索,需要开一个新的数组,然后在新的递归之前,直接返回累加值;
#define max_n 20
int val[max_n][max_n] = {0};
int keep[max_n][max_n] = {0};
int dfs(int i, int j, int n) {
if (i + 1 ==n) return val[i][j];
if (keep[i][j]) return keep[i][j]; //在新的递归开始前,直接返回累加值,正是这个返回记录值,减少了递归的次数。
int val1 = dfs(i + 1, j, n);
int val2 = dfs(i + 1, j + 1, n);
return keep[i][j] = (val1 > val2 ? val1 : val2) + val[i][j]; //记忆化。
}
2、深度优先搜索的写法要记住,这是一种很简洁的写法。
#define max_n 20
int val[max_n][max_n] = {0};
int dfs(int i, int j, int n) {
if (i + 1 == n) return val[i][j];
int val1 = dfs(i + 1, j, n);
int val2 = dfs(i + 1, j + 1, n);
return (val1 > val2 ? val1 : val2) + val[i][j];
}
3、DFS(i, j, n)从位置(i,j)向下走n步的最大值;
总结:
1、注意遍历上限是否可以降低,也可以提升效率
总结:
1、大整数处理用数组,第一位的作用是记录该数字的总位数,可以用len = strlen(str)来提取;逐位相加,超过10,则进位;到最高位仍然超过10则位数+1;
2、注意三点:
①数组每一位就0-9,因此用开字符型数组,char str[]来接收大整数字符串!然后用整型数组,int ans[]来把大整数字符串变为大整数的整型串,从而得以计算加法;
②要注意一开始用读到的是最高位,因此要存放在str[len]上!然后持续往下存放!
②要注意把读到的数字字符转成数字:ans[len - i] = str[i] - '0';
总结:
1、用到素数的时候,先把线性筛函数写出来,然后再根据题目需求进行相应的更改。
线性筛得记下来:
int prime[max_n + 5] = {0};
void init() {
for (int i = 2; i <= max_n; i++) {
if (!prime[i]) prime[++prime[0]] = i;
for (int j = 1; j <= prime[0]; j++) {
if (prime[j] * i > max_n) break;
prime[prime[j] * i] = 1;
if (i % prime[j] == 0) break;
}
}
return ;
}
对于任意正整数N,设它的质因数分解为
(p1^n1)*(p2^n2)*......*(pk^nk),其中n1, n2, ..., nk > 0, p1, p2, ..., pk 为素数
它的所有约数个数为(n1 + 1) *(n2 + 1) *...... *(nk + 1),记作F(n)
若C=A*B,且A,B互素,那么C的约数个数F(C)为多少?
F(C) = F(A) × F(B) ——因为互素,所以没有相同的素因子,因此做笛卡尔积;
枚举n,则第n个三角形数的约数个数F(n)为
F(n) = F(n(n+1)/2)
= F(n/2) × F(n+1) 当n为偶数
= F(n) × F((n+1))/2) 当n为奇数
用线性筛的代码框架,实现了求每个正整数的约数个数;
但是由于其中添加了一个while循环,破坏了整个代码的“优美性”;
有没有快速的方法,知道现在正整数最小素因子的幂次是多少?
有用记录式,而非计算式,这样就是稳稳地O(n)时间复杂度了。
开一个新的数组,记录最小的素因子的次幂,即可省略while来计算cnt了。
总结:
1、算法很重要。素勾股数很重要;
找素勾股数,然后作为“勾股数的基底/秩”,来向上寻找a,b,c;
3 4 5,6 8 10, 9 12 15,......
可以枚举m和n,可以构造一组素勾股数
如果a b c三者互质(它们的最大公因数是1),他们就成为素勾股数:a^2 + b^2 = c^2
素勾股数具有如下性质:
性质1、他们的整数倍也是勾股数;
性质2、a b c之间两两互质
性质3、a、b必为一奇一偶
性质4、任何素勾股数均可表示为如下形式,其中n < m,且gcd(n, m) = 1
a = 2 * n * m
b = m^2 - n^2
c = m^2 + n^2
总结:
1、二分查找的写法:函数指针,查找上界,查找值;
int binary_search(int (*func)(int), int n, int x)
int1 binary_search(int1 (*func)(int1), int1 n, int1 x) {
int1 head = 1, tail = n, mid;
while (head <= tail) {
mid = (head + tail) >> 1;
if (func(mid) == x) return mid;
if (func(mid) < x) head = mid + 1;
else tail = mid - 1;
}
return 0;
}
就是,第一个,差值为五边形数的一对五边形数
也可以解方程Pk - Pj = Pn
枚举到多少才能得到答案呢?可以找到这样一个枚举上界;
如何判断当前得到的和值或者差值,是五边形数?
方法一、如果我生成一个五边形数字的表, 把他放进去即可;那这个表需要设计成多大呢?
方法二、二分查找(算法时间复杂度O(logN);不仅可以用来在数组中查找元素,还可以用来求解单调函数的解。
总结:
1、i×1,链接进ans;i×2,链接进ans;直到超过9位,则不再×n,不再链接进ans;
while (digits(ans) < 9) {
ans *= pow(10, digits(x * n));
ans += n * x;
n += 1;
}
总结
1、判断数字是否有重复,我们可以开一个数组,看数字对应下标位置是否为1,来判断;类似于标记合数;
这里有两处:
①判断a, b, a * b是否有重复数字:
while (n) {
if (num[n % 10]) return 0;
num[n % 10] += 1;
n /= 10;
}
//长度为11的数组,每个数字挨个求10的模的时候,如果该对应下标位置上不为0,说明此前已经有余数为其赋值1了,因此这个数字重复了;
②判断a * b是否有重复数字:
int keep[max_n + 5] = {0}
if (keep[a * b]) continue;
sum += a * b;
keep[a * b] = 1;
//长度为(a * b的上限+5)的数组,如果a * b位置上不为0,说明此前已经有a * b为其标记过了,因此这个a * b重复了;
2、求位数的函数,记得添加头文件,这个要记下来
#include <math.h>
int digits(int n) {
if (!n) return 1;
return floor(log10(n)) + 1;
}
3、求最大公约数的函数,也要记下来
#include <tinttypes.h>
int gcd(int32_t a, int32_t b) {
if (!b) return a;
return gcd(b, a % b);
}
4、和素数有关的一个整体解题结构大概是:
#include <stdio.h>
#include <int32_t.h>
#include <inttypes.h>
#define max_range ....
int32_t isPrime[max_range + 5] = {0}; //判断是否是素数
int32_t prime[max_range + 5] = {0}; //记录素数数组,用于遍历素数
void initprime(int n) {
线性筛
......
}
void is_val(int n) {
循环素数?;
可截素数?;
......
}
void solve() {
遍历素数数组:prime[i]
判断是否is_val
return 累加或者累乘的结果
}
int main() {
init
int many = solve()
printf("%d\n", many);
return 0;
}
遇到复杂的判定条件,或者遇到根据不同情形来计算的值,就声明一个函数来处理;
总结:
1、降成一维的思路就是:
m元钱的组合种类为:
用0种 +(只用前1种且至少用了第1种货币)+(只用前2种且至少用了第2种货币)+(只用前3种且至少用了第3种货币)+ ... +(只用前8种且至少用了第8种货币)的总和;
他们之间是互斥的,因此把他们一层一层地计算,然后一层一层地累加起来即可,这样只需要一维数组。
f(n, m) = f(n-1, m) + f(n, m - w[n])
f(n+1, m) = f(n, m) + f(n+1, m - w[n+1])
f(n+1, m) = f(n-1, m) + f(n, m - w[n]) + f(n+1, m - w[n+1])
f(n+1, m) = f(n-2, m) + f(n-1, m - w[n-1]) + f(n, m - w[n]) + f(n+1, m - w[n+1])
f(n+1, m) = ......
f(n+1, m) = f(0, m) + f(0, m - w[0]) + f(1, m - w[1]) + ... + f(n, m - w[n]) + f(n+1, m - w[n+1])
2、当写递推算法出错的时候,要注意查看数组下标的合法性,看下标是否超过了数组的大小,判断下标的边界条件;
其他:
这是一个递推算法;
f(n, m)表示前n种钱币拼凑m元钱的所有不同的情况总数;
f(n, m) = f(n-1, m) + f(n, m - w[n]),其中w[n]位第n元钱的面值大小
容斥原理(并集算法)
A+B-A∩C
f(n, 0)为多少?f(n, 0) = 1
我们考虑以下数值及其公式:
f(1, 1) = f(0, 1) + f(1, 0) = 0 + 1
f(2, 2) = f(1, 2) + f(2, 0) = 1 + 1
f(3, 5) = f(2, 5) + f(3, 0) = 3 + 1
f(4, 10) = f(3, 10) + f(4, 0) = (1+3+4) + 1
以此类推,因此f(n, 0) = 1
此时,开[8][200]的数组,即可。
真的需要开那么多项吗?
像斐波那契用滚动数组,只需要开3项,甚至2项也足以;
这里也可以相应地进行修改。
因为,只用了这一层上一个数和上一层同样位置的数:
i = 1,从1到200标记完
i = 2,从1到200标记完
i = 3,从1到200标记完
i = 4,从1到200标记完
i = 2,从1到200标记完
...
i = 8,从1到200标记完
这样就可以把8项(8种币值的货币)缩小成2项,即可滚动;
总结:
1、这里用一个二维数组,把位数,分配到二维数组的第二维度上,按位写了一个数学加法运算式;
2、用[0]位,表示有多少位;用[1], [2], [3]来表示,第1位的数字,第2位的数字,第3位的数字;
3、整个过程:999 + 999= 9|9|9 + 9|9|9= 18|18|18 = 18|19|8 = 19|9|8 = 1|9|9|8
4、用指针变量取滚动数组的地址;当n增长的时候,不断地按照012012012的顺序,不断替换*a, *b, *c所对应的地址,来更新下一个位置上的值。
5、不要忽略中间a[0] = b[0];的更新位数的操作;
总结:
1、写代码,除了掌握基本的算法、代码工地,还要对操作系统,计算机有深刻了解,要知道计算机是如何执行的。
2、搜索算法中经常用到记忆化搜索;
int keep[max_n + 5] = {0};
......
if (n <= size && keep[n]) return keep [n]; //这里n <= max_n && keep[n]不能写反了,写成keep[n] && n <= max_n
3、size实验中了解到,记忆化要考虑缓存到底能存多大的内容;
size real
100 0m1.153s
1000 0m0.652s
1000000 0m0.047s
10000000 0m0.094s
这和计算机底层有关
缓存有三个等级:
L1,速度最快,容量最小
L2,速度居中,容量居中
L3,速度最慢,容量最大,最多也就8MB
先从内存读到缓存当中,当前最大的缓存是L3级的,但是也很小,大概也是8MB左右,相当于八百万个字节;
要涉及到页面置换的问题,有可能加载进来的数据,没有当前要找的值,因此要换一页,要分页进行加载,还有命中率的问题。
因此,记忆化,要考虑缓存到底能存多大的内容。
写好代码,要做哪些
1、算法,代码功底
2、要对操作系统,计算机有深刻了解,要知道计算机是如何执行的;
要衡量一天能准确地有价值地完成的代码量的多少;
总结:
1、爆栈的时候,当前的数字n如果超过了int的数值表示范围,会发生什么?首先,数值会一直增加,然后增加到一个负值,最后爆栈了;可以用
if (n < 0) printf("erro\n");
来检查;最后将整型修改为long long