2021年
2021年8月
- 1个字节8位二进制下,最小值-128的二进制是
1000 0000
这是人为规定的,-1的二进制是1111 1111
。想一下为什么是这两个形式,因为-127=-128+1是1000 0001
,-126是-128+21000 0010
,那么-1就是-128+127=1111 1111
。 - 最短路径,通常用BFS来解决。单源最短路径使用djikstra算法。任意两点间最短路径使用floyd算法。
- 向上取整除法: N − 1 M + 1 \frac{N-1}{M}+1 MN−1+1,非常巧妙
- 红黑树(自平衡二叉查找树)能实现增删查
logN
的时间复杂度- 其中查是使用AVL树的二分查找
- 如果遇到一个场景,既需要logN时间复杂度(二分)的查找,又需要LogN时间复杂的插入,则应该考虑底层是红黑树的数据结构。C++中set, map。Java中TreeSet,TreeMap。
- C++
lower_bound()
和set.lower_bound()
和map.lower_bound()
查找有序序列中第一个大于等于目标值的元素的第一个元素的迭代器。- 如果
返回值==set.begin()
说明所有元素均 ≥ \geq ≥目标值 - 如果
返回值==set.end()
说明所有元素均 < < <目标值
- 如果
set.lower_bound()
效率比lower_bound(set.begin(),set.end()
效率高!- 是否能使用递归取决于,子问题的最优解的叠加是否等于父问题的最优解。在316去除重复字母这一题中,我尝试使用递归解法,最后发现结果错误。原因就是,这一题中每一步达到最优解,最终结果不一定是最优解。
- 算法中一个很重要的思想就是:逆向思维。有时候正向思考得不出答案,就需要猜测能否逆向来完成。
- 「使……最大值尽可能小」是二分搜索题目常见的问法
- 将一个数变为其相反数,只需要
i=~i+1
,因为计算机底层是用补码来存储数据。(原码和补码相互转换是除了符号位以外,按位取反,然后+1,那么计算机中的一个数的相反数,即所有位按位取反,然后+1即可)
2021-7-27
- 今天刷题发现一个问题,同一题下,vector预先申请了长度,能够顺利AC,但是如果不预先申请,而是不停的insert,则会严重超时。
- 对于循环数组问题,通常可以用数组长度翻倍来解决,即复制原来的数组,然后接到原来的数组后边。
2021-7-19
- 全手工实现LRU(Least Recently Used)算法
#include <iostream>
#include <map>
using namespace std;
struct Node {
int key, val;
Node *pre, *next;
Node() : pre(NULL), next(NULL) {}
Node(int k, int v) : key(k), val(v) {}
};
class DoubleList {
public:
//构造
DoubleList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head->next = tail;
tail->pre = head;
size = 0;
}
//在链表尾部添加元素,时间复杂度O(1)
void addLast(Node *x) {
tail->pre->next = x;
x->pre = tail->pre;
x->next = tail;
tail->pre = x;
size++;
}
//删除链表中的x节点(该节点一定存在)节点指针(地址)由哈希表给出
void remove(Node *x) {
x->pre->next = x->next;
x->next->pre = x->pre;
//free(x); 此处不能free,因为挪动节点到开头需要用到此函数
size--;
}
//删除链表中的第一个节点,并返回该节点,时间O(1)
Node *removeFirst() {
if (head->next == tail)
return NULL;
Node *first = head->next;
head->next = first->next;
head->next->pre = head;
size--;
return first;
}
int getSize() {
return size;
}
//按照LRU顺序,展示所有键值,用于测试
void show(){
Node*p=tail;
while(p->pre!=head){
cout<<p->pre->key<<":"<<p->pre->val<<" ";
p=p->pre;
}
}
private:
//头尾虚节点
Node *head, *tail;
//链表元素个数
int size;
};
class LRUCache {
public:
//构造:提供最大容量
LRUCache(int capacity) {
this->capacity = capacity;
}
//获取某键的缓存值
int get(int key) {
//没有该键的缓存值
if (Map.find(key) == Map.end()) {
return -1;
}
//将该键提升为最近使用的键
makeRencent(key);
return Map[key]->val;
}
//存放某键的缓存值
void put(int key, int val) {
//缓存键已存在,则更新键值
if (Map.find(key) != Map.end()) {
deleteKey(key);
addRencent(key, val);
return;
}
//如果已满,则淘汰最近最少使用的缓存
if (cache.getSize() == capacity) {
removeLeastRecently();
}
//添加为最近使用的元素
addRencent(key, val);
}
//测试用,按LRU顺序展示缓存中所有内容
void show(){
cache.show();
cout<<endl;
}
//将某个key提升为最近使用的
void makeRencent(int key) {
Node *x = Map[key];
cache.remove(x);
cache.addLast(x);
}
//添加最近使用的元素
void addRencent(int key, int val) {
Node *x = new Node(key, val);
Map[key] = x;
cache.addLast(x);
}
//删除某个key
void deleteKey(int key) {
Node *x = Map[key];
cache.remove(x);
Map.erase(key);
free(x);
}
/*删除最久未使用元素*/
void removeLeastRecently() {
Node *target = cache.removeFirst();
int key = target->val; //这就是为什么双向链表中节点要存储key,是为了删除的时候,反向查找在map中的位置并删除
Map.erase(key);
free(target);
}
private:
//哈希表
map<int, Node *> Map;
DoubleList cache;
int capacity;
};
int main() {
LRUCache lru(4);
while(1){
string op;
cin>>op;
if(op=="get"){
int key;
cin>>key;
int res=lru.get(key);
if(res==-1){
cout<<"Not exist!"<<endl;
continue;
}
cout<<key<<":"<<res<<endl;
lru.show();
}else if(op=="put"){
int key,val;
cin>>key>>val;
lru.put(key,val);
lru.show();
}else{
cout<<"wrong command!"<<endl;
continue;
}
}
}
2021-7-18
- 并查集Union-find算法,主要解决的是图论中的
动态连通性
- 并查集数组实现(路径压缩)
#include <iostream>
using namespace std;
#define MAXN 10
int fa[MAXN+1];
void init(){
for(int i=1;i<=MAXN;i++)
fa[i]=i;
}
//返回当前节点的根节点,并进行路径压缩
int find(int x){
if(fa[x]==x)
return x;
fa[x]=find(fa[x]);
return fa[x];
}
//合并两个节点,注意在同一棵树上的问题
void merge(int a,int b){
fa[find(a)]=find(b);
}
按秩合并
优化版:
路径压缩版本只能在find的时候实现,因此合并过程中,还是会出现很复杂的树。为了避免树过深,可以选择按秩合并(即高度低的树,合并到高度高的树上,来避免树高度的增长,当然两棵树高度相同的情况下,合并后必然有一棵树高度会+1)
#include <iostream>
using namespace std;
const int MAXN = 7;
int fa[MAXN+1];
int rank_[MAXN + 1];
void init(){
for(int i=1;i<=MAXN;i++){
fa[i]=i;
rank_[i]=1;
}
}
int find(int x){
if(x==fa[x]){
rank_[x]=1;
return x;
}
fa[x]=find(fa[x]);
rank_[x]=2;
return fa[x];
}
void merge(int a,int b){
int ap=find(a);
int bp=find(b);
//秩(高度)低的,合并到秩高的树上
if(rank_[ap] > rank_[bp]){
fa[bp]=ap;
}else{
fa[ap]=bp;
}
//如果两棵树高度相同,那么合并后,合并的目标树高度要加一(排除同一棵树的情形)
if(rank_[ap] == rank_[bp] && ap != bp){
rank_[bp]++;
}
}
2021-7-17
- 今天做一道leetcode题目1373二叉搜索子树的最大键值和
- 我花了两个小时的时间,研究怎么在一个非BST的子树中,找出最大BST键值和,费了老劲。最后发现题解中,只需要用一个flagMAX记录一下最大值即可啊!!!气。
- 还吸取了一个教训,找BST的时候,空节点直接视为一棵空BST,有时更为简单。
- 关于判断BST合法性这个问题,有三种思路
- 利用BST中序遍历有序这个性质,先对该树进行中序遍历,再判断是否是升序即可。
- 对于树的操作应该着眼于当前节点,根据BST的定义,当前节点的值应该大于所有左子树节点的值(大于左子树最大值),且小于所有右子树节点的值( 小于右子树最小值)。据此可以写出递归函数,不过要在空节点的返回值上要加以设计。
- 本题还可以为为每个节点设置阈值(上限和下限),通过递归传递和更新阈值。(此方法把节点之间缠绕在一起,单独做此题目容易,如果BST和其他问题结合,则不好处理)
class Solution {
public:
bool isValidBST(TreeNode* root) {
return checkBST(root,LONG_LONG_MIN,LONG_LONG_MAX);
}
bool checkBST(TreeNode*root,long long low,long long high){
if(!root)
return true;
if(root->val<=low || root->val>=high)
return false;
return checkBST(root->left,low,root->val)
&& checkBST(root->right,root->val,high);
}
};
class Solution {
public:
//树的递归中,以当前节点为思考对象,最佳
bool isValidBST(TreeNode* root) {
return checkBST(root)[0];
}
//三个返回值:1.是否是BST 2.最小值 3. 最大值
vector<long long>checkBST(TreeNode*root){
if(!root)
return {1,LONG_LONG_MAX,LONG_LONG_MIN};
vector<long long>l_res=checkBST(root->left);
vector<long long>r_res=checkBST(root->right);
if(l_res[0] && r_res[0] && root->val>l_res[2] && root->val<r_res[1]){
long long l_min=l_res[1];
long long r_max=r_res[2];
return {1,l_min!=LONG_LONG_MAX?l_min:root->val,r_max!=LONG_LONG_MIN?r_max:root->val};
}
return {0,0,0};
}
};
2021-7-16
- 昨天会写的二叉树的序列化,今天再去想这个问题,竟然不会写。发现自己还是没有完全领悟二叉树和递归的思维。二叉树中递归的思维,应该着眼于根节点,处理好一个节点的行为,递归下去便完成了对整棵树的操作。
- 考虑二叉树序列化这个问题,对于以root为根节点的二叉树来说,先得直到左子树和右子树的序列化内容,才能返回
左子树序列化+根节点val字符串+右子树序列化
,因此要使用后序遍历 - 需要先直到子树内容,才能知道当前树内容的,通通用后续遍历。
- 考虑二叉树序列化这个问题,对于以root为根节点的二叉树来说,先得直到左子树和右子树的序列化内容,才能返回
//用**后序遍历**的方式,实现二叉树**前序的序列化**
string serializeTree(TreeNode*root){
//终止条件
if(!root)
return "";
//逻辑
string ans=intToStr(root->val);
string lStr=serializeTree(root->left);
string rStr=serializeTree(root->right);
if(lStr!=""){
ans+=','+lStr;
}
if(rStr!=""){
ans+=','+rStr;
}
return ans;
}
string intToStr(int n){
string s;
stringstream ss;
ss<<n;
ss>>s;
ss.clear();
return s;
}
-
今天对补码有了全新的认识
其中1000 0000
是规定的最小值,然后简单+1就是-127,可以一直简单+1到127
补码的好处就是让如1-2的运算,变成硬件最简单操作的相加运算
1-2=0000 0001
+1111 1110
=1111 1111
=-1
-
C++中如何获取int型的最大值和最小值
- 方法一:头文件
climits
中有INT_MAX
,INT_MIN
- 方法二:
- 最小值:1<<31,即
1000 0000...0000
,补码规定的负数的最小值, − 2 31 − 1 -2^{31}-1 −231−1 - 最大值:unsigned int a=0;
~a>>1
即为0111 1111...1111
, 2 31 2^{31} 231
- 最小值:1<<31,即
- 方法一:头文件
2021-7-15
- 二叉搜索树BST
- 判断一棵树是否是BST:需要借助两个变量记录每个节点应该遵循的上限和下限,然后递归即可
- 增加节点:递归找到位置(NULL),然后接在上面即可。
- 删除节点:
第五种情况,操作如下:
第一步
第二步
第三步
第四步
2021-7-14
- 二叉搜索树(BST,Binary Search Tree)是递归定义的
- BST为空,或者
- 对于二叉搜索树的每一个节点root,左子树所有节点得值小于root值,右子树所有节点的值大于root值
- 对于BST的每一个节点,它的左子树和右子树都是BST
- 没有键值相等的结点
- 二叉搜索树的重要性质:中序遍历是有序的
- 二叉树的各种递归,无非是:1.前序遍历 2.中序遍历 3.后序遍历
- 二叉树的序列化,使用后序遍历(注意要以#处理叶节点的子节点)
- 对于二叉树的递归构造,应该想方设法构造根节点,只要根节点构造出来了,其他所有节点根据递归就全部都构造出来了。
2021-7-13
- 快慢指针、双指针、多指针,在链式结构中是十分重要的思想。
- ”链表是一种兼具迭代性质和递归性质的数据结构”,很有道理。
- 今天做
反转链表这道题目
,迭代和递归方法都非常精妙,其中博主labuladong对递归的思考方式令我印象深刻,受益匪浅。
Leetcode206 反转链表- “不要跳进递归(你的脑袋能压几个栈呀?)”,博主如是说
下面这一步十分精妙
- 我认为,实现递归的代码,不应该跳进递归的轮回中,应该假装递归方法已经实现,直接调用,然后补其具体递归逻辑。
- 此外,递归最为重要的就是,递归函数的含义。
- “不要跳进递归(你的脑袋能压几个栈呀?)”,博主如是说
2021/7/11
- 对于递归函数来说,最重要的就是递归函数的含义。
- 写递归算法的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要跳入递归的细节。
2021/6/3
- 做dp的题目,首先要搞清楚状态转移方程,然后应该举个小例子草稿实现一下dp,以使对边界条件有个清晰的认识。
- 关于字符串的dp,经常把dp[i]定义成以i位置字符结尾的串的个数
2021/6/2
- 遇到问题类似最少多步、最少多少次这类问题,一定要想到用bfs或者最短路径算法
- 蓝桥杯国赛比赛之前默念:搜索、位运算、全排列、数学问题
- 当设计递归函数遇到困难,搞不清楚时,举个例子模拟一下过程就好了。
- 当算法中出现大量重复问题的时候,可以采用记录的方法,可以大大降低时间复杂度
2021/6/1
- 感悟:今天重做蓝桥杯题目对局匹配的时候,更加深刻地明白了两个道理
- 懂和会写,是完全两件事,明白怎么写不等于能写出通过oj的代码
- 编程不能闭门造车,要吸收别人优秀的代码和思想。
- 遇到统计方案数的题目,一定要注意重复问题
- dfs有爆栈的风险,可以用bfs来替代
- 对于图的相关问题如发现环,或者排序,首先要想到拓扑排序
2021/5/31
- 无向图的生成树(使用尽可能少的边将所有顶点连在一起)
- 包含所有顶点
- 任意两顶点之间有边构成唯一路径
- 百度百科定义:一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边
- 书上定义:极小连通子图为某一顶点子集所确定的连通子图中包含边最少的连通子图(n个顶点,无向连通图最少n-1条边,有向连通图最少n条边——成环)。图全部顶点所确定的极小连通子图即为连通图的生成树。即包含所有顶点的极小联通子图就是生成树。
- 在代码中实现,用并查集保持无环,且包含了所有节点即可。
- 蓝桥杯国赛凑平方数这题太经典了,将解题过程加以记录
- 涉及到dfs的优化、状态压缩、位运算
- 涉及到dfs的优化、状态压缩、位运算
#include <iostream>
#include <vector>
typedef long long ll;
using namespace std;
vector<int> state; //11001中1表示消耗了数字
int len; //合法平方数个数
void init() {
for (ll i = 0; i * i <= 9876543210l; i++) {
ll tmp = i * i;
int st = 0;
if (tmp == 0) {
st = 1;
state.push_back(st);
continue;
}
bool flag = true;
while (tmp) {
int t = tmp % 10;
tmp /= 10;
if (st & (1 << t)) { //存在1冲突
flag = false;
break;
} else
st |= (1 << t);
}
if (flag) {
state.push_back(st);
}
}
len = state.size();
}
//判断当前剩余数字能否实现num
bool check(int st, int numSt) {
return !(st & numSt);
}
ll ans = 0;
//在下标k~len-1范围内,找第一个合法的平方数
void f(int k, int st) { //st 0表示可用
if(st==(1<<10)-1){ //0~9都是1
ans++;
return;
}
for (int i = k; i < len; i++) {
if (check(st, state[i])) {
f(i + 1, st | state[i]);
}
}
}
int main() {
//将范围内的平方数都找出来,将其消耗的数字的信息,存入state中
init();
//递归
f(0, 0);
cout << ans;
return 0;
}
- 自己用递归实现全排列的好处在于,可以进行剪枝
- 今天发现了一个一直以来没发现的大问题:BFS找最短路径的时候,不用每个节点单独记录路径
- 所有节点共享一个set就行了,因为如果某个节点下一步走已经在集体公用set中的节点,则你这个节点必然已经落后了。
2021/5/30
-
今天写蓝桥杯国赛真题遇到2020十一届C++B组H题答疑这一题
- 作为B组倒数第二题,JavaC组最后一题,竟然经过一定的数学推导,代码比填空题第一题还简单。
-
看到一个对前缀和、树状数组、线段树的总结,写的不错
-
最长递增子序列
这道经典的算法题,用动态规划解法的时间复杂度是 O ( n 2 ) O(n^2) O(n2)- 使用一个队列,每轮不断优化其元素,使每个元素达到尽可能小(给后续递增留出最大的空间),其时间复杂度可达
O
(
n
log
n
)
O(n\log{n})
O(nlogn) (需要用到二分来查找插入位置,
lower_bound()
可以实现) - 这样只能算出来LIS的长度,最后的结果不一定是LIS
- 因为往前替换元素,违背了子序列顺序的定义,这种方法只能求出来LIS的长度,不能用于求解LIS本身。
- 使用一个队列,每轮不断优化其元素,使每个元素达到尽可能小(给后续递增留出最大的空间),其时间复杂度可达
O
(
n
log
n
)
O(n\log{n})
O(nlogn) (需要用到二分来查找插入位置,
2021/5/29
- 求数组中第K大的数字,可以使用基于快速排序的选择算法,降低时间复杂度
- 快速排序中,每一轮确定一个元素的最终位置,即它是第几大元素(从大到小)
- 如果该元素的下标是我所求的第K大,则直接返回该元素
- 否则根据情况向左、向右分治
2021/5/26
- 将无穷大这个值设置为
0x3f3f3f3f
的好处和1e9+7
一样,量级在10^9
,一般数字都小于它,可以用于无穷大。并且两个0x3f3f3f3f
相加,不会超出4字节32位int的表示范围。此外,用memset
进行初始化的时候,可以直接写成memset(arr,0x3f,sizeof arr)
,非常方便 - 唯一分解定理,算数基本定理:一个正数,如果是合数,则可以唯一分解为多个质数的乘积
- 注意在做方格题目的时候,如果是边7*7的矩形,则边上的点是8*8的
2021/5/25
- 再次反省,做题一定要考虑两件事
- 数据范围(long long,时间复杂度)
- 特殊情况(边界条件)
- dijkstra算法和floyd算法的区别是
- floyd代码简单,时间复杂度O(N^3)
- dijkstra代码复杂,时间复杂度低
- kruskal算法和prim算法的区别
- prim基于节点
- 适用于点少,边多的稠密图
- kruskal算法基于边
- 适用于点多,边少的稀疏图
- prim基于节点
2021/5/24
- 今天写蓝桥杯国赛
搭积木
这一题- 发现递归中,如果把一切问题都丢给下一层去解决,会造成栈溢出(虽然下一层第一步就返回了记忆化的结果)
- 将记忆化存储中已经有的答案直接返回, 减少不必要的递归,似乎能够很好地提高效率
- 但是平时写递归的时候,还是建议本层只处理本层的问题。
- 存在大量相同子问题的问题
- 动态规划
- 记忆化搜索
- 记忆化递归(也可以理解为一种搜索)
不是所有都适合动态规划,比如蓝桥杯国赛搭积木这一题,记忆化递归是最优方案
- 蓝桥杯遇到不会的题就想
- 贪心
- 模拟
- 搜索
- dp
- 状态压缩
- 二分、分治
- 图论
- 数论:gcd,最小公倍数,余数,差分,前缀和
- 全排列
- 什么时候用BFS,什么时候用迪杰斯特拉\弗洛伊德?
- BFS:当任意两节点之间的距离是某些特定值,如蓝桥杯国赛调手表那题,只能是1和k。
- 距离不固定:
- 迪杰斯特拉:复杂度低,O(N)
- 佛洛依德:复杂度高,三重循环O(N^3)
- 算法思想:如果a到c+c到b,比a到b近,则用c这条路替换
- 遇到题目,先想常规方法(如最优解,通常用BFS),再想特殊方法(如前缀和,dp,状态压缩,数学)
- 常规方法中,可以先想暴力法,再进行复杂度的优化。
2021/5/22
- 取余同余、前缀和、差分,这些都是重要的解题手段
- 动态规划其降低复杂度的原因在于,减少重复计算
- 所以如果写出来的dp转移方程,
dp[i]
需要与前i-1
项分别发生作用,那么就失去了动态规划的作用
- 所以如果写出来的dp转移方程,
2021/5/21
- 再一次发现逆向思维的重要性
- 在平方数那个问题中,如果直接遍历1、2、3…,然后判断i是否是平方数,则复杂度是O(n2),但是如果直接遍历平方数,即12,22,32…,则时间复杂度为O(n)
- 在贴瓷砖那题中,因为每个瓷砖是两个方块,如果枚举墙面每块的所有可能性再进行check,会发现非常难以进行检查。但是如果贴瓷砖的时候就按照两个两个进行贴,那么问题就非常简单了!
- 人的思维是很容易有漏洞的!不要想当然,多写写画画!
- 在做蓝桥杯国赛贴瓷砖那题,我理所当然认为,既然是从左上往右下贴,那么当前位置的下方和右方一定没有被访问过,但其实是有可能被访问过的,画个图就发现了。
- 对于二维数组,要想直接用指针+相对位置来访问某个元素
*(*arr+100)
这样才行- 因为
arr
自身是二维指针
2021/5/20
- 不要把邻接表的含义搞错了!它仅能表示一个节点和哪些节点相连,其链式结构不能等同于图中的先后顺序!
- 邻接表中要删除一条边,不能直接将一条链中某个节点的next置为NULL,会导致该链上后续的节点也被遗弃!
2021/5/19
- 在图中查找回路,采用拓扑排序的思想
- 对于有向图,每轮消掉所有入度为0的节点,最后剩下的就是回路
- 对于无向图,每轮消掉度数为1的节点,最后剩下来的就是回路
- BFS中需要使用set记录的path,来防止自己走回头路
- 今天在实现邻接表的时候发现一个大坑:
- 将指针a的指向赋给指针b,则两者指向的内容相同(a和b的存储单元中存储的地址值相同)
- 这时候如果修改b的指向(修改b存储单元地址值)是不影响a的指向的!
- 比赛遇到不会的,就考虑最朴素的暴力
- 图的表示有两种方法
- 邻接表(在面对大规模数据的时候,节省存储空间)
- 邻接矩阵
2021/5/18
- 状态压缩可用的地方很多,不止是状态压缩dp
- 前缀和前缀和,遇事不决前缀和
- 逆向思维非常重要
- 状态压缩中的,二进制状态,别忘了是用int表示的,前面还有很多位!!
- 你以为的
11011
状态,其实是0000...0011011
共32位
- 你以为的
- 使用二进制串表示状态时,如果不方便进行状态调整,可以将0和1的意思反过来,也许就方便了。
2021/5/17
-
数据规模往往决定着该题用什么算法
-
注意,树状数组的下标,一定要从1开始,否则lowbit操作和树状数组的逻辑不匹配
-
一个非负数,取二进制最低位的1和后边的0,即lowbit操作
- lowbit(a)=a &(~a+1)=a & (-a)
- 因为计算机中数字是以补码形式存放,负数的补码就是原码(除符号位)按位取反然后加一
- 那么一个正数取相反数的操作就是
- 符号位取反
- (除符号位)按位取反,然后加一
- 这两步组合在一起就是,所有位按位取反,然后加一,故
~a +1
=-a
-
Docker
- docker的特点
- 互相隔离
- 快速装载和卸载
- 规格标准化
- docker和虚拟机的根本区别:容器技术只隔离应用程序的运行时环境但容器之间可以共享同一个操作系统,这里的运行时环境指的是程序运行依赖的各种库以及配置。
- docker的特点
-
蓝桥杯遇到题目不会写,想一下是否是考察以下知识点
- 差分、前缀和
- dp、状态压缩dp
- 并查集
- 贪心
- 搜索
- 二分
- 数论
-
减少时间复杂度的方法
- 减少循环
- 使用哈希
- 二分
- 提前保存结果并利用
2021/5/15
- 差分和前缀和是对立统一的存在。
- 学到了,自建博客,解决图片问题,用外链图床就行了!
2021/5/14
- 在二维空间中,向量外积的一个几何意义就是:|a×b|在数值上等于由向量a和向量b构成的平行四边形的面积
- S Δ = ∣ 1 2 ⋅ a ⃗ × b ⃗ ∣ = ∣ ( x 1 ⋅ y 2 − x 2 ⋅ y 1 ) ∣ S_\Delta=|\frac 1 2\cdot \vec{a}\times\vec{b}|=|(x_1\cdot{y_2}-x_2\cdot{y_1})| SΔ=∣21⋅a×b∣=∣(x1⋅y2−x2⋅y1)∣
- 已知三角形顶点坐标求面积
- 使用海伦公式
p=(a+b+c)/2; S=sqrt(p(p-a)(p-b)(p-c) )
- 使用海伦公式
- 对于一个(正方形)二维数组的下标来说
- 区分主对角线两边元素的方法是:
- 若看y-x大于0,则在主对角线以上
- 区分副对角线两边元素的方法是:
- 若x+y<行数-1,则在副对角线以上
- 区分主对角线两边元素的方法是:
- 并查集判环的方法是
- 加入当前边之前,两个顶点如果已经在一个集合中了,那么加入该边后一定形成环
2021/5/13
- 拓扑排序
- 使用BFS广度优先遍历实现比较好
- 将所有入度为0的(从未入过队的)节点加入队列
- 不断取出队列中的节点,放入结果集末尾,并将其直接子节点的入度减一
- 重复步骤1和步骤2,直到队列为空
- 其核心思想是,每次将入度为0的节点加入结果集尾部,并且将该节点的直接子节点的入度减一
- 注意:如果存在环,则无法将环中节点加入结果集
- 使用BFS广度优先遍历实现比较好
- 一个常见的BUG是,把变量含义弄错,尤其是
i
和arr[i]
- 解决二分图问题,使用染色法
- 广度优先搜索进行染色
- 使用
lower_bound
和upper_bound
找到一个结果时,要想得到其下标,只需要用迭代器减去arr.begin()
即可 lower_bound
和upper_bound
是基于二分的,也就意味着序列需要是从小到大排序好的- 排序是重要算法思想
- 数学技巧:如果c位于a和b之间,那么c到a的距离+c到b的距离,是一个固定值,为(b-a);
- 在一些累积性的问题中,前缀和是非常有效的解题思路。
- 写代码前
- 理清思路,并且优化思路
- 关注数据大小,关注边界条件
- 然后开始编码
- 蓄水池抽样算法(又叫水库采样算法)
- 适用于总数未知的随机抽样
- 在对一个链表(长度未知)进行随机采样的时候,遍历一次链表,在遍历到第 m 个节点时,有 1/m 的概率选择这个节点作为结果,并且覆盖掉之前的结果。
class Solution {
public:
/** @param head The linked list's head.
Note that the head is guaranteed to be not null, so it contains at least one node. */
ListNode*myList;
Solution(ListNode* head) {
myList=head;
srand((unsigned int)time(NULL));
}
/** Returns a random node's value. */
int getRandom() {
int ans=myList->val; //初始选中第一个节点
ListNode*node=myList->next;
int i=2;
while(node){
if(rand()%i==0){ //1/i的概率选中当前的节点
ans=node->val; //覆盖掉之前的选择
}
i++;
node=node->next;
}
return ans;
}
};
- 写算法时,首先关注数据大小和边界条件
- 战术上的勤奋并不能弥补战略上的懒惰
- 方向错了,怎么努力也没有用
- Fisher-Yates洗牌算法
- 原理:通过随机交换位置来实现随机打乱,有正向和反向两种写法
- 反向洗牌比较好记,反向,并且只和序号小于等于自己的交换
//反向洗牌
for (int i = n - 1; i >= 0; --i) {
swap(shuffled[i], shuffled[rand() % (i + 1)]);
}
//正向洗牌:
for (int i = 0; i < n; ++i) {
int pos = rand() % (n - i);
swap(shuffled[i], shuffled[i+pos]);
}
- C++的rand()随机数是伪随机数,每次打印rand()是打印特定种子下的小M多项式序列的每一项
- 如果随机种子不变,那么每次执行程序,打印出来的一组随机数都与之前一组相同
- 并且该序列的周期是65535
- 头文件
<cstdlib>
2021/5/12
- 找出int范围内3^x的最大值,x为整数
int findLargest3Power(){
int x=3;
while(1){
int next=3*x;
//发生溢出时返回x
if(next/3!=x){
return x;
}
x=next;
}
}
- 判断一个数能否被开方
- 使用逆向思维!
//判断a能是否能被开方
bool canBeExtract(int a){
int res=sqrt(a);
return a==b*b;
}
-
C++判断一个浮点数小数部分是否为0
- 使用C++的浮点数取模函数
fmod(double , double)
fmod(a,1)==0
- 使用C++的浮点数取模函数
-
今日教训:写题目一定要草稿纸勾勾画画,不要想当然。
-
今天遇到一道很有意思的题目
- 开始采用的是返回min(2的数量,5的数量),后来题解中说因子5的数量远远小于因子2的数量,所以答案就是5的数量。
- 开始采用的是返回min(2的数量,5的数量),后来题解中说因子5的数量远远小于因子2的数量,所以答案就是5的数量。
-
写算法三大注意事项
- 关注数据大小和数据量
- 估算算法时空复杂度
- 注意边界条件
-
算数基本定理:任何一个大于1的自然数 N,如果N不为质数,那么N可以唯一分解成有限个质数的乘积
- 也可以表述为:任何一个合数都可以分解为质数的乘积。(1既不是素数(质数),也不是合数)
- 这里的唯一指的是,分解结果不看顺序的话,如3,4,5和5,4,3是一样的分解。
-
质数的定义:在大于1的自然数中,除了1和它本身以外,不再有其他因数的自然数。(只能被1和自身整除的大于1的自然数)
-
自然数肯定包括0,否则应该叫正数才对。
-
拓展欧几里得算法
- 用于计算xa+yb=gcd(a,b); 根据定理此式必有解
- 使用的时候,预先定义变量x和y,最后结果在存在x和y中
int extGcd(int a, int b, int &x, int &y) {
int gcd = a;
if (!b) {
x = 1, y = 0;
} else {
gcd = extGcd(b, a % b, y, x);
y -= (a / b) * x;
}
return gcd;
}
2021/5/11
-
写一道算法题,开始写代码之前要做的事
- 理清思路,搭好框架
- 估计所用方法的时间空间复杂度,对于题目中数据量
- 关注题中的数据大小
- 关注边界条件
-
今天刷到洛谷上一道题,觉得很有启发性。这一题要记录两行的状态,才比较方便解决问题
-
逻辑运算中,今天遇到一个大坑
- 一道状态压缩题目,炮兵攻击距离是3,也就是三排之间不能在一列
- 这时候判断三排的所有士兵不能在一列
- 正确写法
return !(st1 & st2) && !(st1 & st3) && !(st2 & st3)
- 错误写法
return !(st1 & st2 & st3);
- 正确写法
-
使用递归输出一个十进制数的二进制形式
void printBinary(int num) {
if(!num)
return;
printBinary(num>>1);
cout<<(num&1);
}
- C++
scanf
输入数字之后,如果要输入别的类型(如字符,字符串)一定要吸收换行符!(getchar()) - 做动态规划的题目,应该先理清思路,最好写出状态转移方程
- 如果输入的是一个二进制数如1101,如何存储为十进制?
- int t=0;每次读取一个字符c,然后
t<<=1;t+=c
;即可
- int t=0;每次读取一个字符c,然后
- 动态规划的问题要倒过来思考,正过来写代码
- 思想上是用后边的状态倒推前边的,但是写代码的时候要现有前边的状态才能求出后边的状态
2021/5/9
- 编程比赛中,该写
cstdio
头文件,还是要写,只有iostream
的话,printf
和scanf
有可能会报错 &
的优先级比+
低!!- 使用与或非的时候,一定要加括号!
- 取INF时用
1e9+7
是不错的选择- 两个数相加不爆int
- 两个数相乘不爆long long
- 对于TSP问题中需要回到出发点的问题,不要在状压dp中加以考虑,全部状态计算完之后,再单独处理,加上最后访问点到原点的距离即可。
- 计算最短路径的两种经典算法(图片截图自B站up主WAY_zhong的讲解视频)
-
迪杰斯特拉Dijkstra算法基于贪心
- 贪心:计算出每个节点到原点的最短距离,直到计算到des节点
-
佛洛依德Floyd算法基于动态规划
- 核心思想:如果f(a,c)+f(c,d)<f(a,d) 则更新f(a,d)=f(a,c)+f(c,d),如果需要路径的话将d的前驱记录为c即可
- 右边的二维表记录(有向图)某个节点最短路径上前一个节点。如果只需要计算最短路径长度,则不需要右边的二维表。
- 代码十分简洁,三重循环,选出src、des、mid节点即可。
-
有向图floyd
void floyd() {
for (int i = 0; i < N; i++) //src
for (int j = 0; j < N; j++) //des
for (int k = 0; k < N; k++) { //mid
if (i == j || i == k || j == k)
continue;
if (dist[i][j] > dist[i][k] + dist[k][j])
dist[i][j] = dist[i][k] + dist[k][j];
}
}
无向图floyd
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
for(int k=0;k<n;k++) //基于无向图的优化
if(dist[i][k]+dist[k][j]<dist[i][j]){
dist[i][j]=dist[i][k]+dist[k][j];
dist[j][i]=dist[i][j];
}
2021/5/8
- 处理股票买卖的动态规划问题时,有时建立
hold持有
和unhold不持有
两个dp数组会比buy
和sell
更好,因为对一支股票每天只有两种状态,持有或者不持有。 - 遇到复杂问题的编程,要把过程想清楚(最好画图),用注释搭好框架再写,如下边这题
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int arr[2001];
fill(arr,arr+2001,0);
arr[nums[0]+1000]++;
arr[-nums[0]+1000]++;
// 记录已经能表示的target
for(int i=1;i<nums.size();i++){
int N=nums[i];
int temp[2001];
fill(temp,temp+2001,0);
for(int j=0;j<2001;j++){
if(arr[j]>0){
temp[j-N]+=arr[j];
temp[j+N]+=arr[j];
}
}
memcpy(arr,temp,2001*4);
}
return arr[target+1000];
}
};
- 对一些在顺序上左右为难的问题,不妨先进行排序!
- 动态规划注意事项
- 注意人工增加项的初始化(尤其是多维dp的第一行第一列)
- 注意dp含义的设定与题目求解问题的关系(直接还是间接)
- 处理环形数组的办法是:丢弃第一个或者丢弃最后一个,最后取两种方案较优的那种
- 多个条件的动态规划,就像切几根香肠,把每一根(每个条件)都切成一片一片,然后来解决问题。
2021/5/7
- 今天花了一个下午做一道hard动态规划题目,最大的感触是,一定要定义好dp的含义,从含义出发,找
dp[i][j]
和之前的dp的关系。其次就是,多状态问题(如股票买卖),最好将多种状态分开dp。同时,总结结果时也从必须根据dp的定义来返回。
buy[i][j]
表示,0~i
天进行一共进行j
次买入,能够获取的最大利润
sell[i][j]
表示,0~i
天进行一共j
次卖出,能够获取的最大利润
这里的一共j次的一共非常重要,意味着第i天可以进行买\卖,也可以不买\不卖
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int d=prices.size();
//buy[i][j]表示,0~i天进行一共进行j次买入,能够获取的最大利润
vector<vector<int> >buy(d+1,vector<int>(k+1,INT_MIN));
//sell[i][j]表示,0~i天进行一共j次卖出,能够获取的最大利润
vector<vector<int> >sell(d+1,vector<int>(k+1,0));
for(int i=1;i<d+1;i++){
int price=prices[i-1];
for(int j=1;j<=k;j++){
//两种情况,在第i天买入股票,或者不买
buy[i][j]=max(buy[i-1][j],sell[i-1][j-1]-price);
//两种情况,在第i天卖出股票,或者不卖
sell[i][j]=max(sell[i-1][j],buy[i][j]+price);
}
}
return sell[d][k];
}
};
dp一般有两种描述
- 一种是跨度,从0到i项一共xxx
- 一种是标度,第i项xxx
2021/5/6
- 当有多个变化的条件时,从一个条件的变化出发,比较容易有思路
- 当没有低复杂度的算法时,从高复杂度的算法出发,然后进行优化
- 记录一道很棒的动态规划的题目,我对
*
的处理值得回味
class Solution {
public:
bool isMatch(string s, string p) {
int len1 = p.length(), len2 = s.length();
vector<vector<bool> > dp(len1 + 1, vector<bool>(len2 + 1, false));
dp[0][0] = true;
for (int i = 1; i < len1 + 1; i++)
for (int j = 0; j < len2 + 1; j++) {
//第一列特殊处理
if (j == 0) {
dp[i][j] = (p[i - 1] == '*' && dp[i - 2][j]);
continue;
}
//p串末尾是*
if (p[i - 1] == '*') {
//情形1:*表示0个前边的元素
//清醒2:*表示n(n>0)个前边的元素
dp[i][j] = dp[i - 2][j] || ((p[i - 2] == s[j - 1] || p[i - 2] == '.') && dp[i][j - 1]);
continue;
}
//p串末尾不是*
dp[i][j] = (p[i - 1] == s[j - 1] || p[i - 1] == '.') && dp[i - 1][j - 1];
}
return dp[len1][len2];
}
};
2021/5/5
- leetcode这一题太经典了,记录下来
class Solution {
public:
//统计字符串中0和1的个数
void count(string &str,int &count0,int &count1){
for(int i=0;i<str.length();i++){
if(str[i]=='0')
count0++;
else
count1++;
}
}
int findMaxForm(vector<string>& strs, int m, int n) {
int len=strs.size();
//dp[i][j]表示,有i个0和j个1的情况下最多容纳几个子集
vector<vector<int> >dp(m+1,vector<int>(n+1,0));
//每个字符串是压缩空间前的横坐标
for(int k=0;k<strs.size();k++){
string &str=strs[k];
int count0=0,count1=0;
count(str,count0,count1);
//01背包,后向遍历,防止重复使用本轮的字符串
for(int i=m;i>=count0;i--){ //i<count0,无法容纳该字符串,则dp[i][j]维持上一轮的数据
for(int j=n;j>=count1;j--){
//分两种情况,一种选中这个字符串,一种不选中这个字符串
dp[i][j]=max(dp[i][j],1+dp[i-count0][j-count1]);//第一个dp[i][j]是上一轮的结果
}
}
}
return dp[m][n];
}
};
- 0-1背包多为费用问题和一维费用问题本质是相同的,即通过假设占用部分(多维)容量,由于剩下的(多维)容量以前已经处理过了,所以可以进行动态规划。
- 0-1背包问题要点如下
- 不压缩空间:横坐标是要装的物品,纵坐标是背包容量
- 选中当前物品,则假设占用部分容量,看剩下部分容量之前如何处置的
- 不选中当前物品
- 压缩空间:后向遍历,以避免本轮物品的重复取用
- 不压缩空间:横坐标是要装的物品,纵坐标是背包容量
- 对于多费用的01背包问题,其解题过程和普通的01背包问题相同(强烈建议使用空间压缩,注意后向遍历),其动态规划形式似下图:
普通01背包
普通01背包压缩空间
多维费用01背包
多维费用01背包压缩空间后
- 自认为我分析得很精妙的一题
- 我的思考,既然要求最长递增子序列,那么我就维护一个最长递增子序列,nums中从左到右每个元素都是用来优化这个维护着的序列的。
- 如何优化?答:尽可能地使维护序列中的每个元素最小。这样就为序列的增长提供了最充足的空间!
使用lower_bound解决问题!(找到第一个大于等于key的数,返回其迭代器)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int>res;
for(int i=0;i<nums.size();i++){
int key=nums[i];
if(res.empty()){
res.push_back(key);
continue;
}
if(key>res[res.size()-1])
res.push_back(key);
else{
*lower_bound(res.begin(),res.end(),key)=key;
}
}
return res.size();
}
};
- 使用dp数组时,有时dp下标和所求事物的下标有差异(主要是0作首位还是1作首位),这时候重新定一个变量index记录所求事物下标,会让思路更清晰,如下:
class Solution {
public:
int numDecodings(string s) {
//dp[i]表示:0~i位置一共有多少种编码方法
int len=s.length();
vector<int>dp(len+1,0); //s的下标对应dp的1、2、3...
//首个是0就无解
if(s[0]=='0')
return 0;
//排除长度为1的情况
if(len==1)
return 1;
//开始dp
dp[0]=1,dp[1]=1;
stringstream ss;
int t;
//i是dp的下标
for(int i=2;i<len+1;i++){
//s的下标
int index=i-1;
//独立算
if(s[index]!='0')
dp[i]+=dp[i-1];
//和前边合并算
if(s[index-1]!='0'){
ss<<s.substr(index-1,2);
ss>>t;
ss.clear();
if(t>=10 && t<=26)
dp[i]+=dp[i-2];
}
if(dp[i]==0)
break;
}
return dp[len];
}
};
2021/5/4
-
写代码时,养成习惯:先理清思路,接着搭好框架(注释),最后再编码
-
在使用动态规划时,可以进一步思考是否可以空间优化。
-
重新写爬楼梯
leetcode70
这一题,想都没想直接用递归,结果测试用例给了个44,超出时间限制。- 我的思维存在的漏洞:在有一个思路之后,并没有考虑时间复杂度和问题规模。
- 这一题如果使用递归的话,其复杂度是
2^n
,1s最多能容纳的n的规模约为27。 - 1s不同复杂度能处理的规模
- n : 108
- n^2 : 104
- n^3 : 500以下
- 2^n : 27以下
- logn : 2^(108) ⭐
- 所以这一题应该使用动态规划求解,避免重复计算子问题的答案
2021/4/17
-
多个选项中选多个的题目,计算合法方案数。这种题目可以利用全排列来写
- 六个里边选三个
int arr[]={0,0,0,1,1,1,};
- 六个里边选三个
-
消除尾一:
x=x&(x+1)
- 经典的二进制题目!
- 经典的二进制题目!
-
10个数的全排列,时间复杂度为
10!
-
最大公共子串
和最大公共子序列
在二维动态规划时,是不一样的- 前者的状态转移是
if(s1[i-1]==s2[i-1]) dp[i][j]=dp[i-1][j-1]+1;
- 后者需要分三种情况进行讨论。
- 前者的状态转移是
-
背包问题的精髓在于:在递归或动态规划时,假设背包中已经装了xx的情况下,看剩下的容量怎么装。
2021/4/16
- 若a、b、c三者和的余数为K,a的余数为i,b的余数为j
- 则c的余数为
(K-i+K-j)%K
- 即
(i以外需要凑的余数+j以外需要凑的余数)对K取余
- 则c的余数为
- 多重循环时间复杂度优化
- 减少层数
- 减少范围
- 能否二分
- 空间换时间
- 倍数问题大多跟余数有关!
- 多个数的和是某个数的倍数,意味着,各个数的余数的和等于目标数;
- 动手编程之前,把问题想清楚,思路理清楚,每一步要做什么搞清楚,先注释,再编程。
- 对于多维数组的问题,可以转换成一维进行处理
- 如三维转一维
int convert(int i,int j,int k){
int a=i-1,b=j-1,c=k-1;
return a*row*column+b*column+c;
}
2021/4/14
- python的数不会溢出,因此比赛填空题有些可以用python来解决
- 取模与取余的区别在于
- 取余是向0方向舍入(fix函数)
- 取模是向负无穷方向舍入(floor函数)
- 如-7 mod 4= 1 (到-8不足1), -7 %4 = -3 (到-4少了-3)
2021/4/13
- 快速幂的精髓在于,将底数不断膨胀,幂不断缩小
递归写法
long long fastPow(long long base,int pow){
if(pow==0)
return 1;
if(pow==1)
return base;
if(pow%2==1){
return base*fastPow(base*base,pow/2);
}
return fastPow(base*base,pow/2);
}
非递归位运算写法
long long fastPow(long long base,int pow){
long long res=1;
while(pow){
//处理奇数
if(pow&1){
res*=base;
}
base*=base;
pow>>=1;
}
return res;
}
2021/4/12
- 凡是在一组值中,选出一部分,在一定规则下组合成一个目标值,就可以看作是背包问题
- 背包问题的精髓在于将目标值分解成
0~n
来逐个解决,每次选取数组中一个值时,对背包剩下的容量进行调整(一般是减少,也可能增加),装入背包后,剩余容量用已有的dp[j]来解决是背包问题能动态规划的核心原因。
- 背包问题的精髓在于将目标值分解成
2021/4/11
- 一些动态规划的问题,考虑将dp[i]是为,以i为结尾的xxx;
- 使用dp数组,首先要搞明白dp[i]代表什么含义,可以是题中的要求求解的量,也可以是中间结果。并思考,dp[i]和dp[i-1](或者dp[i]和dp[0],dp[1]…dp[i-1])有什么关系。
- 对于动态规划(或者其他问题)中的环形问题,可以采取分类的方法
- 选第一个且不选最后一个,求出最优解
- 选最后一个且不选第一个,求出最优解
- 有一部分动态规划问题的精髓在于,对每一种情况进行试探,从而找出最优解。(不是返回dp[len-1]就行了,而是max(dp[i]) )
- 比如leetcode121题股票交易,这是一道easy题,但是我想了好久没做出来,为什么?因为对动态规划题目定势思维了,认为dp[i]存储的就是最终结果,其实它也可以是中间结果,或者是需要进一步处理的信息。
- 对这种题目,我们可以尝试在每一天卖出股票,从而找到最优的方案。
2021/4/10
-
动态问题一定要搞清楚对谁进行0~n的划分(如果coins问题是对金额进行划分),也有可能是多个变量进行划分(二维、三维dp,但也可压缩。
-
01背包问题为什么要倒序遍历(压缩的)dp数组
- 比如在leetcode474中,这样一个临时的二维dp
- 假如从左上角往右下角遍历,每次利用i,j位置之前的数据修改dp[i][i],就会造成上一轮的临时数据已经被修改,后边的数据无法利用上一轮的临时数据了。而从右下角向左上角遍历,就不会出现这个问题。
- 比如在leetcode474中,这样一个临时的二维dp
-
01背包问题,动态规划的思路是:物品一个一个尝试,容量一点一点尝试,每个物品分类讨论的标准是:选与不选
2021/4/9
- 对于动态规划中的初始条件问题(尤其是二维dp),可以额外写一个函数来处理,如下这个LCS最长公共子序列问题,使用
safeReturn函数来处理第一行第一列的特殊情况
:
class Solution
{
public:
int safeReturn(vector<vector<int> >&dp,int x,int y)
{
if(x<0 || y<0)
return 0;
return dp[x][y];
}
int longestCommonSubsequence(string text1, string text2)
{
int len1=text1.size(),len2=text2.size();
//dp[i][j]表示,text1前i个字符和text2前j个字符的LCS数
vector<vector<int> >dp(len1,vector<int>(len2,0));
for(int i=0; i<len1; i++)
for(int j=0; j<len2; j++)
{
if(text1[i]==text2[j])
{
dp[i][j]+=1;
dp[i][j]+=safeReturn(dp,i-1,j-1);
}
else
{
dp[i][j]=max(safeReturn(dp,i-1,j),safeReturn(dp,i,j-1));
}
}
return dp[len1-1][len2-1];
}
};
- 对于动态规划问题,
dp[i]
记录的可以是中间结果,也可以是最终结果。如果记录的是中间结果,最后不能忘了遍历并返回最大值或最小值
。
2021/4/7
- 排列与组合
- 对于排列题,使用
next_permutation
或者自定义DFS+回溯
的递归函数实现; - 对于组合题,使用递归对每个位置采取
选中
和不选中
两种方法。
- 对于排列题,使用
- 使用DFS+回溯编写不重复的全排列,在for循环的交换中,可以使用set集合来避免重复
- 注意!使用之前,序列必须已经排好序!
- 注意!内部交换时,仅仅让交换过来的数不等于自身,是远远不够的,因为可能多次交换过来同一个数,需要使用set才能完全去重。
void permutation(vector<int>&nums,int k){
//在下一轮进行判断
if(k==nums.size()){
ans.push_back(nums);
return;
}
set<int>record;
for(int i=k;i<nums.size();i++){
int target=nums[i];
if(record.find(target)!=record.end()){
continue;
}
record.insert(target);
swap(nums[i],nums[k]);
permutation(nums,k+1);
swap(nums[i],nums[k]);
}
2021/3/29
- 在递归中,有两种判断终止条件的写法
- 第一种,当前轮进行判断
- 第二种,下一轮进行判断
- 递归中,只有满足当前层条件才能进入下一层,如果能够进入最后一层的下一层(不存在的层),则说明满足了所有层的条件。故在每一层种只需要设置否定条件,能够进入下一层即代表满足条件。
- 排列数和组合数都可以用
递归+回溯
实现,其递归函数逻辑如下
//实现排列数,n个数全排列
void f(int k){
if(k==n+1){
生成一种排列;
return;
}
for(int i=k;i<=n;i++){
swap(nums[i], nums[k]);
f(k+1);
swap(nums[i], nums[k]);
}
}
//实现组合数,从n个数种挑选m个
void f(int k){
if(已选中的个数==m){
生成一种组合;
return;
}
if(k==n+1){
return;
}
选中第k个数;
f(k+1);
不选第k个数;
f(k+2);
}
2021/3/27
- 蓝桥杯只使用ANSI C/ANSI C++ 标准,不能使用C++11语法。具体则是在CodeBlocks中勾选 ISO1998标准。
2021/3/26
- 在处理一些带有棘手的边界问题和特殊情况的问题时,建议把它边界问题和特殊情况单独拿出来进行处理,不要总想着用一套通用的代码解决所有问题。
- 比如在写K-th Element问题 ,需要用到一个辅助函数,用来确定基准元素的最终位置的。其中对i和j下标溢出的问题单独做了处理,那么就不需要那么费神来写一套能够完美覆盖溢出情况的代码了。
// 题目描述:在一个未排序的数组中,找到第 k 大的数字
class Solution {
public:
int findKthLargest(vector<int> &nums, int k) {
int l = 0, r = nums.size() - 1, target = r - k + 1;
while (1) {
int p = fix(nums, l, r);
if (p == target)
break;
if (target < p) {
r = p - 1;
} else
l = p + 1;
}
return nums[target];
}
//找到l位置元素的最终index,并返回
int fix(vector<int> &nums, int l, int r) {
int i=l,j=r;
while(1){
while(i<=r && nums[i]<=nums[l])
i++;
if(i==r+1){
swap(nums[l],nums[r]);
return r;
}
while(j>=l && nums[j]>=nums[l])
j--;
if(j==l-1){
return l;
}
if(i>j)
break;
swap(nums[i],nums[j]);
}
swap(nums[l],nums[j]);
return j;
}
};
int main() {
vector<int> vec = {10, 5, 6, 0, 0, 1, 2};
cout << Solution().findKthLargest(vec,3)<<endl;
return 0;
}
2021/3/24
- K-th Element问题,使用改造的快速排序来解决,是最好的。
- 因为快速排序的本质是每次确定第i个位置的元素,那么只要确定第k个位置的元素就可以结束递归,实际并不关心是否
- 快速排序或类快速排序的算法,需要先打乱顺序,否则最坏情况下时间复杂度会达到O(n2)
2021/3/22
- 选出数组中第K大的数
- 先从大到小排序,再选出第K个
- 时间复杂度更低的方法:使用选择排序或者快速排序的思想(每轮确定一个元素的最终位置 )
2021/3/20
- 牛顿迭代法求平方根(保留整数,如sqrt(8)=2)
class Solution {
public:
int mySqrt(int x) {
long long a=x;
while(a*a>x){
a=(a+x/a)/2;
}
return a;
}
};
- 做题目时,一定要考虑边界情况和溢出的情况。
- 寻找最小或最合适的区间的问题,用滑动窗口解决最优。
- ASCII码一共128个字符,因此统计一个字符串中每个字符出现的次数,可以使用
int chars[128]
。
2021/3/18
- C++11用nullptr代替了NULL表示空指针,因为NULL在某些情况下会产生歧义
int*a=NULL;
判断a是否为NULL/nullptr
可以用if(!a)
表示a是NULL
- 写了篇Floyd判圈法的博文。
2021/3/17
- 贪心的思想是,每次都选择局部最优的方案以达到全局最优。
- 对于有多种处理方式的情况,可以进行分类讨论再汇总
- 32位有符号int型的范围是-231~231-1
- 因为有1位要表示符号
- 0要占用正数一个位置,所以正整数范围是1~231-1
- 负数没有0的占位,所以负整数的范围是-231~-1
- leetcode452题用最少量的箭引爆气球
- 最大的体会是:一定要多画图,考虑不同的情况
- leetcode435题无重叠区间
- 使用贪心算法解决,核心是看区间末尾位置。贪心中,每次选择区间末尾位置最早的,就意味着为其他区间留下了最多的空间。
- lambda表达式可以作为sort的参数(前提是C++11)
- 在leetcode135题分糖果中,要求两侧孩子胃口要是大于中间的孩子,必须分的糖果比中间孩子多,问最少要多少糖果。遇到两侧情况这种题目,可以从左到右贪心一次,再从右到左贪心一次。
- 注意从左往右遍历的时候,要不断利用已经更新的结果。
class Solution {
public:
int candy(vector<int>& ratings) {
int size=ratings.size();
if(size<2)
return size;
vector<int>num(size,1);
for(int i=1;i<size;i++){
if(ratings[i]>ratings[i-1])
num[i]=num[i-1]+1;
}
for(int i=size-1;i>0;i--){
if(ratings[i]<ratings[i-1])
num[i-1]=max(num[i-1],num[i]+1);
}
return accumulate(num.begin(),num.end(),0);
}
};
2021/1/4
- 快慢指针的精髓在于:快指针最终会追上慢指针,以证明链上有环存在
- 为什么判断质数的时候只需要验证[2,floor(n1/2)]
注意右边是闭区间
中数是否能被n整除,因为大于n1/2的数不可能被n整除 - 找n以内的质数个数,更优的方法:筛质数
- 从2开始,每次将2的所有倍数做
False
标记,然后是3,将3的所有倍数做False
标记…最终从2开始所有没有标记到的就是质数的合集
- python同时变量两个序列,可以用用zip
- for a,b in zip(s1,s2):
- python中的空set初始化要用
a=set()
- 非空则可以写成a=set([1,2,1])
- 或者a={1,2,1} 其存储的是{1,2}
- Hash可以降低时间复杂度,通常由set集合和dict来实现
- 判断一个序列是否有重复元素,可以通过判断转变成set集合后,序列的长度是否发生变化
2021/1/11
- 关于数字的处理,尤其是关于2的幂次,可采用位运算
- 如判断一个数是否是2的幂,即看其二进制形式是否只有一个1
- 遇到一个复杂问题的时候,首先想能不能分解成多个子问题
- 快慢指针可以快速找到链表的中间节点
- 找最近公共祖先的方法就是,从头遍历,找到第一个分裂的点,即是最近公共祖先
- 在链表中可以通过指向某个节点(非末尾节点,且链表长度>1)的指针,直接将这个节点删除
- 方法:将下一个节点的所有内容复制到这个节点即可。
2021/1/12
- 单线程完成尽可能多的任务(每个任务工作时长不同),每次选择最早结束的任务即可!
- 对于等差数列求和,用求和公式速度更快
2021/1/13
- 子串不同于子序列,子串是连续的而子序列可以不连续
- 对于题目给定问题规模不那么大的时候,完全可以使用复杂度较高的暴力方法进行解题,如-字符串长度小于1000-,用O(n3)的算法也能够在规定时间内完成。
- 最长回文子串(-子串是连续的-)的解法
- 暴力枚举
- 中心枚举法
- 动态规划
- 字符串转数字,用
Integer.parseInt()
2021/1/22
- java自定义元组数据类型(不可变)
class Tuple<A,B>{
public final A a;
public final B b;
public Tuple(A a,B b){
this.a=a;
this.b=b;
}
}
2021/1/25
-
动态规划的思想
- 如果要解决规模为n的问题的最优解,每一步记录规模为1、2、3…n-1、n的问题的最优解,每一步都利用前面一项或者前面所有项的信息。
- 如
三角形最小路径和
一题中,记录三角形中对应每个节点的最小路径和,最终可获得最后一行的最小路径和。
-
java中截取字符串是消耗性能的,因此一般采取传递索引的方式来表示子串。
-
java中的
==
-
对于基本数据类型
byte,short,int,long,float,double,char,boolean
,是比较其值 -
对于引用数据类型,比较其内存地址
-
用“==”运算符,该运算符表示指向字符串的引用是否相同,比如: String a=“abc”;String b=“abc”,
那么a==b将返回true。这是因为在java中字符串的值是不可改变的,相同的字符串在内存中只会存
一份,所以a和b指向的是同一个对象;再比如:String a=new String(“abc”); String b=new String(“abc”);
那么a==b将返回false,因为a和b指向不同的对象。
-
String a="abc",b="abc";
String c=new String("abc"),d=new String("abc");
System.out.println(a==b);
System.out.println(c==d);
//输出结果
//true
//false
-
java中的StringBuilder的
reverse()
方法,是原地逆置(在自身上逆置) -
Java官方推荐使用Deque替代Stack使用,Deque堆栈操作方法:push()、pop()、peek()。
Deque<String>stack=new ArrayDeque<>();
-
java中将数组转化为ArrayList的方法,注意不能使用基本数据类型
Integer[]arr={1,2,3}; List<Integer> lst=new ArrayList<>(Arrays.asList(arr)); //或者 List<String>l=new ArrayList<>(Arrays.asList("aaaa","aaa"));
2021/1/26
- java中可以直接将List转化为HashSet
List<String> wordDict
Set<String>wordDictHashSet=new HashSet<>(wordDict)
- 动态规划不仅仅局限于dp数组的形式,由一个变量或者多个变量,记录变化亦可。
- java数组切片用
int[]arr_1=Arrays.copyOfRange(arr,0,arr.length-1);
2021/1/27
- java中
int
类型数据有时可能会出现中间结果溢出,用long
即可
2021/1/28
- 如果动态规划中一维或者二维数组的开始点不好处理,可以让其加长一位,遍历时从1开始
int[][]dp=new int[m+1][n+1];
2021/1/29
- DP不仅对求解最优问题有效,对于各种排列组合的个数、概率或者期望之类的计算同样很有用。
2021/2/17
- 涉及2的多少次方的问题,可以使用移位运算
<<
和>>
,能够大大加快运算 - 对一个数组进行逆置(或者其他半数的操作),for循环如此
for(int i=0;i<len/2;i++)
就对了 - 快速排序的代码
- 循环条件
p<=r
的原因是,r下标的元素并未处理,需要p来处理完成后才覆盖所有元素 - 快速排序核心思想:每次都确定一个(或多个相同)元素的最终位置
- 注意一定不能写成: int pivot=left; 后续用arr[p] < arr[pivot];! 因为pivot只记录序号的话,该序号上的数值会发生变化,导致基准不停变化!!
- 循环条件
public void quickSort(int[]arr,int left,int right){
if(left>=right)
return;
int l=left,r=right;
int p=l;
int pivot=arr[left];
while(p<=r){
if(arr[p]>pivot){
int temp=arr[p];
arr[p]=arr[r];
arr[r]=temp;
r--;
}
else if(arr[p]<pivot){
int temp=arr[p];
arr[p]=arr[l];
arr[l]=temp;
l++;
p++; //此处注意需要p++
}
else
p++;
}
System.out.println("l="+l+",r="+r);
quickSort(arr,left,l-1);
quickSort(arr,r+1,right);
}
写法二
#include <stdio.h>
void swap(int a[], int i, int j)
{
int t = a[i];
a[i] = a[j];
a[j] = t;
}
int partition(int a[], int p, int r)
{
int i = p;
int j = r + 1;
int x = a[p];
while(1){
while(i<r && a[++i]<x); //如果i位置元素小于基准,就一直往右,目的是使i位置>=基准
while(a[--j]>x); //如果j的位置大于x,就一直往左,目的是使j的位置<=基准
if(i>=j) break; //如果i>=j,跳出循环
swap(a,i,j); //否则,交换i,j位置上的元素
}
swap(a,p,j);
return j;
}
void quicksort(int a[], int p, int r)
{
if(p<r){
int q = partition(a,p,r); //确定一个元素的最终位置
quicksort(a,p,q-1);
quicksort(a,q+1,r);
}
}
其中标志l左侧的都小于基准,标指r右侧的都大于基准,故递归时使用范围[left,l-1]和[r+1,right]
- Java中注意,用
Scanner
先输入一个数字,再输入一行,会报错- 因为输入数字后,会产生一个换行符
- 输入一个sc.nextLine(),把换行符吃掉就行了
- Java中,判断对象内容是否相等,只能用equals,否则编程比赛中会扣分
- 两个数互质指的是两个数只有公因数1
- 快速求最大公因数:辗转相除
- java使用
BigInteger
来求最大公因数GreatestCommonFactor(GCF)
BigInteger two=BigInteger.valueOf(2);
BigInteger a=two.pow(20).subtract(BigInteger.ONE);
BigInteger b=two.pow(19);
BigInteger gcd=a.gcd(b);
- 快速幂Java实现
public long quickPower(long base,long power){
long result=1;
while(power>0){
if(power%2==0){
power/=2;
base*=base;
}
else{
power/=2;
result*=base; //都在这一步乘到结果中了
base*=base;
}
}
return result;
}
- 快速幂java实现,位运算优化版
public static long normalPower(long base,long power){
long result=1;
while(power>0){
if((power & 1)==1 ) result*=base; //这里与运算相当于判断二进制形式下最后一位是否是1,即是否是奇数
power>>=1;
base*=base;
}
return result;
}
- java int类型最大值约为21*108
&与运算
:二进制形式下,只有相同位置上都为1,才为1,否则为0
2021/2/19
- C++中
union
类型中,赋值时,各成员只能有一个被赋值,即各成分互斥。 union
类型只能赋值一个成员,具有互斥性
union{
int cls;
char position[10];
}category;
2021/2/21
- 对于多个时间区间的任务,要想完成尽量多的任务(不能中断某个任务),每次选取(可选任务中)结束时间最早的即可。即贪心算法。
- 如果
(left+right)/2
产生溢出,那么可以写成left+(right-left)/2
- 二分查找尽量用非递归写法
- 一般的OJ,其能承受的时间复杂度在每秒**108**以内,所以O(n2)复杂度的问题,当n=105,时间复杂度就变得不可接受
- 二路归并排序
- 流程:
- 先将两个一组进行排序
- 然后四个一组排序,八个一组排序…直到所有都被排序
- 其中涉及到两个顺序序列合并的问题,用双指针即可
- 使用递归实现,倒过来思考
- 将原始数组分为左边和右边
- 对左右分别进行归并排序
- 再将左右两个顺序序列合并为一个顺序序列
- 时间复杂度O(nlogn)
- logn是因为每次step*2
- n是因为双指针合并两个有序序列,宏观上需要进行O(n)级别次操作
- 流程:
- 编程时将一些变量视为更容易理解的事物,比如归并排序中将step理解为区间长度远远比步长来的更容易理解。
void mergeSort(int arr[],int left,int right){
//递归终止条件
if(left==right)
return;
//把左边和右边排序好
int mid=(left+right)/2;
mergeSort(arr,left,mid);
mergeSort(arr,mid+1,right);
//然后将两个顺序序列合并
int temp[right-left+1];
int l=left,r=mid+1;
int i=0;
while(l<=mid && r<=right){
if(arr[l]<arr[r]){
temp[i++]=arr[l++];
}
else
temp[i++]=arr[r++];
}
while(l<=mid)
temp[i++]=arr[l++];
while(r<=right)
temp[i++]=arr[r++];
for(int j=left,i=0;j<=right;j++)
arr[j]=temp[i++];
}
2021/2/24
- 处理日期问题时,不仅要注意闰年闰月的问题,在进行函数编写时要注意,传入的月份是1-12,编程中的通常是0-11,传入时要转换为机器友好形式,输出时也要再转回人类阅读友好形式。
2021/3/1
- 判断一个乘式末尾有多少个0,答案就是min(因子2的个数,因子5的个数)。
- 即使某个乘子末尾本身就用0也不用考虑,因为10本身就是2和5的积。
- 丑数(因数只有2,3,5)类似的题,都可以用三指针来做
- 开始时三个指针都指向dp数组0位置,每次满足条件+1;
- C++的set和map具有排序功能
- 注意:在接收字符或字符串后,再接收一行字符,在此之前需要吸收到回车
2021/3/2
- 对于联通块问题,常采用计数的方法解决
- 此外,规模较小的可以采用深度优先遍历,规模大的采用宽度优先遍历,否则会栈溢出
- int类型大小约为21*108这么大,4字节,32位。
- 一般来说OJ,1秒能承受的运算次数在107~108
- 对于多层循环,如何降低时间复杂度
- 减少层数
- 减少范围
- 二分法
- 空间换时间,提前存储好部分信息以便访问
- 关于多个数组合成某个数的倍数的问题,考虑用根据余数分组来做。
- 对于多括号嵌套问题,先处理最小的括号,再递归
2021/3/3
- 遇到题目说从某种初始状态到达终态最少需要多少步,这种题型,一般都是BFS宽度优先搜索来解决。
- 定义静态二维数组时,第二维的长度须确定。
- 所谓回溯就是
- 执行某种行为
- 递归
- 撤销第一步的行为
- 深刻体会这种DFS的不同剪枝方法
/*a个字母A,b个字母B,c个字母C,能组成多少种不同的长度位固定n的字符串*/
/*
* 这种DFS的题目,其需要求出的是DFS能走要正确终点的路径数*/
#include <cstdio>
int DFS(int a,int b,int c,int n){ //这是我的思路,有某个字母数量还有才能递归下去
//给出两种终止条件
if(n==0)
return 1;
int res=0;
if(a>0)
res+=DFS(a-1,b,c,n-1);
if(b>0)
res+=DFS(a,b-1,c,n-1);
if(c>0)
res+=DFS(a,b,c-1,n-1);
return res;
}
int cal(int a, int b, int c, int n) { //这是答案的思路:先递归下来,如果字母数量由0变成-1,那就剪枝return 0
//终止条件
if (a < 0 || b < 0 || c < 0)
return 0;
if (n == 0)
return 1;
//递归
return cal(a - 1, b, c, n - 1) + cal(a, b - 1, c, n - 1) + cal(a, b, c - 1, n - 1);
//这里的返回值代表的是,选择A作为当前位置字母的话后续共有多少种方案+选择B...+选择C...
//cal(a-1...)说明该方案选择A作为当前位置的字母,其返回值代表的是当前位置选择了A这种方案一共有多少种不同方法走到底,若下一步中判断出了a-1是<0的,那么返回值是0,相当于对这个方案做了剪枝
}
int main() {
printf("%d\n",DFS(1,2,3,3));
printf("%d",cal(1,2,3,3));
return 0;
}
2021/3/4
- 互质指的是两个数的最大公约数是1
- 求最大公因数,就用辗转相除法
//递归简约版
int gcd(int a,int b){
if(b==0)return a;
return gcd(b,a%b);
}
- 互质的两个数x和y,其中ax+by不能凑出的数(a、b>=0,x、y>0),最大为a*b-(a+b);
- 意思是对大于a*b-(a+b)的数来说,任意个数的x和y一定能凑出来(a,b有非负解)
- 计算一个长方形(长宽分别为l、w)能装下多少个边长为e的正方形
- (l/e)*(w/e)
- 注意这里的除是整除
- 做竞赛题的时候务必注意数据的范围,如果数据比较大,总时间复杂度超过108就很容易执行时长超过1秒
- 但是对于蓝桥杯这种每个测试用例单独算分的比赛来说,在没有更好的思路情况下,可以用简单的暴力法拿到不少分。
- 对于顺序枚举的情形来说,可以使用二分法来降低时间复杂度
- 常常在枚举优化的题目(数据超大)中体现
//二分法模板
int binarySearch(int arr[],int n,int key){
int l=0,r=n-1;
while(l<=r){ //注意这里必须是等号
int mid=(l+r)/2;
if(arr[mid]==key)
return mid;
if(arr[mid]<key){
l=mid+1;
}else{
r=mid-1;
}
}
}
- 注意全排列
next_permutation(指针1,指针2)
注意指针2是开区间
2021/3/5
- 竞赛时,在时间不够的情况下,优先考虑能多拿分的简单方法解题(如暴力法)
- 绝对不能使用打点法来记录虚线上的实线,会产生歧义
- 因为1,2,3,4四个点都被标记了
- 那么是表示1~4长度为3的实线?
- 还是表示12,34长度均为1的两条实线?
- 解决方案:用数组记录,0号元素表示01的位置有线,1号元素表示12的位置有线
- 对于蓝桥杯的递归填空题,搞懂参数的含义和递归的方向,基本就可以写出来(尝试写一下,然后用测试用例跑一下试试)
- 参加比赛的时候,做题要有取舍,同时对是否暴力求解加以思索
- 如17年的魔方状态,写几百行代码就为了求一个数,而且无法验证是否正确,不如直接跳过
- 容斥原理
∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣A∩C∣−∣B∩C∣+∣A∩B∩C∣
- C++中
int arr[3]={0};
或int arr[3]={-1};
不能使用这种方法为数组初始化赋值- 也不能
bool arr[3]={true};
- 这样只会给第一个元素赋值
- 也不能
2021/3/6
- 快速排序时间复杂度是O(nlogn)的原因是:
- 每次确定一个元素的最终位置,然后划分两个子区间(递归划分左右子区间需要logn复杂度)
- 每次确定一个元素的最终位置总共需要O(n)的时间复杂度
&
是按位与- 如10110&1,其实质是10110&00001
- 在处理二进制数末尾情况的时候用
+1
,-1
,&
- 如消除末尾的1,
x=0011011
,那么x&(x+1)
=0011000
- 如消除末尾的1,
- 10!=3,628,800, 11!=39,916,800 ,超过10的全排列基本都会超时
- 美国人的习惯是左闭右开,所以函数参数中,基本都是左闭右开
- 在处理一些数学问题的时候注意,程序中的除法是整除
- 故采用
a/b==c&&a%b==0;
避免数学上和程序上的不一致
- 故采用
- DFS递归时,递归主体中要对当前层所有情况进行处理
- 剪枝可以大幅提升递归的效率
- 全排列代码理解
- 其中函数参数k,代表的是固定前k个位置的数(或者理解为固定第k个位置的数)
//递归实现全排列
#include <cstdio>
#include <algorithm>
using namespace std;
int arr[]={1,1,3};
void show(){
for(auto a:arr)
printf("%d ",a);
putchar('\n');
}
//k代表的是固定第i个位置的数
void full_permunation(int k){
if(k==3){
show();
return;
}
for(int i=k;i<3;i++){
//这个条件加上了,能实现去重的全排列
if(i!=k && arr[i]==arr[k])
continue;
swap(arr[i],arr[k]);
full_permunation(k+1);
swap(arr[i],arr[k]);
}
}
int main(){
full_permunation(0);
return 0;
}
- 使用递归自定义实现全排列,相对于使用
next_permutation()
的优点在于可以自定义剪枝(剪枝时务忘回溯),优化效率 - OJ一般对O(108)时间复杂度,执行时间为1s
- 如
13!=6,227,020,800
,执行13个数的全排列大概需要1分钟左右 - 填空题无需过度在意执行时间,此外还可以使用剪枝、二分等方法来提升执行效率
- 如
- 回溯法中,如果进行了剪枝,要及时回溯
2021/3/7
- 判断一个数是否能开方,开方后乘方看是否与原来的数相等
sqrt(a)*sqrt(a)==a
- 如何优化多层for循环的效率
- 减少for循环层数
- 减少每层的枚举范围
- 二分法(对于顺序序列)
- 缓存+查询
- 对于某种状态都有后续两种状态的情形,可以理解为一颗二叉树
- 问:一个字符串s最少补几个字符能把它变成对称字符串
- 答案=s长度-lcs(s,s逆置)的长度
- lcs最长公共子序列
- 仔细体会其中的奥妙!
2021/3/8
- LCS最长公共子串的dp求解方法,注意核心在于
max(dp[i-1][j-1],dp[i][j-1],dp[i-1][j])
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int main(){
char s1[100];
char s2[100];
int dp[101][101];
gets(s1);
gets(s2);
int len1=strlen(s1);
int len2=strlen(s2);
//初始化
fill(dp[0],dp[0]+101*101,0);
for(int i=1;i<len1+1;i++)
for(int j=1;j<len2+1;j++){
//计算三个值,取最大值
int t=dp[i-1][j-1];
if(s1[i]==s2[j])
t+=1;
dp[i][j]=max(max(t,dp[i][j-1]),dp[i-1][j-1]);
}
printf("%d",dp[len1][len2]);
return 0;
}
- 内存优化版本,用一维数组替换矩阵
//LCS最长公共子串的dp 一维数组写法
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int main(){
char s1[100];
char s2[100];
int dp[101];
int temp[101];
gets(s1);
gets(s2);
int len1=strlen(s1);
int len2=strlen(s2);
//初始化dp数组
fill(dp,dp+101,0);
fill(temp,temp+101,0);
for(int i=0;i<len1;i++)
for(int j=0;j<len2;j++){
int t=dp[j];
if(s1[i]==s2[j]) {
t+=1;
}
temp[j+1]=max(max(dp[j+1],t),temp[j]);
for(int k=0;k<len2;k++)
dp[k+1]=temp[k+1];
}
printf("%d",dp[len2]);
return 0;
}
- Clion无法调试,使用202.2.5的版本就可以
- 最大公因数用辗转相除法(递归版本简单,上面有代码),最小公倍数用a*b/gcd(a,b),即a和b的积除以a和b的最大公因数
2021/3/10
- 做了一道选牌的题目,点数为1~13的牌各4张,问不考虑顺序的情况下取13张,一共有多少种组合
- 这道题的核心在于理解什么叫不考虑顺序:即顺序不重要,每种牌取出来的张数才是重要的
- 所以这道题就转换成了一个DFS问题,即每种牌选0~4张,总数限定13张,一共有多少种可能
回溯法
//深刻理解什么叫不考虑得到牌的先后顺序
//即第一张、第二章拿到A 和 第一张、第三张拿到A是一样的
//也就是说,重要的是A的张数
#include <cstdio>
#include <algorithm>
using namespace std;
int cards[13];
int ans=0;
int chosen=0; //记录已经选的牌的数量
//k表示选下标为k的牌的张数
void func(int k){
//终止条件
if(chosen==13){
ans++;
return;
}
else if(chosen>13){
//剪枝
return;
}
if(k==13){
//选完了还没选够13张牌
return;
}
for(int i=0;i<=4;i++){
chosen+=i;
cards[k]-=i;
func(k+1);
chosen-=i;
}
}
int main(){
fill(cards,cards+13,4);
func(0);
printf("%d",ans);
return 0;
}
非回溯法
#include <cstdio>
#include <algorithm>
using namespace std;
int cards[13];
int ans=0;
void f(int k,int c){
//终止条件
if(k>13 || c>13) //这里k不能=13是因为每次都是下一轮判断前面的选牌总数
return;
if(c==13){
ans++;
return;
}
//递归主体
for(int i=0;i<=4;i++){
f(k+1,c+i);
}
}
int main(){
fill(cards,cards+13,4);
f(0,0);
printf("%d",ans);
return 0;
}
- 有重复元素的圆排列与环排列的计数问题
- 如何判断手链
abc
和cab
是同一个手链,只是旋转了一下 - 第一步:
abc
->abcabc
- 第二步:如果
cab
是abcabc
的子串,那么abc
和cab
是旋转关系- C++中判断子串用
s1.find(s2)!=string::npos
表示s2是s1的子串
- C++中判断子串用
- 同理:如果加上翻转,那么就判断是否是
abcabc
和翻转cab
的子串关系‘
- 如何判断手链
2021/3/11
-
在二维数组的两维之间来回切换
cur=0;
- for循环中每轮
cur=1-cur;
-
对于结果需要对某个大数取余的题目,中间结果也需要去余,防止溢出
#define MOD 1000000007
,注意没有;
-
生成最小生成树的两个经典算法
-
克鲁斯卡尔算法Kruskal
- 将所有边从小到大排序
- 每一轮选择最小的边加入已选边集,若形成环,则丢弃该边
- 直到选中n-1条边
-
Prim普利姆算法
-
本质是动态规划,使用三个数组
- selected[n]:记录顶点是否在已选顶点集中
- minDist[n]:记录某顶点距离已选顶点集的最短距离
- parent[n]:记录父节点
-
每一轮实现一个顶点数为1,2,3…n的最小生成树(动态规划)
-
具体方法
- 选取0号节点加入已选顶点集,然后更新未选顶点距离已选顶点集的距离
-
-
- 在未选顶点中选取距离已选顶点集最近的点,加入已选顶点集
3. 更新未选顶点与已选顶点集的距离
4. 重复2、3步骤直到加入了所有节点
2021/3/12
- 并查集可以用于克鲁斯卡尔算法中,提前判断加入当前边是否会形成环
- 原理是判断边的两个顶点是否在一个集合中,如果已经在一个集合中,再加入该边则会形成环
- 实现方法:寻找两个节点是否有相同的祖宗节点
- 同一个集合中的顶点都拥有共同的祖宗节点
- 解决实际问题的时候,需要将并查集节点和实际问题中的节点映射起来,可以采用数组的形式来映射,创建一个并查集节点数组,根据序号来映射
//实现一个并查集
#include <cstdio>
#include <set>
using namespace std;
struct UFNode {
UFNode *parent;
UFNode() : parent(NULL) {}
};
/**
* 目的是找到p所指向的节点的父节点
* 此外做了优化,将访问路径上的节点全部指向祖宗节点,这样下次查找祖宗节点时更快
* @param p
* @return
*/
UFNode *find(UFNode *p) {
set<UFNode *> path;
while (p->parent != NULL) {
path.insert(p); //这里放入的是p指向的节点的地址,而不是p的地址
p = p->parent;
}
//将路径上所有的节点,父指针都指向祖宗节点
for (set<UFNode *>::iterator it = path.begin(); it != path.end(); it++) {
(*it)->parent = p;
}
return p;
}
void merge(UFNode *p1, UFNode *p2) {
if(find(p1)==find(p2))
return;
//将p2的祖宗节点变成p1的祖宗节点的父节点
find(p1)->parent = find(p2);
}
- 讲一个指针放入vector之中,其实质是将指针所指向的对象的地址放入vector中,而不是指针的地址
- 同样,指针作为函数参数,其实质是临时复制一个指针,函数体内的操作,并不会改变原来指针的指向
int a=1,b=2;
int*p=&a;
vector<int*>vec;
vec.push_back(p);
p=&b;
printf("%d",*vec[0]); //这里的结果是1
2021/3/13
- 对于枚举过程中的重复问题
- 存在左1右3,左3右1的情况,砍掉一个
- 存在左2右2的情况,加上左<右的判断语句
- C++ STL中的set和map内部就是用平衡树实现的,因此出现平衡树相关问题,可以直接使用set和map
- KMP算法:
- 设主串长度为N,子串长度为M,KMP算法可以将暴力求解最坏情况下O(MN)的时间复杂度降低到O(M+N)
- 其核心思想是利用子串自身的前后缀信息,来减少暴力求解时的无用回溯
- 如何利用next数组,见
https://www.bilibili.com/video/BV18k4y1m7Ar?from=search&seid=15693083662541831658
#include <cstdio>
/**
* 如果主串中存在子串,则返回第一次匹配时,第一个字符的下标
* @param s 主串
* @param sub 子串
* @return 如果不存在,则返回-1
*/
int KMP(char s[],int len1,char sub[],int len2){
//创建next数组
int next[len2];
next[0]=0;
int i=0,j=1;
while(j<len2){
if(sub[j]==sub[i]){
next[j]=i+1; //i表示,再j位置之前的串中,其从0~i位置和末尾向匹配
i++; //当然我们从最大的可能性开始查找,直至i为0
j++;
}
else{
if(i==0){
next[j]=0;
j++;
}
else{
i=next[i-1];
}
}
}
i=0,j=0;
while(j<len2 && i<len1){
printf("i=%d,j=%d\n",i,j);
if(s[i]==sub[j]){
if(j==len2-1)
return i-len2+1;
i++;
j++;
}
else{
if(j==0){
i++;
}
else
j=next[j-1];
}
}
return -1;
}
int main(){
char s1[]="abxabcabcaby";
char s2[]="abcaby";
int ans=KMP(s1,12,s2,6);
printf("%d",ans);
return 0;
}
2021/3/14
- 在可能会超时的双(多)重循环枚举中,我们可以考虑只枚举一重循环,剩下的使用二分查找(前提是有序)或者提前存储好hash表再查找的方法,来降低时间复杂度
- 滑动窗口可以用双指针来实现
- 蓝桥杯的规则
C/C++中怎样使用64位整数?
64位整数的类型为:long long
使用cin读的操作为:cin >> x;
使用cout写的操作为:cout << x;
使用scanf读的操作为:scanf("%l64d", &x);
使用printf写的操作为:printf("%l64d", x);
- 只求最后四位数字,那么就
%10000
对一万取余 - 通常取模运算也叫取余运算,它们返回结果都是余数 .rem 和 mod 唯一的区别在于:当 x 和 y 的正负号一样的时候,两个函数结果是等同的;当 x 和 y 的符号不同时,rem 函数结果的符号和 x 的一样,而 mod 和 y 一样。
&1
相当于取二进制最低位数字,一般用于判断奇偶数
2021/3/15
- 1既不是素数,也不是合数
- 线性筛(欧拉筛)
#include <cstdio>
#include <cstring>
const int maxn=1000; //表长
int prime[maxn],pNum=0; //记录素数
bool p[maxn]={false};
int N; //求N以内的素数
void f(int n){
for(int i=2;i<=n;i++){
if(!p[i]) //如果没有被筛掉,说明它是素数
prime[pNum++]=i;
for(int j=0;j<pNum;j++){
if(i*prime[j]>n)
break;
p[i*prime[j]]=true;
if(i%prime[j]==0)
break;
}
}
}
int main(){
memset(p,0,sizeof(p));
scanf("%d",&N);
f(N);
for(int i=0;i<pNum;i++){
printf("%d ",prime[i]);
}
return 0;
}
- C++中对
set
进行排序,自定义比较函数cmp(T p1, T p2)
,如果按照优先级从低到高,则返回值return 优先级低的<优先级高的
,其背后的原理时,sort将调换参数位置分别调用两次cmp
函数,如果一次返回true,一次返回false,则认定其一个对象小另一个大,若两次都返回false,则认定两个对象相等。- 故这里对vector嵌套pair根据first值从小到大进行排序这样写
vector<pair<int, int> > time_and_shopID;
bool cmp(pair<int, int> p1, pair<int, int> p2) {
return p1.first < p2.first;
}
2020年
2020/12/23
- 对于递归函数,其空间复杂度与最大递归深度有关
- 递归的诀窍在于找到形式相同但规模更小的子问题
- python函数中可以定义函数,但是参数不能不能写
self
,调用的时候直接写函数名即可 - 树的深度优先遍历,一条路径末端不一定是叶节点!
- 如:这里的1向右深度优先递归结束了,1不是叶节点
- 如:这里的1向右深度优先递归结束了,1不是叶节点
2020/12/24
- python判断是否是字母或者数字的函数
isalnum
- 动态规划的精髓在于找到
第i项
和前i-1项
(或第i-1项
)的关系- 动态规划是由规模较小的问题不断得到较大规模问题解的过程,关键在于状态转移方程
- 异或
^
在单个位上相同为0,不同为1
的性质使之- 任意数字和0异或,等于自身
- 任意数字和自身异或,等于0
- 异或满足交换律和结合律
- python
reduce
函数的作用是将序列所有元素以一定的规则累积起来- 如
recude(lambda x,y:x+y,[1,2,3,4])
是对序列进行求和
- 如
- 判断链表是否有环,使用快慢指针
2020/12/25
- 哈希可以降低时间复杂度(以空间换时间)
- 两条相交链表找交点,使用拼接链表的方式
- 对于有序序列,可以使用二分查找降低时间复杂度
- 双指针向中间扫描,可以解决有序数列找两数和的问题
- python实现ASCII操作的函数:
ord():字符转ASCII
,chr():ASCII数字转字符
- 列表推导式表示字母表
[chr(ord('A')+c) for c in range(26)]