备战蓝桥杯(搜索DFS+BFS+二分+递归+博弈)
什么是dfs
深度优先遍历(Depth First Search, 简称 DFS) 与广度优先遍历(Breath First
Search)是图论中两种非常重要的算法,生产上广泛用于拓扑排序,寻路(走迷宫),搜索引擎,爬虫等,也频繁出现在
leetcode,高频面试题中。
介绍
深度优先遍历主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。
深度优先搜索的步骤分为
1.递归下去
2.回溯上来。
顾名思义,深度优先,则是以深度为准则,先一条路走到底,直到达到目标。这里称之为递归下去。 否则既没有达到目标又无路可走了,那么则退回到上一步的状态,走其他路。这便是回溯上来。
例题(蓝桥真题)
小朋友崇拜圈
题目链接:https://www.lanqiao.cn/problems/182/learning/
解题
在这里插入代码片#include<bits/stdc++.h>
using namespace std;
int ans=0;
int start; // 起点
int a[100010]; //存值 i是本身下表 a【i】 是下一个下表
int b[100010]={
0}; //b是所到达的位置 如果到达重复的 就结束
void dfs(int k,int sum) //当前为k sum和为多少个
{
if(b[k]==1)
{
if(a[k]==a[start])
{
if(sum>=ans)
ans=sum;
//return 一开始写在这 然后 想了半天 为啥错 傻子!!!
}
return ; //return的位置一定要写对
}
//表示当前这个点走过了
b[k]=1;
dfs(a[k],sum+1);
b[k]=0; //一定要记得清空 不然 会影响下次的结果
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i]; //i是自己的位置 a[i]是指向的位置
for(int i=1;i<=n;i++)
{
start=i; // 起点
dfs(i,0);
}
cout<<ans<<endl;
}
什么是bfs
介绍
广度优先搜索较之深度优先搜索之不同在于,深度优先搜索旨在不管有多少条岔路,先一条路走到底,不成功就返回上一个路口然后就选择下一条岔路,而广度优先搜索旨在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路,并重复这样的操作。
类似 层序遍历!!!
例题(蓝桥真题)
长草
题目链接: https://www.lanqiao.cn/problems/149/learning/
题目描述
小明有一块空地,他将这块空地划分为 n 行 m 列的小块,每行和每列的长度都为 1。
小明选了其中的一些小块空地,种上了草,其他小块仍然保持是空地。
这些草长得很快,每个月,草都会向外长出一些,如果一个小块种了草,则它将向自己的上、下、左、右四小块空地扩展,
这四小块空地都将变为有草的小块。请告诉小明,k 个月后空地上哪些地方有草。
输入描述
输入的第一行包含两个整数 n, m。
接下来 n 行,每行包含 m 个字母,表示初始的空地状态,字母之间没有空格。如果为小数点,表示为空地,如果字母为 g,表示种了草。
接下来包含一个整数 kk。 其中 2≤n,m≤1000,1≤k≤1000。
输出描述
输出 n 行,每行包含 m 个字母,表示 k 个月后空地的状态。如果为小数点,表示为空地,如果字母为 g,表示长了草。
输入
4 5
.g...
.....
..g..
.....
2
输出
gggg.
gggg.
ggggg
.ggg.
解题:
#include<bits/stdc++.h>
using namespace std;
int n,m;
char a[1010][1010]; //初始值
char b[1010][1010];
void bfs()
//为什么要使用一个b呢 不然 你地址传递 就是动态修改 你需要的是利用这一次修改下一次的值 而不是动态修改
{
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
b[i][j]=a[i][j];
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
if(b[i][j]=='g'){
//当前为 地 那么就长!!!
{
if(i>=1) //zuo
a[i-1][j]='g';
if(j>=1) //上
a[i][j-1]='g';
if(i<n-1)
a[i+1][j]='g' ;
if(j<m-1)
a[i][j+1]='g' ;
}
}
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
cin>>a[i][j];
int k;
cin>>k;
for(int i=0;i<k;i++)
bfs();
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
cout<<a[i][j];
cout<<endl;
}
}
什么是二分查找
介绍
二分査找就是折半查找,其基本思想是:首先选取表中间位置的记录,将其关键字与给定关键字 key 进行比较,若相等,则査找成功;若 key 值比该关键字值大,则要找的元素一定在右子表中,则继续对右子表进行折半查找:若 key 值比该关键宇值小,则要找的元素一定在左子表中,继续对左子表进行折半査找。如此递推,直到査找成功或査找失败(或査找范围为 0)。
例题(蓝桥真题)
分巧克力
题目链接:https://www.lanqiao.cn/problems/99/learning/
题目描述
儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。
小明一共有 N 块巧克力,其中第 i块是 Hi×Wi 的方格组成的长方形。为了公平起见,
小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。切出的巧克力需要满足:
1.形状是正方形,边长是整数;
2.大小相同;
例如一块 6x5 的巧克力可以切出 6 块 2x2 的巧克力或者 2 块 3x3 的巧克力。
当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?
输入描述
输出描述
输出切出的正方形巧克力最大可能的边长。
输入输出样例
输入
2 10
6 5
5 6
输出
2
解题:
可以将原问题理解为:在1~100000之间寻找一个maxX,使得将巧克力按照边长maxX进行切分,切分成的份数要大于等于K,而如果按照maxX+1进行切割,将不再能够切出K块。如果从1-100000逐个查找,那么肯定超时,所以采用二分查找。
代码解析:
#include<bits/stdc++.h>
using namespace std;
int a[110000],b[110000]; //长宽
int n,k;
//由于一块大小为H*W(长为H,宽为W)的巧克力,
//可以切割成的边长为mid的正方形巧克力的块数为:(H/mid)*(W/mid)
bool check(int t){
int sum=0;
for(int i=0;i<n;i++)
{
sum+=(a[i]/t)*(b[i]/t);
}
if(sum>=k) return true;
return false;
}
int main(){
cin>>n>>k;
for(int i=0;i<n;i++)
cin>>a[i]>>b[i];
int left=0;
int right=100010; // 二分大小
while(left<=right){
int mid=(left+right)/2;
if(check(mid))
left=mid+1;
else right=mid-1;
}
cout<<left-1<<endl;
}
扫地机器人
题目链接: https://www.lanqiao.cn/problems/199/learning/
输出
一个整数代表答案。
样例输入
10 3
5
2
10
样例输出
6
解题:
分析:
挺明显的一个二分题目。我们二分每个机器人的扫地范围,根据数学知识我们可以知道,当每个机器人清扫的面积相差不大时,耗时最少。假设二分的扫地范围是x,对于每一个扫地机器人,我们尽可能让它扫地的右边界大一些,也就是扫过的格子,没有必要绝对不扫。最后看扫地的右边界是否大于等于格子的边界,如果是的话,就说明符合条件,否则就不符合条件。
ps:花费时间 = 2 × (扫地范围 - 1) 可以画图试一下!!!
代码解析:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, k;
int a[N];
bool check(int x)
{
int sum = 0; // sum 表示机器人已经扫到的区域右边界
for (int i = 0; i < k; i ++)
{
if(a[i] - x > sum)
return false; // 不能无缝衔接扫地区域,则一定失败
else
{
if(a[i] <= sum)
sum= a[i] + x - 1; // 在区域内,则从本区域开始,能扫到的最远区域
else
sum += x; // 在区域外,则直接从边界累加
}
}
return sum >= n; // 判断是否能扫完整个区域
}
int main()
{
cin >> n >> k;
for (int i = 0; i < k; i ++) cin >> a[i];
sort(a, a + k);
int l = 0, r = n;
while(l < r)
{
int mid = l + r >> 1; // 二分机器人的扫地范围
if(check(mid)) r = mid;
else l = mid + 1;
}
cout << 2 * l - 2 << endl; // 花费时间 = 2 × (扫地范围 - 1)
return 0;
}
递归
介绍
借用百度百科的话:
程序调用自身的编程技巧称为递归( recursion)。
递归是一种解决问题的方法,其主要思想在于:
将问题分为规模更小的相同问题 持续分解,直到问题规模小到可以用非常简单直接的方式来解决
递归问题的分解方式非常独特,其算法方面的明显特征就是:在算法流程中调用自身。递归为我们提供了一种对复杂问题的优雅解决方案,精妙的递归算法常会出奇简单,令人赞叹。
例如:斐波拉契数列,阶乘,汉诺塔。
好了 废话不多说
接下来 我们来 说一说 蓝桥真题
模板
#include<stdio.h>
// 这是一个阶乘的模板 其实都是差不多的
int factor(int n)
{
if (n == 1)
{
return 1;
}
return n * factor(n - 1);
}
int main()
{
int n = 5;
int ret = factor(n);
printf("%d\n", ret);
return 0;
}
写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。 :
如何做递归题
递归第一个步骤:明确函数要做什么 找子问题递归第二个步骤:明确递归的结束(退出递归)条件 找出口
递归的第三个步骤:找到函数的等价关系式 如何递归
例题(蓝桥真题)
金额查错
题目链接:https://www.lanqiao.cn/problems/291/learning/
题目描述
某财务部门结账时发现总金额不对头。很可能是从明细上漏掉了某 1 笔或几笔。如果已知明细账目清单,能通过编程找到漏掉的是哪 1 笔或几笔吗?
如果有多种可能,则输出所有可能的情况。
输入描述
用户输入的第一行是:有错的总金额。
接下来是一个整数 n,表示下面将要输入的明细账目的条数。
再接下来是 n 行整数,分别表示每笔账目的金额。
为了方便,不妨假设所有的金额都是整数;每笔金额不超过 1000,金额的明细条数不超过 100。
输出描述
所有可能漏掉的金额组合。每个情况 1 行。金额按照从小到大排列,中间用空格分开。
输入输出样例
输入
6
5
3
2
4
3
1
输出
1 2 4
1 3 3
3 4
解题:
思路 : 就是 除去那些金额组合情况 然后剩下的金额加起来刚好等于一开始给的数字(乌鱼子 一开始 读了半天题目 没有读明白 真是个小天才(大傻逼))
代码:
#include<bits/stdc++.h>
using namespace std;
int m;
// 其实 我不清楚 为什么这个题 划分到递归 感觉就是一个dfs
void dfs(int a[],int x,int err,int b[],int sum)
//a 是数组,x是当前位置 err是错误的钱,b是使用了那些钱 sum 是已经统计的钱
{
if(sum>err)
return ;
if(sum==err) //当前值 刚好成立
{
for (int i=1; i < m; i++) // 去重
{
if (a[i] == a[i-1] && b[i-1] && !b[i]) return;
}
for(int i=0;i<m;i++)
{
if(b[i]==0)
cout<<a[i]<<" ";
}
cout<<endl;
return ;
}
if(x>=m)
return ;
//如果 我不要当前值
b[x]=0;
dfs(a,x+1,err,b,sum);
//需要当前值
b[x]=1;
dfs(a,x+1,err,b,sum+a[x]);
b[x]=0; //回溯
}
int main(){
int n;
cin>>n;
cin>>m;
int a[m],b[m]={
0};
for(int i=0;i<m;i++)
cin>>a[i];
sort(a,a+m);
dfs(a,0,n,b,0);
}
// 3 4 情况 输出了俩次 解决方法
// 去重 因为3 3 出现俩次 那么 必定只选了一个
// for (int i=1; i < n; i++) // 去重
// {
// if (a[i] == a[i-1] && b[i-1] && !b[i]) return;
// }
母牛的故事
题目链接: https://www.dotcpp.com/oj/problem1004.html
题目描述
有一头母牛,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。请编程实现在第n年的时候,共有多少头母牛?
输入
输入数据由多个测试实例组成,每个测试实例占一行,包括一个整数n(0<n<55),n的含义如题目中描述。
n=0表示输入数据的结束,不做处理。
输出
对于每个测试实例,输出在第n年的时候母牛的数量。
每个输出占一行。
样例输入
2
4
5
0
样例输出
2
4
6
解题:
第n年的母牛的来源分别来自于前一年剩下的和往前推3年的母牛(能生的母牛,最早的也算在里面)
第五年:
6 = 前一年剩下的4头牛+(第2年新产的母牛刚具有生育能力生下的1头+最早的母牛1头)
解题公式:f(n) = f(n-1) + f(n-3)
#include<bits/stdc++.h>
using namespace std;
int a[56]={
0,1,2,3,4}; //为什么要用数组 因为 n<55 递归量不大 同时防止时间超限
int init(){
for(int i=5;i<56;i++)
a[i]=a[i-1]+a[i-3];
}
int main(){
init();
int n;
cin>>n;
while(n!=0){
cout<<a[n]<<endl;
cin>>n;
}
}
博弈
介绍
博弈的策略:参与者在行动之前所准备好的一套完整的行动方案,就是想好下完这步棋,对方会如何下.特点
1.博弈模型为两人轮流决策的非合作博弈。即两人轮流进行决策,并且两人都使用最优策略来获取胜利
2.博弈是有限的。即无论两人怎样决策,都会在有限步后决出胜负
3.公平博弈。即两人进行决策所遵循的规则相同
常见的博弈算法
1.巴什博弈
1、问题模型:有一个堆物品,物品数量为n个,两个人轮流从这堆物品中取物品,规定每次至少取一个,最多取m个,最后取光者得胜。
2、解决思路:当n=m+1时,由于一次最多只能取m个,所以无论先取者拿走多少个,后取者都能够一次拿走剩余的物品,后者取胜,所以当一方面对的局势是n%(m+1)=0时,其面临的是必败的局势。所以当n=(m+1)*r+s,(r为任意自然数,s≤m)时,如果先取者要拿走s个物品,如果后取者拿走x(≤m)个,那么先取者再拿走m+1-k个,结果剩下(m+1)(r-1)个,以后保持这样的取法,那么先取者肯定获胜。总之,要保持给对手留下(m+1)的倍数,就能最后获胜。
结论:如果条件是最后取光者得胜,那么当先手面临的局势是n%(m+1)==0,先手必败#include<stdio.h>
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n,m;
scanf("%d%d",&n,&m);
if(n%(m+1)==0)
printf("second win\n");
else
printf("first win\n");
}
return 0;
}
3、变形:条件不变,改为最后取光的人输。
结论:如果条件是最后取光者失败,那么当先手面临的局势是(n-1)%(m+1)==0时,先手必败。
2.威佐夫博弈
1、问题模型:有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
2、解决思路: 设(ai,bi) (ai ≤bi ,i=0,1,2,…,n)表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了,这种局势我们称为奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。任给一个局势(a,b),如下公式判断它是不是奇异局势:ak=[k(1+√5)/2],bk=ak+k (k=0,1,2,…,n 方括号表示取整函数)。
3、满足上公式的局势性质:
(1)任何自然数都包含在一个且仅有一个奇异局势中。
由于ak是未在前面出现过的最小自然数,所以有ak>ak-1,而bk=ak+k>ak-1+k-1=bk-1>ak-1,所以性质成立。
(2)任意操作都可将奇异局势变为非奇异局势。
若只改变奇异局势(ak,bk)的某一个分量,那么另一个分量不可能在其他奇异局势中,所以必然是非奇异局势。如果使(ak,bk)的两个分量同时减少,则由于其差不变,且不可能是其他奇异局势的差,因此也是非奇异局势
(3)采用适当的方法,可以将非奇异局势变为奇异局势。
假设面对的局势是(a,b),若 b = a,则同时从两堆中取走 a 个物体,就变为了奇异局势(0,0);如果a = ak ,b >bk,那么,取走b – bk个物体,即变为奇异局势;如果 a = ak,b < bk ,则同时从两堆中拿走 ak – ab – ak个物体,变为奇异局势( ab – ak , ab – ak+ b – ak);如果a > ak,b= ak + k,则从第一堆中拿走多余的数量a – ak 即可;如果a < ak ,b= ak + k,分两种情况,第一种,a=aj (j < k),从第二堆里面拿走 b – bj 即可; 第二种,a=bj (j<k),从第二堆里面拿走 b – aj 即可。
4、结论:两个人如果都采用正确操作,那么面对非奇异局势,先拿者必胜;反之,则后拿者取胜。
#include<stdio.h>
#include<math.h>
#include<algorithm>
using namespace std;
int main()
{
int a,b;
while(~scanf("%d%d",&a,&b))
{
if(a>b)
swap(a,b);
int ans=floor((b-a)*(sqrt(5)+1)/2); //判断是否是奇异局势
if(a==ans)
printf("second win\n");
else
printf("first win\n");
}
return 0;
}
另外还有 斐波那契博弈 尼姆博弈 等等 不在一一介绍了
例题(蓝桥真题)
取球游戏
题目链接: https://www.lanqiao.cn/problems/278/learning/
题目描述
今盒子里有 nn 个小球,A、B 两人轮流从盒中取球,每个人都可以看到另一个人取了多少个,也可以看到盒中还剩下多少个,并且两人都很聪明,不会做出错误的判断。
我们约定:
每个人从盒子中取出的球的数目必须是:1,3,7 或者 8 个。轮到某一方取球时不能弃权!A
先取球,然后双方交替取球,直到取完。被迫拿到最后一个球的一方为负方(输方)请编程确定出在双方都不判断失误的情况下,对于特定的初始球数,A 是否能赢?
输入
输出
程序则输出 nn 行,表示 A 的输赢情况(输为 0,赢为 1)。
样例输入
4
1
2
10
18
样例输出
0
1
1
0
解题:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a[11000]={
0};
for(int i=1;i<10000;i++)
{
if(a[i]==0)
{
a[i+1]=1;
a[i+3]=1;
a[i+7]=1;
a[i+8]=1;
}
}
// 没啥好说的 暴力
int n;
cin>>n;
while(n--)
{
int m;
cin>>m;
cout<<a[m]<<endl;
}
return 0;
}