2023年 c++ b组 省一+国二
题单链接:活动:蓝桥杯C++ AB组辅导课 - AcWing
第一讲 递归与递推(8/8)
AcWing 95. 费解的开关
/*
给定一组5*5的矩阵,矩阵上的元素只有0和1两种状态,可以操作一位,将其元素改变,并且其上下左右位置的元素也要进行改变
求是否存在小于六次的操作,使得矩阵的元素全变为1,并且输出最小操作次数
*/
#include <iostream>
#include <cstring>
using namespace std;
const int N=10;
char ch[N][N],key[N][N];
//偏移量
int dr[5]={-1,1,0,0,0};
int dc[5]={0,0,-1,1,0};
void turn(int r,int c) //进行翻转操作
{
for(int i=0;i<5;i++)
{
int tr=r+dr[i];
int tc=c+dc[i];
if(tr>=0&&tr<5&&tc>=0&&tc<5)
{
if(ch[tr][tc]=='0') ch[tr][tc]='1';
else ch[tr][tc]='0';
}
}
}
int main()
{
int all;
cin>>all;
while(all--)
{
for(int i=0;i<5;i++) scanf("%s",key[i]); //输入状态
int res=7; //因为是求小于6次的次数,故初始化为大于6的数
/*
枚举第一行按的所有情况(即2^5=32),当第一行的情况固定时,第一行的状态此时只能由第二行改变
故此时,当对第一行操作后,第一行如果还存在为0的元素,就只能且必须通过第二行的相同列来将其元素转换为1
以此类推,第i行操作结束后由第i+1行的操作将第i行的元素全部转化为1,直到最后一行
当最后一行存在0时,表明第一行的操作不合理,否则其所得到的次数就是在第一行固定操作下的最小次数
*/
for(int i=0;i<32;i++) //枚举第一行所有的情况
{
memcpy(ch,key,sizeof(ch)); //拷贝所给的初始状态
int cnt=0; //记录操作次数
for(int j=0;j<5;j++) //根据二进制来判断第一行的当前位数是否需要进行翻转
{
if(i>>j&1)
{
cnt++; //进行翻转,操作次数加一
turn(0,j);
}
}
for(int j=0;j<4;j++) //枚举0~4行,表明当其所在行操作结束后,其下一行应该怎样操作使得该行元素全为1
{
for(int k=0;k<5;k++)
{
if(ch[j][k]=='0') //当该元素ch[j][k]为0时,因为为了维护j-1行全为1(0行的操作是枚举得来的),故不能操作第j行及其上面的行的元素,
{ //又因为必须要使得ch[j][k]为1,故此时就只能通过ch[j+1][k]的操作来改变
turn(j+1,k); //翻转ch[j+1][k],使得ch[j][k]为1
cnt++; //操作次数加一
}
}
}
bool flag=true;
for(int j=0;j<5;j++) //枚举最后一行是否存在0,即第一行的操作是否能够使得所有的元素变为1
{
if(ch[4][j]=='0')
{
flag=false;
break;
}
}
if(flag) res=min(res,cnt); //如果能进行,则取最小操作次数
}
//输出结果
if(res<=6) printf("%d\n",res);
else printf("-1\n");
}
return 0;
}
AcWing 116. 飞行员兄弟
对于4*4的格子,每个格子最多按一次,故可以枚举每个格子按或不按的情况,通过二进制来枚举这些情况,并进行比较,求最小操作次数只需要在可满足解中选出操作次数最小的情况,求字典序最小只需要保证在操作次数最小时,记录最先枚举到的情况即可,综上即可求出最优解。
第二讲 二分与前缀和(9/9)
AcWing 730. 机器人跳跃问题
/*
给定1~n号建筑的高度,从高度为0的0号建筑出发
假设现在的能量值为e,如果从高到底跳,则失去h(下一位建筑)-e的能量,如果从低到高跳,则得到e-h(下一位建筑)的能量
要求在到达第n号建筑时,在整个过程中能量值不能为负数,求最小使用的能量值
通过推理得出,无论是失去还是获得,能量值的变化为e -> 2*e-h(下一位建筑),由此可以通过假设初始能量大小来判断其是否可行
并且初始的能量值越大,能完成游戏的成功率就越高,满足单调性可以进行二分
*/
#include <iostream>
using namespace std;
const int N=1e5+10;
int n;
int num[N];
bool check(int x) //检查假设的初始能量值大小是否可行
{
for(int i=1;i<=n;i++)
{
x=2*x-num[i];
if(x>=1e5) return true; //由于数据范围,最大高度为1e5,故当能量值为1e5时,就必定可以成功完成游戏
if(x<0) return false; //出现负数,则不能完成
}
return true;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>num[i];
int l=0,r=1e5;
while(l<r) //二分找出最终结果
{
int mid=(l+r)/2;
if(check(mid)) r=mid;
else l=mid+1;
}
cout<<r<<endl;
return 0;
}
AcWing 1221. 四平方和
/*
四平方和定理,又称为拉格朗日定理:每个正整数都可以表示为至多 4 个正整数的平方和。
对于a*a+b*b+c*c+d*d=n,给定一个n,求所有满足条件的(a,b,c,d)中,字典序最小的解,此处a,b,c,d可以为0
由于四重循环为超时,故先将c和d枚举出来,通过(c*c+d*d,c,d)升序排列
再枚举a,b,通过n-a*a-b*b二分查找出相对应且字典序最小的c*c+d*d的c和d
由于保证了a和b的字典序最小,且找相应c,d时也保证c,d的字典序在满足c*c+d*d=n-a*a-b*b时最小
故第一次得到的满足解即为所求
三重循环会超时
用map来存储mp[c*c+d*d]={c,d}会超时
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N=5e6+10;
int n,m;
bool st;
struct Node
{
int s,c,d;
bool operator < (const Node &t) const
{
if(s!=t.s) return s<t.s;
if(c!=t.c) return c<t.c;
return d<t.d;
}
}node[N];
int find(int x)
{
int l=0,r=m-1;
while(l<r)
{
int mid=(l+r)/2;
if(x<=node[mid].s) r=mid;
else l=mid+1;
}
return r;
}
int main()
{
cin>>n;
//预处理所有c和d的可能情况
for(int c=0;c*c<=n;c++)
{
for(int d=c;c*c+d*d<=n;d++)
{
int s=c*c+d*d;
node[m++]={s,c,d};
}
}
sort(node,node+m); //保证s=c*c+d*d具备二分查找所需要的单调性,且相同的s下的c和d字典序最小
//按字典序从小到大枚举a和b,并通过二分查找相应的最优c和d,故第一次可满足解即为最优解
for(int a=0;a*a<=n;a++)
{
for(int b=a;a*a+b*b<=n;b++)
{
int t=n-a*a-b*b;
int k=find(t);
if(node[k].s==t)
{
cout<<a<<" "<<b<<" "<<node[k].c<<" "<<node[k].d<<endl;
st=true;
break;
}
}
if(st) break;
}
return 0;
}
AcWing 99. 激光炸弹
边长覆盖的是格子,但是价值位于每个顶点上,注意辨别,进行二维前缀和即可。
AcWing 1230. K倍区间
/*
给定一个长度为n的数列,如果其中一段连续的子序列之和为k的倍数,就称这个区间为k倍区间
对于区间[i,j]分析,如果该区间为k倍区间,并且假设前缀和数组为sum,那么一定存在sum[i-1]%k==sum[j]%k
故只需要对每一位前缀和查询其之前存在多少个前缀和同时模k之后仍旧相等
特别,对于第0位,其前缀和模上k为0,该情况对应,当sum[i]%k==0时,[1,i]的区间为k倍区间
*/
#include <iostream>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int n,k;
LL sum[N],cnt[N];
int main()
{
cin>>n>>k;
//计算前缀和
for(int i=1;i<=n;i++)
{
cin>>sum[i];
sum[i]+=sum[i-1];
}
LL res=0;
cnt[0]++; //第0位前缀和应该被记录下
for(int i=1;i<=n;i++)
{
res+=cnt[sum[i]%k]; //查找第i位之前存在多少位与sum[i]模k所产生余数相同的前缀和数组,即存在多少位以i结尾的k倍区间
cnt[sum[i]%k]++; //把当前这一位存入计数数组里面,为后面求k倍区间提供条件
}
cout<<res<<endl;
return 0;
}
第三讲 数学与简单DP(8/8)
AcWing 1211. 蚂蚁感冒
/*
一根1~100的数轴,数轴上存在若干物体,物体初始在整数坐标上,按单位距离移动
物体的初始值表示其所在坐标,正负表示移动方向,每两个物品相遇后会瞬间折返,求接触过第一个物品(包括其本身)的物品个数
首先,两个物品相遇且瞬间折返的情况其实可以看成两个物品对穿
对数轴上除第一个物品之外的所有物品存在四种分类情况,假设第一个物品向左
1、位于物品左边且向左,此时永远不会与物品相遇
2、位于物品右边且向右,此时永远不会与物品相遇
3、位于物品左边且向右,此时一定会与物品相遇
4、位于物品右边且向左,此时如果存在第三种状态的物品,那么就一定会和物品相遇,否则就永远不会
第一个物品向右也同理
*/
#include <iostream>
#include <cmath>
using namespace std;
const int N=60;
int n;
int place[N];
int lr,rl;
int main()
{
cin>>n;
for(int i=0;i<n;i++) cin>>place[i];
for(int i=1;i<n;i++)
{
if(abs(place[0])>abs(place[i])&&place[i]>0) lr++; //记录位于第一个物品左边向右的物品个数
else if(abs(place[0])<abs(place[i])&&place[i]<0) rl++; //记录位于第一个物品右边向左的物品个数
}
if(place[0]<0&&!lr||place[0]&&!rl) cout<<1<<endl; //如果对于第一个物品,不存在第三种情况,那么只有第一个物品自身能与之接触
else cout<<lr+rl+1<<endl; //否则就是 与第一个物品向对的物品数量+与跟在第一个物品之后的数量+物品本身
return 0;
}
AcWing 1212. 地宫取宝
/*
本题给出n*m的矩阵,每个格子上面存在一个有价值的物品
从左上角出发到右下角,只能向下或者向右走,当已拿取的物品价值全部小于当前所走到的格子的物品价值时,当前物品才能被拿取,当然也可以不拿
且要求走到右下角时所拿取的物品个数恰好是k,求所有的方案数
*/
#include <iostream>
using namespace std;
const int N=60,M=15,mod=1000000007;
int n,m,k;
int w[N][N];
int f[N][N][M][M];
//数组f[i][j][u][v]数组为动态规划数组,[i,j]表示i行j列,u表示已拿取物品个数,v表示最后一个拿取的物品价值,也就是所拿取的物品的最大价值
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>w[i][j]; //输入每个物品的价值
w[i][j]++; //价值范围是0~12,为方便表示未拿取物品的情况,故对所有价值加1
}
}
f[1][1][1][w[1][1]]=1;
f[1][1][0][0]=1;
//左上角存在两种情况,拿取或者不拿
for(int i=1;i<=n;i++) //枚举行
{
for(int j=1;j<=m;j++) //枚举列
{
if(i==1&&j==1) continue; //左上角的情况已经预处理
for(int u=0;u<=k;u++) //枚举所有走到(i,j)时的已拿取个数
{
for(int v=0;v<=13;v++) //枚举所有已拿取的物品最大价值
{
/*
对于f[i][j][u][v]由四个状态转移过来
1.当从上方转移,且未拿取(i,j)上的物品,此时由f[i-1][j][u][v]转移而来
2.当从左方转移,且未拿取(i,j)上的物品,此时由f[i][j-1][u][v]转移而来
3.当从上方转移,且拿取(i,j)上的物品,此时由f[i-1][j][u-1][0~v-1]转移而来
4.当从左方转移,且拿取(i,j)上的物品,此时由f[i][j-1][u-1][0~v-1]转移而来
*/
int &val=f[i][j][u][v];
val=(val+f[i-1][j][u][v])%mod; //当从上方转移,且未拿取(i,j)上的物品
val=(val+f[i][j-1][u][v])%mod; //当从左方转移,且未拿取(i,j)上的物品
if(u>0&&v==w[i][j])
{
//当拿取(i,j)上的物品时,此时u必须大于0,因为(i,j)的物品必定被拿取
//且最后一个拿取的的物品价值最大,也就是说此时v必须等于w[i][j]
for(int c=0;c<v;c++) //枚举所有价值小于v,且物品个数比u小1的情况
{
val=(val+f[i-1][j][u-1][c])%mod; //当从上方转移,且拿取(i,j)上的物品
val=(val+f[i][j-1][u-1][c])%mod; //当从左方转移,且拿取(i,j)上的物品
}
}
}
}
}
}
int res=0;
for(int i=0;i<=13;i++) res=(res+f[n][m][k][i])%mod; //枚举最大值的所有情况,求和即为所求
cout<<res<<endl;
return 0;
}