真题考察的重点是:暴力枚举(学会计算复杂度与优化)、递归(回溯、剪枝,关键是理解递归套路与原理)、排序(冒泡、插入、希尔、快排)、搜索(深搜、广搜、二分查找)、动态规划、过程模拟、思维、贪心等。以下是早期蓝桥备赛学习的一点个人总结,以后会继续更新完善。
步骤:
1确定枚举的变量
2确定每个枚举变量对应范围
3确定条件判断语句
枚举常见优化套路:
1减少枚举变量
2缩小枚举范围
3二分查找(复杂度由O(n)—>O(lgn))
4空间换时间(双指针、Hash)
/*
题目:平方十位数
由0~9这10个数字不重复、不遗漏,可以组成很多10位数字。
这其中也有很多恰好是平方数(是某个数的平方)。
比如:1026753849,就是其中最小的一个平方数。
请你找出其中最大的一个平方数是多少?
思路:
1.确定枚举的变量:X
2.确定枚举的范围:[9876543210,1026753849]
3.判断答案符合的条件:判断恰好0-9十个数字、判断是不是完全平方数
但直接枚举枚举平方数显然规模太大,看看如何优化,一种明显的方案是枚举较小的平方根数
1.枚举X的开平方跟Y
2.范围[100000,32043]
3.判断答案符合的条件:平方后的数组成恰好是0-9是个数字
代码实现:
*/
#include
#include
using namespace std;
bool contain(long long x)//用set容器的特性判断数字的组成是0-9不同的数字
{
if (x==0)
{
return false;
}
set<long long> s;
//分离x的各位数字,每次把x的末尾数字分离出去
while (x)
{
long long d = x % 10;
s.insert(d);
x /= 10;
}
return s.size() == 10;
}
int main()
{
for (long long i = 100000; i >=32043;i--)
{
long long x = i*i;
if (contain(x))
{
cout << x << endl;
break;
}
}
return 0;
}
//收获:参考了大佬的代码学习了set容器的新知识,集合set是一种包含已排序对象的关联容器
//其每个键只能对应一个元素,即不存在键相同的不同元素
//故常用set容器来快速判断元素的组成问题。
/*
题目:递增三元组
给定三个整数数组
A = [A1, A2, … AN],
B = [B1, B2, … BN],
C = [C1, C2, … CN],
请你统计有多少个三元组(i, j, k) 满足:
1. 1 <= i, j, k <= N
2. Ai < Bj < Ck
思路:
1.确定枚举的变量:三个数组的下标:i,j,k
2.确定枚举的范围:i j k[1,n]三重循环
3.判断答案符合的条件:判断满足Ai < Bj < Ck,计数
但直接三重循环枚举显然复杂度又太大,看看如何优化,看题目只要求计数而与元素位置无关,故想到了先排序,显然排完序之后二分查找就是最快的了。
1.枚举数组B的下标j;
2.范围 j [1,n];
3.判断答案符合的条件:判断每个j中满足Ai < Bj < Ck的i和k的个数;
代码实现:
*/
#include
#include
#include
#include
#include
using namespace std;
const int maxn=1e5+10;
int a[maxn],b[maxn],c[maxn];
int main(){
int n;
cin>>n;
for(int i=0; i<n; i++ )
cin>>a[i];
for(int i=0; i<n; i++ )
cin>>b[i];
for(int i=0; i<n; i++ )
cin>>c[i];
sort(a,a+n);
sort(b,b+n);
sort(c,c+n);
long long cnt=0;
//以b为中间值
for(int i=0; i<n; i++ ){
long long pos1 = lower_bound(a,a+n,b[i])-a; //在a数组中查找不小于b[i]的第一个数的位置
long long pos2 = upper_bound(c,c+n,b[i])-c; //在c数组中查找大于b[i]的最第一个数的位置
cnt += (long long)pos1*(n-pos2);
}
cout<<cnt<<endl;
return 0;
}
//收获:将有序与二分查找绑定在一起,巧用STL库函数 lower_bound()、upper_bound()等
练习策略:
循环改递归
经典递归问题
写递归的三个步骤:
/*
* f1(n):求n的阶乘-->f(n-1):求n-1的阶乘
* 找重复:n*(n-1)的阶乘,求n-1的阶乘是原问题的重复 (规模更小)——子问题
* 找变化:变化的量作为参数
* 找边界:出口(当n=1时停止)返回一个值,当n=1时函数求1的阶乘,故返回1;
* */
int f1(int n){
if(n==1) return 1;
return n*f1(n-1);
}
/*
* f2(i,j)打印从i到j
* 找重复:先打印i,再打印i+1到j,即打印i+1到j是原问题的重复——子问题
* 找变化:i变成i+1
* 找边界:当i>j时停止
* */
void f2(int i,int j){
if(i>j){
return;
}
cout<<i<<' ';
f2(i+1,j);
}
/*
* f3(int[] arr)数组求和
* 找重复:先求第一个元素,再求剩下的,即求剩下的是原问题的重复——子问题
* 找变化:开始的元素在往后推,所以要加变量f3(int[] arr,int begin)
* 找边界:当i>j时停止
* */
int f3(int[] arr,int begin){
if(begin==arr.length-1){
return arr[begin];
}
return arr[begin]+f3(arr,begin+1);
}
/*
* binarySearch1()在全范围内二分查找
* 可分解为:
* 左边找(递归)
* 中间比
* 右边找(递归)
* */
int binarySearch1(int[] arr,int low,int high,int key){
if(low>high){
return -1;
}
int mid=low+((high-low)>>1);//(low+high)>>>1
// ;右移1位相当于乘以二分之一,防止溢出,位运算也更加高效
int midVal=arr[mid];
if(midVal<key){
return binarySearch1(arr,mid+1,high,key);
}else if(midVal>key){
return binarySearch1(arr,low,mid-1,key);
}else{
return mid;//找到key
}
}
//递归的思想感觉就像是分治的思想,分而治之,把一个复杂的问题分解成很多规模较小的子问题,然后解决这些子问题,把解决的子问题合并起来,大问题就解决了
//经典递归:汉诺塔游戏
//把n个盘从A移动到C
//C是目标盘,B是辅助盘
//等价于
//step1:把前n-1个从A移动到B,C做辅助
//step2:把最底下的一个从A移动到C
//step3:把前n-1个从B移动到C,A做辅助
int sum=0;//移动次数
void printHanoiTower(int n, char from, char to, char help){
if(n==1){
cout<<"move"<<n<<":"<<from<<"->"<<to<<endl;
sum++;
return;
}
printHanoiTower(n-1,from,help,to);
cout<<"move"<<n<<":"<<from<<"->"<<to<<endl;
sum++;
printHanoiTower(n-1,help,to,from);
}
int main() {
printHanoiTower(3,'A','B','C');
cout<<sum;
}
#include
using namespace std;
int f(int x,int y){
if(y==0||x==y)
return 1;
else
return f(x-1,y-1)+f(x-1,y);
}
int main(){
int x,y;
cin>>x>>y;
cout<<f(x,y)<<endl;
return 0;
}
基础排序算法:
冒泡(交换)、选择(求最大最小)、插入(挪动数组)、希尔(缩小增量排序)
常用且实用排序:
以上三个排序的复杂度都是O(nlogn)
一、sort()函数
头文件:<algorithm>
1.调用方法:sort(第一项的地址,最后一项的地址+1);
如sort(&a[0],&a[n]);或sort(a,a+n);
注意STL的区间是左闭右开;
2.自定义规则的排序:
有时排序的条件不止一个,或不方便对原数据进行排序,就需要自定义比较规则。
这时需要一个写一个函数,把比较的条件解释清楚。
如:
bool cmp(const int &i,const int &j){return w[i]<w[j];}//自定义比较规则
sort(a,a+n,cmp);
二、快速排序
快排是基于比较排序中最快的一种算法。
void quicksort(int *a,int start,int end){
int low=start,high=end;
int temp,check=a[start];
//划分:把比check小的数据放在它的左边,把比check大的数据放在右边
do{
while(a[high]>=check&&low<high) //注意不要写成“low<=high”!
high--;
if(a[high]<check)
temp=a[high],a[high]=a[low],a[low]=temp;
while(a[low]>=check&&low<high) //注意不要写成“low<=high”!
low++;
if(a[low]>check)
temp=a[high],a[high]=a[low],a[low]=temp;
}
while(low!=high);
a[low]=check;
low--;
high++;
//递归:对已经划分好的两部分分别进行快速排序
if(low>start) quicksort(a,start,low);
if(high<end) quicksort(a,high,end);
}
三、归并排序
其他排序算法的空间复杂度是O(1),而并归排序的空间复杂度很大,为O(n)。
int temp[N];
void mergesort(int *a,int start,int end){
if(start+1>=end) return;
//划分阶段、递归
int mid=start+(end-start)/2;
mergesort(a,start,mid);
mergesort(a,mid,end);
//将mid两侧的两个有序表合并为一个有序表
int p=start,q=mid,r=start;
while(p<mid||q<end){
if(q>=end||(p<mid&&a[p]<=a[q]))
temp[r++]=a[p++];
else
temp[r++]=a[q++];
}
for(p=start;p<end;p++){
a[p]=temp[p];
}
}
//并归排序的空间复杂度较大,当数据规模不大时,也可以用插入排序代替并归排序
操作流程图:
文字描述:
清空队列
为队列增加起点所在节点
在队列首部取出一个节点
扫描这个节点所能到达的节点,并将它们放在队列尾部(任意顺序)
结束本节点的动作,并准备再次拿取队列首部节点,直到队列为空。
回溯和剪枝:
递归调用后就会开启一个分支,如果想这个分支返回后数据恢复到分支开启之前的状态一遍重新开始,就要用到回溯技巧。全排列的交换法、数独游戏、部分和都用到了回溯;
深搜时如果已经明确当前一条路是错误的,无法得到最优解时,就应该中断往下的继续搜索,这种方法称为剪枝。数独游戏和部分和用到了剪枝。
/*
题目:迷宫
下图给出了一个迷宫的平面图,其中标记为1 的为障碍,标记为0 的为可
以通行的地方。
010000
000100
001001
110000
迷宫的入口为左上角,出口为右下角,在迷宫中,只能从一个位置走到这
个它的上、下、左、右四个方向之一。
对于上面的迷宫,从入口开始,可以按DRRURRDDDR 的顺序通过迷宫,
一共10 步。其中D、U、L、R 分别表示向下、向上、向左、向右走。
对于下面这个更复杂的迷宫(30 行50 列),请找出一种通过迷宫的方式,
其使用的步数最少,在步数最少的前提下,请找出字典序最小的一个作为答案。
请注意在字典序中D
/*
*/
//DFS
#include
#include
using namespace std;
const int ax[4]={0,0,1,-1};//方向
const int ay[4]={1,-1,0,0};
const char dir[5]={'R','L','D','U'};
int maze[60][60],mins[60][60];
char a[2000];
int best;
string ans;
bool judge(int x,int y) {//判断这个点是否越界,是否走过。
if(x>0&&x<=30&&y>0&&y<=50&&!maze[x][y])
return true;
return false;
}
void dfs(int x,int y,int pos) {
if(pos>best)
return ;
if(x==30&&y==50) {
string temp;
for(int i=1;i<pos;i++)
temp+=a[i];
if(pos<best) {//是最短的路
ans=temp;
best=pos;
}
else if(pos==best&&temp<ans) ans=temp;//在实现路时最短的同时,保证字典序最小。
return ;
}
for(int i=0;i<4;i++) {//遍历上下左右四个方向
int tempx=x+ax[i];
int tempy=y+ay[i];
if(judge(tempx,tempy)&&pos+1<=mins[tempx][tempy]) {
maze[tempx][tempy]=1;
mins[tempx][tempy]=pos+1;
a[pos]=dir[i];
dfs(tempx,tempy,pos+1);//递归
maze[tempx][tempy]=0;//回溯找短的路,或者时字典序最小的。
}
}
}
int main()
{
freopen("ce.txt","r",stdin);//打开本地文件,将文件数据输入
memset(mins,1,sizeof(mins));
best=1<<28;
for(int i=1;i<=30;i++)
for(int j=1;j<=50;j++) {
char t;
cin>>t;
maze[i][j]=t-'0'; //定义为char才能一次输入一个数字,-‘0’转换为int
}
maze[1][1]=1;
dfs(1,1,1);
cout<<ans<<endl;
return 0;
}
//数组mins记录了从起始位置到当前的最短步数
//BFS
#include
#include
#include //用到队列
const int ax[4]={1,0,0,-1};
const int ay[4]={0,-1,1,0};
const char dir[5]={'D','L','R','U'};
int maze[40][60];
using namespace std;
struct point {
int x,y;
string ans;
point(int a,int b,string c):x(a),y(b),ans(c){}
};
bool judge(int x,int y) {
if(x>0&&x<=30&&y>0&&y<=50&&!maze[x][y])
return true;
return false;
}
void bfs() {
queue<point>ans;
ans.push(point(1,1,""));//这样写一行就初始化加入栈了
maze[1][1]=1;
while(!ans.empty()) {
point temp=ans.front();
ans.pop();
if(temp.x==30&&temp.y==50) {
cout<<temp.ans<<endl;
return ;
}
for(int i=0;i<4;i++) {
int tempx=temp.x+ax[i];
int tempy=temp.y+ay[i];
if(judge(tempx,tempy)) {
maze[tempx][tempy]=1;
ans.push(point(tempx,tempy,temp.ans+dir[i]));
}
}
}
}
int main() {
freopen("ce.txt","r",stdin);
for(int i=1;i<=30;i++)
for(int j=1;j<=50;j++) {
char t;
cin>>t;
maze[i][j]=t-'0';
}
bfs();
return 0;
}
//收获:
1. 当题目输入数据非常庞大时,为了避免输入错误,可以通过使用freopen函数可以解决测试数据输入问题,避免重复输入。调用freopen,就可以修改标准流文件的默认值,实现重定向。
freopen("in.txt","r",stdin); //输入重定向,输入数据将从in.txt文件中读取
freopen("out.txt","w",stdout); //输出重定向,输出数据将保存在out.txt文件中
freopen("in.txt","r",stdin)的作用就是把标准输入流stdin重定向到in.txt文件中,这样在用scanf或是用cin输入时便不会从标准输入流读取数据,而是从in.txt文件中获取输入
2.BFS中队列的用法:
back()返回最后一个元素
empty()如果队列空则返回真
front()返回第一个元素
pop()删除第一个元素
push()在末尾加入一个元素
size()返回队列中元素的个数
DFS求解8皇后问题
#include
using namespace std;
const int N=10;
int a[N]; //a[i]表示第i行的第a[i]列上放皇后
int cnt,n; //答案个数
bool check(int x,int y){
for(int i=1;i<=x;i++){
if(a[i]==y)return false;
if(a[i]+i==x+y)return false;
if(a[i]-i==y-x)return false;
}
return true;
}
void dfs(int row){//表示第row皇后放于何处
if(row==n+1){
//产生了一组解
cnt++;
return;
}
for(int i=1;i<=n;i++){
if(check(row,i)){
a[row]=i;
dfs(row+1);
a[row]=0;
}
}
}
int main(){
cin>>n;
dfs(1);
cout<<cnt<<endl;
return 0;
}
//输入8,输出92
/*思路:
把每个点都模拟删一遍
用dfs求这个节点下面节点的个数,同时这个节点以上节点的个数就可以知道了
*/
#include
using namespace std;
const int N=1e5+10;
int h[N],e[N*2],ne[N*2],idx,n;//无向边n*2
int g[N];//每个断开节点连通块的最大值
bool st[N];//是否遍历过
void add(int a, int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int dfs(int u){//u当前判断的节点
int sum=1;//sum表示包括自己往下的节点的合
for(int i = h[u];i!=-1;i=ne[i]){//遍历u的单链表
int j=e[i];
if(st[j])continue;//判断是否合法
st[j]=true;
int col=dfs(j);
//st[j]=false;
if(col>g[u])g[u]=col;
sum+=col;
}
if(g[u]<n-sum) g[u]=n-sum;
return sum;
}
int main(){
memset(h,-1,sizeof(h));
cin>>n;
for(int i=0;i<n-1;i++){
cin>>a>>b;
add(a,b);
add(b,a);
}
st[1]=true;
dfs(1);
int min=0x3f3f3f3f;
for(int i=1;i<=n;i++){
if(g[i]<min)min=g[i];
}
cout<<min<<endl;
return 0;
}
记住几个常用的函数与其对应头文件
vector、set和map这三个容器
set<int> ans;
ans.earse();//删除指定元素
ans.clear();//清空容器
ans.find(val) != ans.end();//说明找到了val这个元素
for(set<int>::iterator it = ans.begin(); it != ans.end(); it++){//从小到大输出容器内元元素
cout << *it << ' ';
}
查找:
find()、find_if()、search()
这些函数的底层实现都采用的是顺序查找(逐个遍历)的方式
当区域数据处于有序状态时,就要想到用二分查找提高效率
lower_bound()、upper_bound()、equal_range() 以及 binary_search() ,都是二分查找函数
排序:
前面说过的sort(a,a+n),复杂度是O(nlogn)
全排列next_permutation(a,a+n ),可产生指定范围内的下一个全排列
#include
#include
using namespace std;
int main(){
int n;
while(scanf("%d",&n)&&n){
int a[1000];
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
sort(a,a+n);
do{
for(int i=0;i<n;i++)
printf("%d ",a[i]);
printf("\n");
}while(next_permutation(a,a+n));
}
return 0;
}
s.substr(pos, len):复制对应区域子字符串
s.substr(pos):复制这个位置之后的字符串
//字符串转成整型数字
//string头文件
string s;
int n;
n=atoi(s.c_str());
因为不知道有多少组以前经常不知道该如何结束程序
看大佬们的题解发现常用的有以下三种方法
1 while(scanf("%d",&n)!=EOF),最常用
2 while(scanf("%d",&n)),取反位
3 while(scanf("%d",&n),n) ,当题目要求输入0停止时使用
//4.30今日错误
人见人爱A-B,题目要求当输入的m和n都为0时,结束程序
一开始写成了这样:while(scanf("%d%d", &m, &n) , m, n)
虽然用例能过,也能正常推出程序,但是应该值判断了m是否为零而没判断n,导致WA
后来想通了之后,如下正确写法!!!
while(scanf("%d%d", &m, &n) , m||n)
递归时若需要反复进行相同计算,可以用动态规划,用数组存起来
实验思维与猜想:当问题过于复杂,无法联系自己所学知识时,要尽己所能,先解决规模更小的问题,找找问题是否存在规律。