- 穷举
- 无遗漏
- 无冗余
- 如何穷举
- 如何聪明穷举
技巧大概就是,把大的问题细化到一个点,先研究在这个小的点上如何解决问题,然后再通过递归/迭代的方式扩展到整个问题
排序
归并、快排都使用了分治的思想
- 稳定的排序算法:冒泡、插入、归并、计数、桶、基数
- 不稳定:选择、希尔、快速、堆

快速排序
什么时候最差(退化成冒泡排序)
RSA
- DSA
- ECDSA
- ECC
- DH
- DSS
-
对称加密算法
AES
- DES
-
国密算法
即国家商用密码算法。是由国家密码管理局认定和公布的密码算法标准及其应用规范,其中部分密码算法已经成为国际标准。如SM系列密码
贪心算法
一般用来求最值
需要满足贪心选择性质: 局部最优—>全局最优
这就是贪心思路的本质,如果找不到重复计算,那就通过问题中一些隐藏较深的规律,来减少冗余计算。
一般找规律
要有贪心选择性质动态规划
问题性质
- 具备最优子结构
- 一个问题的最优解可以由其子问题的最优解有效地构造出来
- 存在重叠子问题
- 果一个问题可以被分解成若干个子问题,且这些子问题会重复出现
- 一般用来求最值
- 具备最优子结构
- 正确的状态转移方程(问题不同规模的关系)
- 解法
- 明确base case -> 明确状态 -> 明确选择 -> 定义dp数组/函数
- 遍历顺序应该是以base case 为起点向结果靠近
- 最后一步优化,如果我们发现每次状态转移只需要 DP table 中的一部分,那么可以尝试缩小 DP table 的大小,只记录必要的数据,从而降低空间复杂度
- 状态是什么?在问题分解(状态转移)的过程中变化的,就是状态。变化量
- 动态规划算法本质上就是穷举「状态」,然后在「选择」中选择最优解。
- 选择, 也就是导致状态产生变化的行为
- 一般用到数学归纳法
- 根据 dp 数组的定义,运用数学归纳法的思想,假设 dp[0…i-1] 都已知,想办法求出 dp[i],一旦这一步完成,整个题目基本就解决了。
- 但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组
- 1、遍历的过程中,所需的状态必须是已经计算出来的。
- 2、遍历结束后,存储结果的那个位置必须已经被计算出来。
数组技巧
前缀和
前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。
前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和
差分数组
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减
递归
不要跳进递归,而是利用明确的定义来实现算法逻辑
1、这个函数是干嘛的?
2、这个函数参数中的变量是什么的是什么?
3、得到函数的递归结果,你应该干什么?
当数据结构和算法是递归时,适合使用
写递归算法的一个技巧就是不要试图跳进递归细节,而是从递归框架上思考,从函数定义去理解递归函数到底该怎么实现
PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。
递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。
双指针
链表, 数组 字符串, 回文串,常用
快慢双指针,首尾双指针,滑动窗口等等,
滑动窗口
二分
回溯
怎么做选择
解决一个回溯问题,实际上就是一个决策树的遍历过程
站在回溯树的一个节点上,你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
defbacktrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return//相当于多叉树的遍历,i的个数从2变成了n,并且写成了for循环的形式
for 选择 in 选择列表:
路径.add(选择)
backtrack(路径, 选择列表) //将数值改回成原来的状态 路径.remove(选择)
我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。
BFS
// 计算从起点 start 到终点 target 的最近距离int BFS(Node start, Node target) {Queue<Node> q; // 核心数据结构Set<Node> visited; // 避免走回头路q.offer(start); // 将起点加入队列visited.add(start);int step = 0; // 记录扩散的步数while (q not empty) {int sz = q.size();/* 将当前队列中的所有节点向四周扩散 */for (int i = 0; i < sz; i++) {Node cur = q.poll();/* 划重点:这里判断是否到达终点 */if (cur is target)return step;/* 将 cur 的相邻节点加入队列 */for (Node x : cur.adj()) {if (x not in visited) {q.offer(x);visited.add(x);}}}/* 划重点:更新步数在这里 */step++;}}
位运算
利用或操作 | 和空格将英文字符转换为小写
利用与操作 & 和下划线将英文字符转换为大写
利用异或操作 ^ 和空格进行英文字符大小写互换
判断两个数是否异号
int x =-1, y =2;boolean f =((x ^ y)<0);// true
不用临时变量交换两个数
int a = 1, b = 2;a ^= b;b ^= a;a ^= b;
加一
int n = 1;n = -~n;
减一
int n = 1;n = ~-n;
n & (n-1) 这个操作是算法中常见的,作用是消除数字 n 的二进制表示中的最后一个 1
一个数和它本身做异或运算结果为 0,即 a ^ a = 0;一个数和 0 做异或运算的结果为它本身,即 a ^ 0 = a。异或运算满足交换律和结合律
i = i & (i-1),统计i二进制中有多少个1
i = i | (i+1),统计i二进制中有多少个0
并查集
顾名思义,并查集是来解决图的连通性问题
- Union — 连接两个节点
Find — 查找所属的连通分量 ```cpp class UnionFind { private:
vector<int> parents;vector<int> rank;
public:
int unionCount = 0;UnionFind(vector<vector<int>>& isConnected) {int n = isConnected.size();parents.resize(n);rank.resize(n, 0);for (int i = 0; i < n; ++i) {parents[i] = i;}}void _union(int x, int y) {int rootX = find(x);int rootY = find(y);if (rootX != rootY) {if (rank[rootX] > rank[rootY]) {parents[rootY] = rootX;} else if (rank[rootX] < rank[rootY]) {parents[rootX] = rootY;} else {parents[rootY] = rootX;rank[rootX] += 1;}++unionCount;}}int find(int x) {if (parents[x] == x) {return x;}int p = parents[x];parents[x] = find(p);return parents[x];}
};
// 按照大小的
class UnionFind {
private:
vector
public:int ans = 0;UnionFind(vector<vector<int>>& grid) {int m = grid.size();int n = grid[0].size();parents.resize(m * n);size.resize(m * n);for (int i = 0; i < m; ++i) {for (int j = 0; j < n; ++j) {if (grid[i][j] == 1) {int k = i * n + j;parents[k] = k;size[k] = 1;if (ans != 1) {ans = 1;}}}}}void _union(int x, int y) {int xRoot = find(x);int yRoot = find(y);if (xRoot != yRoot) {if (size[yRoot] <= size[xRoot]) {parents[yRoot] = xRoot;size[xRoot] += size[yRoot];ans = max(size[xRoot], ans);} else {parents[xRoot] = yRoot;size[yRoot] += size[xRoot];ans = max(size[yRoot], ans);}}}// 带压缩路径的查找int find(int x) {if (x == parents[x]) {return x;}int p = parents[x];parents[x] = find(p);return parents[x];}
};
```javaclass UnionFind{private int[] parent;private int[] rank; // 实际代码中,按秩求并和按大小求并选择其中一种即可private int[] size; // 实际代码中,按秩求并和按大小求并选择其中一种即可public UnionFind(int[] parent) {this.parent = parent;this.rank = new int[parent.length];this.size = new int[parent.length];for (int i = 0; i < parent.length; i++) {this.rank[i] = 1;this.size[i] = 1;}}// 直接求并public void unionDirect(int x, int y) {if(find(x) != find(y)){parent[find(y)] = find(x);}}// 按大小求并public void unionBySize(int x, int y){int xRoot = find(x), yRoot = find(y);if(xRoot != yRoot) { // 根节点不同才求并if(size[yRoot] <= size[xRoot]){parent[yRoot] = xRoot;size[xRoot] += size[yRoot];} else {parent[xRoot] = yRoot;size[yRoot] += size[xRoot];}}}// 按秩求并public void union(int x, int y){int xRoot = find(x), yRoot = find(y);if( xRoot != yRoot){if(rank[yRoot] <= rank[xRoot]) parent[yRoot] = xRoot;else parent[xRoot] = yRoot;if(rank[xRoot] == rank[yRoot]) rank[xRoot]++;}}// 直接查找public int findDirect(int x) {if(parent[x] == x) return x;return findDirect(parent[x]);}// 带路径压缩的查找public int find(int x) {if(parent[x] == x) return x;return parent[x] = find(parent[x]);}}
class UnionFind2{private int[] parent;public UnionFind2(int[] parent) {this.parent = parent;}// 直接求并public void unionDirect(int x, int y) {if(find(x) != find(y)){parent[find(y)] = find(x);}}// 按大小求并public void unionBySize(int x, int y){int xRoot = find(x), yRoot = find(y);if(xRoot != yRoot) { // 根节点不同才求并if(parent[xRoot] <= parent[yRoot]){ // 负数比较,较小者树较大parent[xRoot] += parent[yRoot];parent[yRoot] = xRoot;} else {parent[yRoot] += parent[xRoot];parent[xRoot] = yRoot;}}}// 按秩求并public void union(int x, int y){int xRoot = find(x), yRoot = find(y);if(xRoot != yRoot) {if(parent[xRoot] <= parent[yRoot]){parent[yRoot] = xRoot;} else {parent[xRoot] = yRoot;}// 当两棵树秩相等根节点不同时,新树的高度增高1(负数表示,减1)if(parent[xRoot] == parent[yRoot]){parent[xRoot]--;}}}// 直接查找public int findDirect(int x) {if(parent[x] < 0) return x; // 只有代表元满足 parent[x] < 0return findDirect(parent[x]);}// 带路径压缩的查找public int find(int x) {if(parent[x] < 0) return x;return parent[x] = find(parent[x];}}
数学
(a b) % k = (a % k) (b % k) % k
换句话说,对乘法的结果求模,等价于先对每个因子都求模,然后对因子相乘的结果再求模。
求质数只要求到sqrt(n)
int mypow(int a, int k){if (k == 0)return 1;if (k % 2 == 1){return (a * mypow(a, k - 1));}else{int sub = mypow(a, k / 2);return sub * sub;}}
概率::
原则一、计算概率一定要有一个参照系,称作「样本空间」,即随机事件可能出现的所有结果。事件 A 发生的概率 = A 包含的样本点 / 样本空间的样本总数。
原则二、计算概率一定要明白,概率是一个连续的整体,不可以把连续的概率分割开,也就是所谓的条件概率。
https://labuladong.gitee.io/algo/4/30/121/
十进制—>n进制 除基取余法
tips
注意数据的范围
使用合理的数据类型, 防止溢出和下表越界
4.11 微众银行实习岗血的教训
C++ longlong
java biginteger 或者直接python
a + b == c 时可以写成 a==c-b,防止溢出
需要多次扩容的话,可以在第一次多分配点空间来降低之后的时间消耗
位运算 >> 1 : 除二
<<1 :乘二
l +(r -L)/ 2防溢出
注意边界: 确定每个变量的定义和区间的开闭情况
O(nlogn)想到排序
画图
恰好可能有猫腻
对于这种数组问题,关键点在于元素和索引是成对儿出现的,常用的方法是排序、异或、映射。
PS:向上取整是一个常用的算法技巧。大部分编程语言中,如果你想计算 M 除以 N,M / N 会向下取整,你想向上取整的话,可以改成 (M+(N-1)) / N。
不会写的可以找找规律
对于一些比较困难的问题,其解法并不是一蹴而就的,而是步步推进,螺旋上升的
退而求其次是一种很聪明策略。
多次用到的信息可以存起来
有序的
需要最值或者一些动态的时候
一般是二叉堆构建的优先队列
或是二叉搜索树,比如set这种
