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;
}
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/3/18
- C++11用nullptr代替了NULL表示空指针,因为NULL在某些情况下会产生歧义
int*a=NULL;
判断a是否为NULL/nullptr
可以用if(!a)
表示a是NULL
- 写了篇Floyd判圈法的博文。
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/22
- 选出数组中第K大的数
- 先从大到小排序,再选出第K个
- 时间复杂度更低的方法:使用选择排序或者快速排序的思想(每轮确定一个元素的最终位置 )
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/27
- 蓝桥杯只使用ANSI C/ANSI C++ 标准,不能使用C++11语法。具体则是在CodeBlocks中勾选 ISO1998标准。
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);
}