2018年第九届蓝桥杯B组 国赛

一、换零钞

题目描述

本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。

X 星球的钞票的面额只有:100 元,5 元,2 元,1 元,共 4 种。

小明去 X 星旅游,他手里只有 2 张 100 元的 X 星币,太不方便,恰好路过 X 星银行就去换零钱。

小明有点强迫症,他坚持要求 200 元换出的零钞中 2 元的张数刚好是 1 元的张数的 10 倍,剩下的当然都是 5 元面额的。

银行的工作人员有点为难,你能帮助算出:在满足小明要求的前提下,最少要换给他多少张钞票吗?(5 元,2 元,1 元面额的必须都有,不能是 0)

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 128M

思路:理清题意,要求最终换成5、2、1三种面额的,要求张数最少、每种钞票都至少有1张且满足2的张数是1的张数的10倍。直接枚举即可,可以直接枚举5元+贪心(5元越多总张数就越少,因为一张5元至少需要2+2+1三张钞票顶上)。

#include <iostream>
using namespace std;
int main()
{
  int n, m, k, res;
  for(n = 39; n >= 1; -- n)
  {
    res = 200 - n * 5;
    if(res % 21 == 0){
      cout << n + res / 21 * 11; 
      break;
    }
  }
  return 0;
}

二、激光样式

题目描述

本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。

X 星球的盛大节日为增加气氛,用 30 台机光器一字排开,向太空中打出光柱。

安装调试的时候才发现,不知什么原因,相邻的两台激光器不能同时打开! 国王很想知道,在目前这种 bug 存在的情况下,一共能打出多少种激光效果?

显然,如果只有 3 台机器,一共可以成 5 种样式,即: 全都关上(sorry, 此时无声胜有声,这也算一种);开一台,共 3 种;开两台,只 1 种。

30 台就不好算了,国王只好请你帮忙了。

要求输出一个整数,表示 3030 台激光器能形成的样式种数。

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 128M

思路:首先考虑问题的分解,要求30台等达成的效果总数。可以明确的是,这个总数中,一部分是第30台灯开着,另一部分是第30台灯关着,不存在其他情况。我们用dp[i][j]表示第i台灯,j=0表示关着,j=1表示开着。于是所求的答案便为:dp[30][0]+dp[30][1]。

考虑状态的转移:对于第i台灯来说,如果它不开,那么它之前一个开不开都无所谓,因此有:

dp[i][0]=dp[i-1][0]+dp[i-1][1]

如果它开,那么它之前一个必须得关闭,因此有:

dp[i][1]=dp[i-1][0]

#include <iostream>
using namespace std;

long long dp[50][2];

int main()
{
  dp[1][0] = dp[1][1] = 1;
  for(int i = 2; i <= 30; ++ i)
  {
    dp[i][0] = dp[i - 1][0] + dp[i - 1][1];
    dp[i][1] = dp[i - 1][0];
  }
  cout << dp[30][0] + dp[30][1];
  return 0;
}

三、格雷码

题目描述

本题为代码补全填空题,请将题目中给出的源代码补全,并复制到右侧代码框中,选择对应的编译语言(C/Java)后进行提交。若题目中给出的源代码语言不唯一,则只需选择其一进行补全提交即可。复制后需将源代码中填空部分的下划线删掉,填上你的答案。提交后若未能通过,除考虑填空部分出错外,还需注意是否因在复制后有改动非填空部分产生错误。

格雷码是以 n 位的二进制来表示数。与普通的二进制表示不同的是,它要求相邻两个数字只能有 1 个数位不同。首尾两个数字也要求只有 1 位之差。

有很多算法来生成格雷码。以下是较常见的一种:

从编码全 0 开始生成。

当产生第奇数个数时,只把当前数字最末位改变(0 变 1,1 变 0)。

当产生第偶数个数时,先找到最右边的一个 1,把它左边的数字改变

用这个规则产生的 4 位格雷码序列如下:

0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000

以下是实现代码,仔细分析其中逻辑,并填写划线部分缺少的代码。

源代码

C

#include <stdio.h>
void show(int a,int n)
{
    int i;
    int msk = 1;
    for(i=0; i<n-1; i++) msk = msk << 1;
    for(i=0; i<n; i++){
        printf((a & msk)? "1" : "0");
        msk = msk >> 1;
    }
    printf("\n");
} 

void f(int n)
{
    int i;
    int num = 1;
    for(i=0; i<n; i++) num = num<<1;
    
    int a = 0;
    for(i=0; i<num; i++){
        show(a,n);
        
        if(i%2==0){
            a = a ^ 1;
        }
        else{
            a = _________________________ ; //填空
        }
    }
}

int main()
{
    int a;
    scanf("%d",&a);
    f(a);
    return 0;
}

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 256M

思路:阅读代码可知本题已经写好了奇数的情况,只需要针对偶数情况补全即可。当产生第偶数个数时,先找到最右边的一个 1,把它左边的数字改变。注意这边说的是数字改变,是原0变1,原1变0,而不是直接变为0。本题需要知道两个知识:

1.求最低位的1对应的权值:a & (-a) 

2.数字改变指的是 对应位置上 数位与1进行抑或  1 ^ 1 = 0,0 ^ 1 = 1。

#include <stdio.h>
void show(int a,int n)
{
    int i;
    int msk = 1;
    for(i=0; i<n-1; i++) msk = msk << 1;
    for(i=0; i<n; i++){
        printf((a & msk)? "1" : "0");
        msk = msk >> 1;
    }
    printf("\n");
} 

void f(int n)
{
    int i;
    int num = 1;
    for(i=0; i<n; i++) num = num<<1;
    
    int a = 0;
    for(i=0; i<num; i++){
        show(a,n);
        
        if(i%2==0){
            a = a ^ 1;
        }
        else{
            a ^= (a&(-a)) << 1; //填空
        }
    }
}

int main()
{
    int a;
    scanf("%d",&a);
    f(a);
    return 0;
}

四、调手表

题目描述

小明买了块高端大气上档次的电子手表,他正准备调时间呢。

在 M78 星云,时间的计量单位和地球上不同,M78 星云的一个小时有 n 分钟。

大家都知道,手表只有一个按钮可以把当前的数加一。在调分钟的时候,如果当前显示的数是 0 ,那么按一下按钮就会变成 1,再按一次变成 2 。如果当前的数是 n−1,按一次后会变成 0。

作为强迫症患者,小明一定要把手表的时间调对。如果手表上的时间比当前时间多 1,则要按 n−1 次加一按钮才能调回正确时间。

小明想,如果手表可以再添加一个按钮,表示把当前的数加 kk 该多好啊......

他想知道,如果有了这个 +k 按钮,按照最优策略按键,从任意一个分钟数调到另外任意一个分钟数最多要按多少次。

注意,按 +k 按钮时,如果加 k 后数字超过 n−1,则会对 n 取模。

比如 n=10, k=6 的时候,假设当前时间是 0,连按 2 次 +k 按钮,则调为 2。

输入描述

一行两个整数 n,k (0<k<n≤10^5),意义如题。

输出描述

输出一行一个整数,表示按照最优策略按键,从一个时间调到另一个时间最多要按多少次。

输入输出样例

示例

输入

5 3

输出

2

样例解释

如果时间正确则按 0 次。否则要按的次数和操作系列之间的关系如下:

1:+1

2:+1, +1

3:+3

4:+3, +1

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 256M

思路:把每加一次当作走一步(不管是加k还是n),就可以把本题抽象为一个BFS解决最短路问题。

#include <iostream>
#include <queue>
using namespace std;

bool visited[100007];

typedef pair<int, int> Node;

int BFS(int s, int n, int k)
{
  int x, path = 0;
  queue<Node> que;
  Node node;
  que.push(Node{0, 0});
  visited[s] = true;
  while(!que.empty())
  {
    node = que.front();
    que.pop();
    x = node.first;
    path = node.second;
    
    
    if(!visited[(x + 1) % n])
    {
      visited[(x + 1) % n] = true;
      que.push(Node{(x + 1) % n, path + 1});
    }

    if(!visited[(x + k) % n])
    {
      visited[(x + k) % n] = true;
      que.push(Node{(x + k) % n, path + 1});
    }
  }
  return path;
}

int main()
{
  int n, k;
  cin >> n >> k;
  cout << BFS(0, n, k);
  return 0;
}

当然也是可以用动态规划来解,但是编码复杂度和直观性上并不如BFS。原因在于,对于i和j来说(i<j),是可以由j+m*k转移到i的,因此我们得遍历多遍直到无法再更新值。

#include <iostream>
#include <cstring>
using namespace std;

int dp[100007];

int main()
{
  int n, k, n_i, max_path = 0;
  bool is_update = true;
  cin >> n >> k;
  memset(dp, 0x3f, sizeof(dp));
  dp[0] = 0;
  while(is_update)
  {
    is_update = false;
    for(int i = 1; i < n; ++ i)
    {
      n_i = i - 1;
      if(dp[n_i] + 1 < dp[i])
      {
        dp[i] = dp[n_i] + 1;
        is_update = true;
      }
      n_i = ( i - k + n ) % n;
      if(dp[n_i] + 1 < dp[i])
      {
        dp[i] = dp[n_i] + 1;
        is_update = true;
      }
    }
  }
  for(int i = 1; i < n; ++ i)
  {
    max_path = max_path < dp[i] ? dp[i] : max_path;
  }
  cout << max_path;
  return 0;
}

五、搭积木

题目描述

小明对搭积木非常感兴趣。他的积木都是同样大小的正立方体。

在搭积木时,小明选取 mm 块积木作为地基,将他们在桌子上一字排开,中间不留空隙,并称其为第 0 层。

随后,小明可以在上面摆放第 1 层,第 2 层,......,最多摆放至第 n 层。摆放积木必须遵循三条规则:

规则 1:每块积木必须紧挨着放置在某一块积木的正上方,与其下一层的积木对齐;

规则 2:同一层中的积木必须连续摆放,中间不能留有空隙;

规则 3:小明不喜欢的位置不能放置积木。

其中,小明不喜欢的位置都被标在了图纸上。图纸共有 n 行,从下至上的每一行分别对应积木的第 1 层至第 nn 层。每一行都有 m 个字符,字符可能是 '.' 或 'X' ,其中 'X' 表示这个位置是小明不喜欢的。

现在,小明想要知道,共有多少种放置积木的方案。他找到了参加蓝桥杯的你来帮他计算这个答案。

由于这个答案可能很大,你只需要回答这个答案对 10^9+7 取模后的结果。

注意:地基上什么都不放,也算作是方案之一种。

输入描述

输入格式:

输入数据的第一行有两个正整数 n,m (n≤100,m≤100),表示图纸的大小。

随后 n 行,每行有 m 个字符,用来描述图纸。每个字符只可能是 '.' 或 'X' 。

输出描述

输出一个整数,表示答案对 10^9+7 取模后的结果。

输入输出样例

示例

输入

2 3
..X
.X.

输出

4

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 256M

思路:注意题目的条件,我们用dp[n][l][r]表示在第n层的[l,r]区间上堆放积木的方案数。

那么根据规则1:每块积木必须紧挨着放置在某一块积木的正上方,与其下一层的积木对齐;

可以知道,如果第n层的[l,r]区间要有积木,那么第n-1层的[l,r]区间必须有积木,也就是说,可以到达dp[n][l][r]的前一个状态dp[n-1][i][j]必须满足i<=l且j>=r。

根据规则 2:同一层中的积木必须连续摆放,中间不能留有空隙;

我们可以知道,dp[n][l][r]表示的是第m层[l,r]区间连续摆放积木,不存在[l,k]、[k+q,r]的这种离散着摆放积木的情况。

根据规则 3:小明不喜欢的位置不能放置积木;

若区间[l,r]中间有'x',则dp[n][l][r]=0。

于是我们可以得到状态转移方程:

                                         dp[n][l][r] = 0, m层l~r中有非法位置

                                        dp[n][l][r] = \sum_{i=1}^{l}\sum_{j=r}^{m}dp[n-1][i][j]

朴素DP:

#include <iostream>
using namespace std;

const int mod = 1e9+7;
const int MaxN = 110;

char space[105][105];
int dp[MaxN][MaxN][MaxN], preSum[MaxN][MaxN];

int main()
{
  int n, m, i, j, k, l, r;
  cin >> n >> m;
  for(i = 1; i <= n; ++ i)
  {
    cin >> space[i] + 1;
    for(j = 1; j <= m; ++ j)
    {
      preSum[n + 1 - i][j] = preSum[n + 1 - i][j - 1] + (space[i][j] == 'X');
    }
  } 
  
  dp[0][1][m] = 1;
  int ans = 1;
  
  for(k = 1; k <= n; ++ k)
  {
    for(l = 1; l <= m; ++ l)
    {
      for(r = l; r <= m; ++ r)
      {
        if(preSum[k][r] - preSum[k][l - 1] > 0)
        {
          dp[k][l][r] = 0;
        }
        else
        {
          for(i = 1; i <= l; ++ i)
          {
            for(j = r; j <= m; ++ j)
            {
              dp[k][l][r] = (dp[k][l][r] + dp[k - 1][i][j]) % mod;
            }
          }
        }
        ans = (ans + dp[k][l][r]) % mod;
      }
    }
  }

  cout << ans;
  return 0;
}

优化:

朴素做法最大的问题在于对[i,j]区间的枚举,我们需要一个更加高效的方式来计算这些不同的[i,j]区间对答案的贡献的和。

我们将需要计算的部分以坐标轴的形式进行表示:

  需要快速求这一部分的和,可以使用二维前缀和。

Area(x1,y1,x2,y2)=c[x2][y2]-c[x1-1][y2]-c[x2][y1-1]+c[x1-1][y1-1]

​​​​​​于是引入了二维前缀和优化后,我们的转移方程变为:

dp[n][L][R]=0

dp[n][L][R]=Area(1,R,L,M)

#include <iostream>
using namespace std;

const int mod = 1e9+7;
const int MaxN = 110;

char space[105][105];
long long dp[MaxN][MaxN][MaxN], c_x[MaxN][MaxN], c_dp[MaxN][MaxN];

void update_prefix_sum(int k, int m)
{
  for(int i = 1; i <= m; ++ i)
  {
    for(int j = 1; j <= m; ++ j)
    {
      c_dp[i][j] = (c_dp[i - 1][j] + c_dp[i][j - 1] - c_dp[i - 1][j - 1] + dp[k][i][j] + mod) % mod;
    }
  }
}

int get_prefix_sum(int x1, int y1, int x2, int y2)
{
  return (c_dp[x2][y2] - c_dp[x1 - 1][y2] - c_dp[x2][y1 - 1] + c_dp[x1 - 1][y1 - 1] + mod) % mod;
}

int main()
{
  int n, m, i, j, k, l, r;
  cin >> n >> m;
  for(i = 1; i <= n; ++ i)
  {
    cin >> space[i] + 1;
    for(j = 1; j <= m; ++ j)
    {
      c_x[n + 1 - i][j] = c_x[n + 1 - i][j - 1] + (space[i][j] == 'X');
    }
  } 
  
  dp[0][1][m] = 1;
  update_prefix_sum(0, m);

  int ans = 1;
  
  for(k = 1; k <= n; ++ k)
  {
    for(l = 1; l <= m; ++ l)
    {
      for(r = l; r <= m; ++ r)
      {
        if(c_x[k][r] - c_x[k][l - 1] > 0)
        {
          dp[k][l][r] = 0;
        }
        else
        {
          dp[k][l][r] = get_prefix_sum(1, r, l, m) % mod;
        }
        ans = (ans + dp[k][l][r]) % mod;
      }
    }
    update_prefix_sum(k, m);
  }

  cout << ans;
  return 0;
}

[注意]:

1.值得注意的一点是,本题要求求的方案并不要求最终一定要摞到第n层,我们可以摞到中间第i层,然后不再摞,也是一种方案,因此我们需要统计这些并没有摞满n层的方案。

2.初始化的时候,第0层的条件其实是1~m全部摆满,因此只需要dp[0][1][m]=1即可。

3.代码使用了前缀和的方法来判断[l,r]区间内是否有'X'。

六、矩阵求和

题目描述

经过重重笔试面试的考验,小明成功进入 Macrohard 公司工作。

今天小明的任务是填满这么一张表:

表有 n 行 n 列,行和列的编号都从 1 算起。

其中第 i 行第 j 个元素的值是 gcd(i, j) 的平方,gcd 表示最大公约数,以下是这个表的前四行的前四列:

1 1 1 1

1 4 1 4

1 1 9 1

1 4 1 16

小明突然冒出一个奇怪的想法,他想知道这张表中所有元素的和。

由于表过于庞大,他希望借助计算机的力量。

输入描述

一行一个正整数 n (n≤10^7) 意义见题。

输出描述:

一行一个数,表示所有元素的和。由于答案比较大,请输出模 10^9 + 7 后的结果。

输入输出样例

示例

输入

4

输出

48

运行限制

  • 最大运行时间:2s
  • 最大运行内存: 256M

思路:如果gcd(i,j)=d,那么必有gcd(i/d,j/d)=1,也就是说,对于每一个最大公因数d,我们只需要知道在[1,n/d]中有多少个相互互质的数对(注意gcd(1,1)=1),就可以知道d^2最终会出现多少次。(当然需要注意i-j与j-i的gcd是 ,所以需要乘2)。

#include <iostream>
using namespace std;

const int MaxN = 1e7+5;
const long long mod = 1e9+7;

long long phi[MaxN], Prime[MaxN], cnt, cnt_d[MaxN];
bool isNotPrime[MaxN];

void eluer(int n)
{
  int i, j;
  for(i = 2; i <= n; ++ i)
  {
    if(!isNotPrime[i])//是素数
    {
      phi[i] = i - 1;
      Prime[cnt++] = i;
    }
    for(j = 0; j < cnt && Prime[j] * i <= n; ++ j)
    {
      isNotPrime[Prime[j] * i] = true;
      if(i % Prime[j] == 0)
      {
        phi[i * Prime[j]] = Prime[j] * phi[i];
        break;
      }
      else // Prime[j]是素数  i%Prime[j]!=0 因此只能有 gcd(i, Prime[j]) = 1
      {
        phi[i * Prime[j]] = phi[i] * phi[Prime[j]];
      }
    }
  }
  cnt_d[1] = phi[1] = 1;
  for(i = 2; i <= n; ++ i)
  {
  	cnt_d[i] = cnt_d[i - 1] + phi[i] * 2;
  }
}

int main()
{
  int n, i;
  long long ans;
  eluer(1e7);
  while(cin >> n)
  {
    ans = 0;
    for(i = 1; i <= n; ++ i)
    {
      ans = (ans + (cnt_d[n / i] * i) % mod * i % mod ) % mod;
    }
    cout << ans % mod << endl;
  }
  return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值