蓝桥杯部分题目详解
1.X 进制减法
题目详解
这题的难度还是比较大的,一方面是涉及一个数学公式,另一方面是看你敢不敢贪心。
首先这道题目与我们常规的进制问题不太同,它是每一位都有一个进制,每一位到这个位之后再进一,举个简单的例子。
贪心做法
这道题目的贪心做法还是比较好想的,如果我想要A-B最小,那么其实我们只要保证AB的每一位对应的进制最小。
举个例子:
我们把每一位进制转化成要乘以的数字设为X,那么我们的每一位就是某个数字乘以X,那么A-B每一位设成Y,那么A-B的每一位的数字转十进制相减都是X*Y,那么Y是固定的X越大,A-B就越大,X越小,A-B就越小。然后就是如果Y为负值的时候,其实问题也不大,因为A>B那么其实A的高位的影响其实要比低位可能的负的影响要大。所以高位的差越小整体越小。
解题思路
那么我们要想让每一位最小,其实就是去取A,B两个对应位的数字的最大值加一就是最小的进制。然后我们把这个最小的进制设为pi差不多可以得到这个等式。
我们可以算一下时间复杂度是肯定会超时的,两次循环加上这么大的数据量,所以要引用一个数学公式。
然后通过这个公式化简我们的式子可以得到:
(di表示AB对应位的差)
代码实现
#include <iostream>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long ll;
const int N =1e6, MOD=1000000007;
int numa[N],numb[N];
int cnt=0,wei=0;
int main()
{
int n,ma,mb,ans=0;
cin>>n>>ma;
for(int i=0;i<ma;i++)cin>>numa[ma-i-1];
cin>>mb;
for(int i=0;i<mb;i++)cin>>numb[mb-i-1];
int m=max(ma,mb);
ans=numa[m-1]-numb[m-1];
for(int i=max(ma,mb)-2;i>=0;i--){
ans=(ans*(ll)max({2,numa[i]+1,numb[i]+1})+numa[i]-numb[i])% MOD;
}
cout<<ans;
return 0;
}
2.统计子矩阵
暴力做法
这道题目的暴力做法就是直接用二位数组去存每个位置的前缀和,然后再枚举两个点,去算两个点的框出来区域的和,但是很明显这样的话,是四次循环也就是ON的四次方时间复杂度加上这个数据,基本上只能过一定的数据,这里也是只过了百分之70的数据。
#include <iostream>
using namespace std;
const int N=550;
long long cnt=0;
int map[N][N];
long long sum[N][N];
long long n,m,k;
//暴力一下看看//枚举两个位置,左上角和右下角,然后求出前缀和
void pd(int x,int y,int a,int b)
{
long long g=sum[a][b]-sum[a][y-1]-sum[x-1][b]+sum[x-1][y-1];
if(g<=k)cnt++;
}
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>map[i][j];
//前缀和
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
sum[i][j]=sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1]+map[i][j];
//枚举
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=i;k<=n;k++){
for(int l=j;l<=m;l++){
pd(i,j,k,l);
}
}
}
}
`cout<<cnt;
return 0;
}
当然在比赛场上性价比还是可以的。
优化二维转一维问题+双指针
这道题目的另一种做法就是去在每个数组中存一个列的区间,然后你用双指针做法,找到符合的区间,把符合的区间加进去。
存列元素的元素和
那么其实这个还是比较简单的,你把新一列的新一行的元素加上这一列上一行的元素的和。用一个小递归去实现一下。
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>sum[i][j];
sum[i][j]+=sum[i-1][j];//加上对应的列的上面的元素和
}
区间内符合的区间数量个数
我们可以先模拟一下。假如我们的数列要求区间和小于等于6
那么这个区间很明显就只有1个,那么就是right-left+1;
我们发现这个时候新增的其实就是2 ,21,其实这个时候就能看出来我们新增的区间就是以当前位置去向左扩展的连续的区间,也就是我们这段区间的元素的个数:right-left+1
那么到第三个时候就更明显了,3, 32,321区间。
而当我们的区间和大于6的时候我们只需要把左边的区间向右移动,直到符合要求。
枚举区间
枚举区间的时候我们只需要枚举两行(也就是夹着的区间),然后用双指针去枚举每一列就好了。
代码实现
#include <iostream>
using namespace std;
const int N=550;
long long cnt=0,ans;
long long sum[N][N];//一列的值
int main()
{
int n,m;
long long k;
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>sum[i][j];
sum[i][j]+=sum[i-1][j];//sum[i][j]表示第i行第j列的一列的元素和
}
}
//双指针去判
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
long long s=0;
for(int l=1,r=1;r<=m;r++){
s+=sum[j][r]-sum[i-1][r];
while(s>k){
s-=sum[j][l]-sum[i-1][l];
l++;
}
ans+=r-l+1;//从最右边位置向外扩展,增加的数量
}
}
}
cout<<ans;
return 0;
}
3.积木画
解法:二维状态dp解法
二维dp的解题方法对于之前刷过类似题目的人来说比较简单,大致方法如下。
dp问题首先还是要定义一下状态转移式:我们可以先定义一下dp[i][j]表示已经操作完第i-1列现在第i列是j状态的所有方案.
,
对于某一列的位置,其实我们都只有两个位置,每个位置都是要么占用要么没占用,组合起来刚好是四个数字,那么我们可以用四个数字去表示
然后我们可以用一个数组g来表示是否可以从这个状态转移到另一个状态(不弄这个存的话会比较麻烦)因为我们表示的是一列的状态,所以转移到另一列的时候必须保证这一列已经满了。(同时这个状态转移要满足我们定义的dp即摆放的是i列导致i+1列)
举例
为了方便大家理解举一下i-1为空的例子。(另外三种情况大家可以自己推一下)
0状态到0状态
这里我们发现是可以的。
0状态到1状态
这里也是可以的把对应的方块放上去就行了。
0状态到2状态
也是可以找到对应的方块的。
0状态到3状态
这里可以用两个长条横着放就行。
然后我们每个状态都弄一下最后得到我们的g数组
int g[4][4]{
{1,1,1,1},
{0,0,1,1},
{0,1,0,1},
{1,0,0,0},
}
代码实现
#include <iostream>
using namespace std;
const int N=1e7+10000;
const int mod= 1000000007;
int dp[N][4];
int g[4][4]{
{1,1,1,1},
{0,0,1,1},
{0,1,0,1},
{1,0,0,0},
};
int main()
{
int n;
cin>>n;
dp[1][0]=1;
for(int i=1;i<=n;i++)
{
0 for(int j=0;j<4;j++)
{
for(int k=0;k<4;k++)
{
dp[i+1][j]=(dp[i][k]*g[k][j]+dp[i+1][j])%mod;
}
}
}
cout<<dp[n+1][0];
return 0;
}
滚动数组优化代码
#include <iostream>
#include<cstring>
using namespace std;
const int N=1e7+10000;
const int mod= 1000000007;
int dp[2][4];
int g[4][4]{
{1,1,1,1},
{0,0,1,1},
{0,1,0,1},
{1,0,0,0},
};
int main()
{
int n;
cin>>n;
dp[1][0]=1;
for(int i=1;i<=n;i++)
{
memset(dp[i+1&1],0,sizeof(dp[0]));
for(int j=0;j<4;j++)
{
for(int k=0;k<4;k++)
{
dp[i+1&1][j]=(dp[i&1][k]*g[k][j]+dp[i+1&1][j])%mod;
}
}
}
cout<<dp[n+1&1][0];
return 0;
}
4.李白打酒加强版
这道题目比较容易直接想到的做法是dfs暴力,dp其实有点不太好想,暴力的话就是可以过百分之40数据,也还可以。
#include <iostream>
using namespace std;
//暴力一下dfs
int n,m,cnt=0;
void dfs(int d,int h,int j)
{
if(d>n||h>m||j<0)return;
if(d==n&&h==m-1&&j==1){
cnt++;
cnt%=1000000007;
return;
}
dfs(d+1,h,j*2);
dfs(d,h+1,j-1);
}
int main()
{
cin>>n>>m;
dfs(0,0,2);
cout<<cnt;
return 0;
}
dp做法
首先还是定义状态转移式:
d
p
[
i
]
[
j
]
[
n
]
表示遇见第
i
个店第
j
个花酒壶里的酒味
n
的所有情况
dp[i][j][n]表示遇见第i个店第j个花酒壶里的酒味n的所有情况
dp[i][j][n]表示遇见第i个店第j个花酒壶里的酒味n的所有情况
然后来分析可以从哪些情况过来,一种前面遇见店,另一种则是遇见花
代码实现
#include <iostream>
using namespace std;
const int N=110;
const int MOD=1000000007;
int dp[N][N][N];
int main()
{
int n,m;
cin>>n>>m;
dp[0][0][2]=1;
for(int i=0;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
for(int k=0;k<=m;k++)//酒的取值收到花的影响
{
if(j)dp[i][j][k]=(dp[i][j-1][k+1]+dp[i][j][k])%MOD;
if(i&&k%2==0) dp[i][j][k]=(dp[i-1][j][k/2]+dp[i][j][k])%MOD;
}
}
}
cout<<dp[n][m-1][1]<<endl;
return 0;
}
其实想出来之后发现也不是很难,只是确实不太容易直接想到三维dp;
5 .砍竹子
解题思路
这道题目我们可以先计算一下对于最大的数字需要经过多少次才能变为1,这里大家自己操作下应该是6;
然后其实我们每个数字都是经过一定操作变成1的;
我们可以用一段表示1次操作,那么其实我们这道题如果不考虑最小次数就是对每个数依次进行。而如果要考虑最小次数,那么就要考虑到的其实是数据可能相等的时候,那么其实我们发现其实只有我们经过的操作数一样才有可能相等。
而如果我们到达1的操作数都一样那就不可能相等。用这个性质,我们去存一下每一层的数据,先把不考虑连续的次数存下来,然后再把连续的数字减掉
代码实现
#include <iostream>
#include<cmath>
using namespace std;
typedef long long LL;
const int N=200010,M=7;
int n,m;
LL f[N][M];
int main()
{
cin>>n;
int stk[M],res=0;//用栈去存一下的操作数字,方便翻转
for(int i=0;i<n;i++)
{
LL x;
cin>>x;
int top=0;
while(x>1)stk[++top]=x,x=sqrtl(x/2+1);//每次操作后的数字存下来
res+=top;
m=max(m,top);
for(int j=0,k=top;k;j++,k--) f[i][j]=stk[k];//存要多少次操作到1
}
for(int i=0;i<m;i++){
for(int j=1;j<n;j++){
if(f[j][i]&&f[j][i]==f[j-1][i]) res--;//相等的删掉
}
}
cout<<res;
return 0;
}