蓝桥杯真题训练第七届——补充了两种全排列的模板和博弈问题

一、第六题 寒假作业

题目

现在小学的数学题目也不是那么好玩的。看看这个寒假作业:

□ + □ = □

□ - □ = □

□ × □ = □

□ ÷ □ = □

每个方块代表1~13中的某一个数字,但不能重复。比如:

6 + 7 = 13

9 - 8 = 1

3 * 4 = 12

10 / 2 = 5

以及:

7 + 6 = 13

9 - 8 = 1

3 * 4 = 12

10 / 2 = 5

就算两种解法(加法,乘法交换律后算不同的方案)。你一共找到了多少种方案?请填写表示方案数目的整数。注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。

1.补充知识点

本题要补充全排列的两种方法:交换法和选择法

(1)交换法生成全排列模板:

static int []a =new int [N];//此数组为存储需要排列的全部元素

static void  genenrateP(int K){
	if (k==N)//由于数组可访问的最大下标为K-1.所以当到达K时,完成一次排列
	{
		if(check()){
			//输出要执行的内容 
		}
		return ;
	}
	//这个位置还可以设置剪枝
	
	for( int i=k;i<N;i++){
		//交换a[i]与a[k]元素
		{ int t=a[i];a[i]=a[k];a[k]=a[i];} 
		//进行下一个元素的交换 
		generateP(K+1);
		//回溯
		{ int t=a[i];a[i]=a[k];a[k]=a[i];} 
	}
	
}

(2) 选择法生成全排列模板

①如果需要排列的数字不是一串连续的数字时

static int []a =new int [N];//此数组为存储需要排列的全部元素
static boolean []marked=new boolean [N]//此为判断a数组哪个元素是被访问过的,如果a中的第i个元素被访问过时,marked[i]=true,因此,一开始要初始化marked为false
static int path[]=new int [N];//该数组为存取每一次的全排列,即在a中选择一个元素后存入path 

static void generateP(int k){
	if (k==N){
		if(check()){
			//执行需要的内容 
		}
		return;
	}
	

	
	for(int i=0;i<N;i++)//这是与交换法最大的区别,他要从头开始遍历需要排列的元素 
	{
		//在这个位置还可以设置提前剪枝 ,如果选取的元素里面有重复元素的话
	    if (i > 0 && a[i] == a[i - 1] && !vis[i - 1]) continue;//现在准备选取的元素和上一个元素相同,但是上一个元素还没被使用
	    //以后都加上这一句话进行剪枝
		if(!marked[i])//如果没被选择 
		 {
			marked[i]=true;
			path[k]=a[i];
			generateP(k+1);
			//注意最后要回溯
			marked[i]=false;
		}
	}
} 

②如果需要排列的数字是一串连续的数字时


//如果,需要排列的数组时一串连续的数字,比如说是1~13,那么还可以这样写,即省去a[i]数组

static boolean []marked=new boolean [14]//此为判断哪个元素是被访问过的,如果i被访问过时,marked[i]=true,因此,一开始要初始化marked为false
static int path[]=new int [14];//该数组为存取每一次的全排列,即在a中选择一个元素后存入path 

static void generateP(int k){
	if (k==14){
		if(check()){
			//执行需要的内容 
		}
		return;
	}
	

	
	for(int i=1;i<14;i++)//这是与交换法最大的区别,他要从头开始遍历需要排列的元素 
	{
	     //在这个位置还可以设置提前剪枝 a[]数组存的是选取元素,如果选取的元素里面有重复元素的话
	    if (i > 0 && a[i] == a[i - 1] && !vis[i - 1]) continue;//现在准备选取的元素和上一个元素相同,但是上一个元素还没被使用
	    //以后都加上这一句话进行剪枝
		if(!marked[i])//如果没被选择 
		 {
			marked[i]=true;
			path[k]=i;
			generateP(k+1);
			//注意最后要回溯
			marked[i]=false;
		}
	}
} 

2.本题解题思路

直接用交换法全排列1~13,然后判断最后结果的前12位是否满足公式,可以利用提前剪枝加快效率,注意利用交换法剪枝的时候要把交换回来才能够continue

3.本题代码

public class _06寒假作业 {
  static int a[]={1,2,3,4,5,6,7,8,9,10,11,12,13};
  static int ans;
  static boolean check(){
    if(a[0]+a[1]==a[2]&&
        a[3]-a[4]==a[5]&&
        a[6]*a[7]==a[8]&&
        a[9]%a[10]==0&&
        a[9]/a[10]==a[11])
      return true;
    return false;
  }
  static void f(int k){
    if(k==13){
      if(check()){
        System.out.printf("%d+%d=%d  %d-%d=%d  %d*%d=%d  %d/%d=%d\n",
            a[0],a[1],a[2],
            a[3],a[4],a[5],
            a[6],a[7],a[8],
            a[9],a[10],a[11]);
        ans++;
      }
    }
    for (int i = k; i < 13; ++i) {
      {int t=a[i];a[i]=a[k];a[k]=t;}
//        提前排除,提升效率
      if((k==2&&a[0]+a[1]!=a[2]) || k==5&&a[3]-a[4]!=a[5]){
        //注意提前剪枝时要交换回来
        {int t=a[i];a[i]=a[k];a[k]=t;}
        continue;
      }
      f(k+1);
      {int t=a[i];a[i]=a[k];a[k]=t;}
    }
  }

  public static void main(String[] args) {
    f(0);
    System.out.println(ans);
  }
}

二、第七题 剪邮票

题目
有12张连在一起的12生肖的邮票。现在你要从中剪下5张来,要求必须是连着的(即两张邮票有一条公共边,仅仅连接一个角不算相连)。用如下表格表示12张邮票:
在这里插入图片描述
0表示没有被剪,1表示剪去,则如下表格为一个合格的剪取:
在这里插入图片描述
请填写表示方案数目的整数。注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。

1.本题涉及到的知识点

(1) 连通性检测

假如有一张图,然后将图用二维数组表示,这个图中,被剪掉的位置就设为1,然后要检测被剪掉的邮票有多少条连通分支,此时,这个的总体思路就是:对二维数组进行扫描,当遇到元素为1的时候,对它进行深搜,注意,此时深搜不是平常那样每次只朝着一个方向搜,而是四面八方都要搜,当有出现1的时候,就进行往下四面八方搜,等到搜完后,连通分支数就++。
连通性检测的模板如下:

  static int check(int g[][]) {
     //g为存储图的二维数组
    int cnt = 0;//连通块的数目
//    g上面就有5个格子被标记为1,然后用dfs做连通性检查,这几个标为1的属于几个连通块
    for (int i = 0; i < 3; ++i) {
      for (int j = 0; j < 4; ++j) {
        if (g[i][j] == 1) {
          dfs(g, i, j);
          cnt++;
        }
      }
    }
    return cnt;
  }
  //下面为连通性检测专属的dfs
    static void dfs(int g[][], int i, int j) {
    //注意,要把该位置标为0,防止重复
    g[i][j] = 0;
    //四个方向都要去检测,满足条件则继续搜,注意不能超过边界
    if (i - 1 >= 0 && g[i - 1][j] == 1) dfs(g, i - 1, j);
    if (i + 1 <= 2 && g[i + 1][j] == 1) dfs(g, i + 1, j);
    if (j - 1 >= 0 && g[i][j - 1] == 1) dfs(g, i, j - 1);
    if (j + 1 <= 3 && g[i][j + 1] == 1) dfs(g, i, j + 1);
  }

(2) 将一维数组的位置映射到二维数组的位置

方法:遍历一遍二维数组进行赋值,假设二维数组a有X行Y列,它的一个元素[i][j],就相当于一维数组的path[iX+J]元素,所以赋值的时候直接 a[i][j]=path[iX+J]就行,下面为映射的模板:

  static void Change(int path[]) {
    int g[][]=new int[3][4];
//    将某个排列映射到二维矩阵上
    for (int i = 0; i < 3; ++i) {
      for (int j = 0; j < 4; ++j) {
        if (path[i * 4 + j] == 1) g[i][j] = 1;
        else g[i][j] = 0;
        //上述的if else可以直接变成
        //g[i][j]=path[i*4+j],一个道理的,因为path里面除了0,就是1
      }
    }

2.本题的解题思路:

一开始是想着对每邮票的每一个点进行一个深搜,当到达边界且步数为5时,就count++,但是在以往的例题就说过,如果要用这种思路的时候,要重点考虑是否会出现T字形的情况,如果出现了的话,那么这种方法就y不适合,要转换思路。

这题可以转化为,在12张邮票里面,随便抽五个数,然后判断他们的连通分分支是不是只有一条。

对于12张邮票里面随便抽五个数我们可以转化为,在一个长度为12的一维数组里面,全排列5个1,有1的就是要抽的,这便是随便抽五个数的全部情况,注意为了加快效率,要用选择法进行全排列,并且要加上那句话进行剪枝

所以这道题一共划分为3个函数:生成全排列函数,连通性检查函数,深搜函数

3.代码如下

public class _07剪邮票 {

  static int a[] = {0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1};//它的每个排列代表着12选5的一个方案
  static int ans;

  static void dfs(int g[][], int i, int j) {
    g[i][j] = 0;
    if (i - 1 >= 0 && g[i - 1][j] == 1) dfs(g, i - 1, j);
    if (i + 1 <= 2 && g[i + 1][j] == 1) dfs(g, i + 1, j);
    if (j - 1 >= 0 && g[i][j - 1] == 1) dfs(g, i, j - 1);
    if (j + 1 <= 3 && g[i][j + 1] == 1) dfs(g, i, j + 1);
  }

  static boolean check(int path[]) {
    int g[][]=new int[3][4];
//    将某个排列映射到二维矩阵上
    for (int i = 0; i < 3; ++i) {
      for (int j = 0; j < 4; ++j) {
        if (path[i * 4 + j] == 1) g[i][j] = 1;
        else g[i][j] = 0;
      }
    }
    int cnt = 0;//连通块的数目
//    g上面就有5个格子被标记为1,现在才用dfs做连通性检查,要求只有一个连通块
    for (int i = 0; i < 3; ++i) {
      for (int j = 0; j < 4; ++j) {
        if (g[i][j] == 1) {
          dfs(g, i, j);
          cnt++;
        }
      }
    }
    return cnt == 1;
  }

  static boolean vis[]=new boolean[12];

  static void f(int k, int path[]) {
    if (k == 12) {
      if (check(path)) {
        ans++;
      }
    }
    for (int i = 0; i < 12; ++i) {
      if (i > 0 && a[i] == a[i - 1] && !vis[i - 1]) continue;//现在准备选取的元素和上一个元素相同,但是上一个元素还没被使用

      if (!vis[i]) {//没有被用过的元素可以抓入到path
        vis[i] = true;//标记为已访问
        path[k] = a[i];//将a[i]填入到path[k]中
        f(k + 1, path);//递归
        vis[i] = false;//回溯
      }

    }
  }

  public static void main(String[] args) {
    int path[]=new int[12];
    f(0,path);
    System.out.println(ans);
  }
}

三、第八题 取球博弈

题目
两个人玩取球的游戏。
一共有N个球,每人轮流取球,每次可取集合{n1,n2,n3}中的任何一个数目。
如果无法继续取球,则游戏结束。
此时,持有奇数个球的一方获胜。
如果两人都是奇数,则为平局。

假设双方都采用最聪明的取法,
第一个取球的人一定能赢吗?
试编程解决这个问题。

输入格式:
第一行3个正整数n1 n2 n3,空格分开,表示每次可取的数目 (0<n1,n2,n3<100)
第二行5个正整数x1 x2 … x5,空格分开,表示5局的初始球数(0<xi<1000)

输出格式:
一行5个字符,空格分开。分别表示每局先取球的人能否获胜。
能获胜则输出+,
次之,如有办法逼平对手,输出0,
无论如何都会输,则输出-

例如,输入:
1 2 3
1 2 3 4 5

程序应该输出:

  • 0 + 0 -

再例如,输入:
1 4 5
10 11 12 13 15

程序应该输出:
0 - 0 + +

再例如,输入:
2 3 5
7 8 9 10 11

程序应该输出:

  • 0 0 0 0

资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 3000ms

请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。

所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
注意:不要使用package语句。不要使用jdk1.7及以上版本的特性。
注意:主类的名字必须是:Main,否则按无效代码处理。

1.解题思路
这是典型的博弈问题,我们一开始就可以用递归的思想,即球的总数是变化的,两人手中的球数也是变化的,将这三个参数作为递归传参,每次都去试探选择三种取法的情况,进行深搜判断,递归得出口就是球的总数小于最小的取球数,注意由于是博弈问题,所以需要进行下一次搜索时,需要变化you 和me

2.本题的注意点
要注意分析递归过程中,有哪些参数是在变化的,变化的参数即要传到递归函数中

3.暴力代码如下

public class a{
  Scanner sc=new Scanner (System.in);
  int []n=int [3];
  n[0]=sc.nextInt();
  n[1]=sc.nextInt();
  n[2]=sc.nextInt();
  //对数组进行排序,从小到大
  Array.sort(n);
  for(int i=0;i<5;i++){
    int num=sc.nextInt();
    char res=f(num,0,0);//返回博弈结果
    System.out.print(res+" ");
  }
  System.out.println();
}

  /**
   * 参数代表着当前取球人面临的局面
   * @param num 球的总数
   * @param me 我方持有的数目
   * @param you 对手持有的数目
   * @return
   */

private static char f(int num,int me,int you){

  if(num<n[0])//不够数,递归出口
  {
    if((me&1)==1&&(you&1)==0) //如果我手中为奇数,对手手中为偶数
    {
      return '+';
    }
    else if ((me&1)==0&&(you&1)==1) return '-';
    else return '0';
  }

  boolean ping =false;

  //对于每一种取球情况进行深搜
  for(int i=0;i<3;i++){
    if(num>n[i]) {
      char res = f(num - n[i], you, me);//这个是博弈问题的关键,要互换
      if (res == '-') return '+';
      if (res == '0') ping = true;
    }
  }
  if(ping) return '0';
  else return '-';
}

4.记忆化递归代码如下

public class _08取球博弈 {

  private static int[] n;

  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    n = new int[3];
    for (int i = 0; i < 3; i++) {
      n[i] = sc.nextInt();
    }
    Arrays.sort(n);//排序
    for (int i = 0; i < 5; i++) {
      int num = sc.nextInt();
      char res = f(num, 0, 0);
      System.out.print(res + " ");
    }
    System.out.println();
  }
static char[][][]cache = new char[1000][2][2];
  /**
   * 参数代表着当前取球人面临的局面
   * @param num 球的总数
   * @param me 我方持有的数目-->我方数目的奇偶性
   * @param you 对手持有的数目-对方数目的奇偶性
   * @return
   */
  private static char f(int num, int me, int you) {
    if (num<n[0])//不够取
    {
      if ((me&1)==1&&(you&1)==0)return '+';
      else if ((me&1)==0&&(you&1)==1)return '-';
      else return '0';
    }
    if (cache[num][me][you]!='\0')return cache[num][me][you];
    boolean ping = false;
    for (int i = 0; i < 3; i++) {
      if (num >= n[i]) {
        char res = f(num - n[i], you, (n[i]&1)==0?me:(1-me));//注意此处,传递me和you的奇偶性
        if (res == '-')
        {
          cache[num][me][you]='+';
          return '+';
        }
        if (res == '0')
          ping = true;
      }
    }
    //如果能走到第这行,说明不存在对手输的情况,那么是否存在平的情况
    if (ping)
    {
      cache[num][me][you]='0';
      return '0';
    }
    else
    {
      cache[num][me][you]='-';
      return '-';
    }
  }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值