回溯法扩展(n位逐位整除数、n皇后问题、素数圈)

n位逐位整除数

任务描述

本关任务:掌握回溯法算法思想,并能利用回溯法算法思想解决n位逐位整除数问题。

n位逐位整除数(简称整除数):从其高位开始,高1位能被整数1整除(显然),高2位能被整数2整除,…,整个n位能被整数n整除。给定整数n,求所有的n位整除数的个数。

例如,整数102450就是一个6位整除数。

相关知识

为了完成本关任务,你需要掌握:1.回溯法的基本思想,2.回溯法的基本步骤,3.回溯法的算法框架,4.整除数的求解思路。

回溯法的基本思想

有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。

回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。

回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果包含,进入该子树,继续按深度优先策略搜索。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯。

回溯法的基本步骤

根据回溯法的基本思想,可以得到回溯法的基本步骤如下:

  1. 针对所给问题,定义问题的解空间;
  2. 确定易于搜索的解空间结构;
  3. 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

其中,两个常用的剪枝函数为:

  1. 约数函数:在扩展结点处减去不满足约束的子树 ;
  2. 限界函数:减去得不到最优解的子树。

对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间:

  • 问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,x**n)的形式;
  • 显约束:对分量x**i的取值限定;
  • 隐约束:为满足问题的解而对不同分量之间施加的约束(通常用于剪枝)。
回溯法的算法框架

回溯法对解空间树作深度优先搜索,因此,在一般情况下用递归方法实现回溯法。具体算法框架及其伪代码如下:

void backt\frack (int t)//按深度优先从t层推进到第t+1层
{
    if (t > n){
        output (x); // 到达叶子结点,将计算或结果输出
    }
    else {
        for (int i = f(n,t); i <= g(n,t); i ++ ){// 遍历结点t的所有子结点
            x[t] = h[i];
            if (constraint (t) && bound (t)){
                backt\frack (t + 1);//如果不满足剪枝条件继续遍历
            }
            //回溯到该层的其他结点,然后继续搜索解空间
        }
    }
}
整除数的求解思路

n位整除数的解法可以暴力枚举每一位数字,然后通过试除法进行验证,显然,这样的方法复杂度非常高,而且非常多的无效组合。运用回溯法算法可以很好的避免无效的组合,提高检索效率。根据回溯法的基本步骤,可得到如下回溯解法:

  1. 针对n位整除数问题,它的解空间为:第1位可能的数字为1,2,…9,第2至第n位可能的数字都为0,1,…9,因此共9×10(n−1)个可能的解;
  2. 解空间结构:用数组a[1,2,…n]表示整除数问题的一个解,其中a[1]!=0,求解时按索引t=1开始,依次递归得到数组a
  3. 以深度优先的方式搜索解空间,若当前的整数a[1,2,…t]不能整除整数t,则剪枝。

例如n=6时,回溯法的过程如下图所示。t=1,候选数字为0,1,2,…9,其中0是不符合整数第1位不为0的规则,设置当前的数字为a[t=1]=1;递归,t=t+1=2,数字0,2,4,6,8与a[1]所组合的整数a[1,2]是能被t=2整除的,设置当前的数字为a[t=2]=0;以此类推,直到t=7时,超过了指定的长度n=6,当前递归搜索结束,整除数个数加1。然后回溯到t=6时其它的合法数字,继续递归搜索。再然后回溯到t=5时其他的合法数字,继续递归搜索。以此类推,完成整个解空间的搜索。

回溯法扩展(n位逐位整除数、n皇后问题、素数圈)_第1张图片

编程要求

本关的编程任务是补全右侧代码片段backt\frackBeginEnd中间的代码,具体要求如下:

  • backt\frack中,根据回溯法算法的思想,遍历和统计n位逐位整除数的个数。 其中,参数数组a记录满足条件的整除数(整除数长度n超过int64位存储范围,所以用整型数组模拟存储),参数t(初始为1)表示当前整除数长度(t<=n),参数n表示待查询的整除数长度,取地址引用的参数sum表示整除数的个数。

测试说明

平台将自动编译补全后的代码,并生成若干组测试数据,接着根据程序的输出判断程序是否正确。

以下是平台的测试样例:

测试输入:6 预期输出:1200

思路:这里采用的是大数的运算,单纯的用int值会超界。

n位整除数的解释:例如例题给的整数102450是一个6位整除数。怎么是6位呢?1能被1整除,10能被2整除,102能被3整除以此类推,102450也正好能被6整除。也就是说n位整除数其前k位的高k位数都能被k整除。

那么我们就可以有剪枝操作,以2为例,10可以,11不可以被2整除,12可以,13不可以。那么我们就可以直接减掉11,13,也就是不会出现11和13开头的数字.

不考虑大数,以整型运算进行思考。int a,a表示当前存放的数,当前的整除数为t,如果a先更新:a=a*10+i;i的范围为0-9(t>1),如果a%t==0,那么就进行回溯。当满足t>n时回溯结束。

#include "stdio.h"

int n,sum=0,a=0;

void backtrack(int t){
  if(t>n){
    sum++;
    return ;
  }
  else
  {
    if(t==1){
      for(int i=1;i<10;i++){//一个数字都能被1整除
          a=a*10+i;
          
          backtrack(t+1);
          a=a/10;
      }
    }
    else{
    for(int i=0;i<10;i++){
      a=a*10+i;//更新a的值
      if(a%t!=0){
        a=a/10;
        continue;
      }
     
      backtrack(t+1);
      a=a/10;//a变回原来的数
    } 
    }
    return ;
  }


}

int main(int argc, char const *argv[])
{
  scanf("%d",&n);

  backtrack(1);
  printf("%d",sum);
  return 0;
}

考虑大数,用数组来存放

//
//  main.cpp
//  step1
//
//  Created by ljpc on 2018/12/8.
//  Copyright © 2018年 ljpc. All rights reserved.
//

#include 
#include 
#include 

using namespace std;


void backtrack(int *a, int t, int n, int &sum)
{
    // 请在这里补充代码,完成本关任务
    /********* Begin *********/
    int r=0;//这里定义一个r
    for(int i=1;i<t;i++){//r来进行计算除法
      r=r*10+a[i];//向我们学过的除法计算一样,从第一位数字开始往后累加进行除法运算
      r=r%(t-1);//r=0表示能够整除,这里a存放的数是t-1层的数,所以要和t-1进行整除

    }
    if(t==1){//第一层比较特殊,第一层表示是只有一个数,只能是个位1-9
      for(int i=1;i<10;i++){
          a[t]=i;
          backtrack(a,t+1,n,sum);
          a[t]=0;
      }
    }
    else{
      if(r!=0){//没能整除,剪枝操作
        return ;
      }
      if(t>n){//到达临界值
        if(r==0){
          sum++;
        }
        return ;
      }
      if(t>1){
        for(int i=0;i<10;i++){//1层之后的数字为0-9,例如10,200,3000可以包括0
          a[t]=i;
          backtrack(a,t+1,n,sum);
          a[t]=0;
        }
      }
    }
 
    /********* End *********/
}


int main(int argc, const char * argv[]) {
    
    int a[101];
    int n;
    scanf("%d", &n);
    
    int sum = 0;
    backtrack(a, 1, n, sum);

    printf("%d\n", sum);
    
    return 0;
}


非递归实现皇后问题

任务描述

本关任务:在n×n格的棋盘上放置彼此不受攻击的 n 个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。用非递归算法解决该问题。

下图是一个 8 个皇后的例子,8 个皇后彼此不受攻击。

回溯法扩展(n位逐位整除数、n皇后问题、素数圈)_第2张图片

编程要求

请在右侧编辑器Begin-End处补充代码,完成本关任务。

测试说明

平台会对你编写的代码进行测试,比对你输出的数值与实际正确数值,只有所有数据全部计算正确才能通过测试:

测试输入:4(皇后的数目)

预期输出:

  *  Q  *  *
  *  *  *  Q
  Q  *  *  *
  *  *  Q  *
  
  *  *  Q  *
  Q  *  *  *
  *  *  *  Q
  *  Q  *  *
4皇后问题共有2种摆放方案

开始你的任务吧,祝你成功!

思路:遍历n行皇后,第i行用数组x[i]来表示列数,如果满足皇后条件(见上一篇文章),那么久把行数增加,同时x[n]初始为0,每次一进入循环就增加x[i]的值,依次遍历列数。从1-n来存放数据。如果遍历完当前行所有的列,那就退出遍历列的循环,并把当前列x[i]=0,回退到上一行继续遍历行。当回退的t=0时所有数据遍历完。

#include
#include
int x[100]={0};//从1-n里放皇后,x[i]=1表示在那里放皇后
int n;

int judge(int k){
    int y,z;
  for(int i=1;i<k;i++){
        y=i-k;
        z=x[i]-x[k];
        if(y<0) y=-y;
        if(z<0) z=-z;
        if(y==z||x[i]==x[k]) return 0;
  }
  return 1;
}

int main(int argc, char const *argv[])
{
  
  scanf("%d",&n);
  int t=1,sum=0;
  while (t>0)//t小于1时结束
  {
    while(x[t]<n){//t从1到n放皇后
      x[t]++;//在第t行开始放皇后,x[t]的范围为0-n,0表示没有放,1-n表示放置的列数,初始为0
      if(t==n&&judge(t)){//在t行放置过皇后之后进行判断,如果满足放置到了n行并且位置无误
          sum++;
          for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
              if(x[i]==j) printf("  Q");
              else printf("  *");
            }
            printf("\n");
          }
          printf("\n");
      } 
      else if(judge(t)){//部分解,进入下一行
        t++;
      }
    }
    //回到上一个点,并把当前t行的列位置置为0
    x[t]=0;
    t--;

  }
  
  printf("%d皇后问题共有%d种摆放方案",n,sum);
  return 0;
}

递归算法解决皇后问题

任务描述

本关任务:在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。用递归算法解决该问题。

编程要求

请在右侧编辑器Begin-End处补充代码,完成本关任务。

测试说明

平台会对你编写的代码进行测试,比对你输出的数值与实际正确数值,只有所有数据全部计算正确才能通过测试:

测试输入:4(皇后的数目)

预期输出:

 *  Q  *  *
  *  *  *  Q
  Q  *  *  *
  *  *  Q  *
  *  *  Q  *
  Q  *  *  *
  *  *  *  Q
  *  Q  *  *
4皇后问题共有2种摆放方案

开始你的任务吧,祝你成功!

#include
#include
int x[100]={0};//从1-n里放皇后,x[i]=1表示在那里放皇后
int n;
int sum=0;

int judge(int k){
    int y,z;
  for(int i=1;i<k;i++){
        y=i-k;
        z=x[i]-x[k];
        if(y<0) y=-y;
        if(z<0) z=-z;
        if(y==z||x[i]==x[k]) return 0;
  }
  return 1;
}
void backtrack(int t){
    while (t>0)//t小于1时结束
  {
    while(x[t]<n){//t从1到n放皇后
      x[t]++;//在第t行开始放皇后,x[t]的范围为0-n,0表示没有放,1-n表示放置的列数,初始为0
      if(t==n&&judge(t)){//在t行放置过皇后之后进行判断,如果满足放置到了n行并且位置无误
          sum++;
          for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
              if(x[i]==j) printf("  Q");
              else printf("  *");
            }
            printf("\n");
          }
          printf("\n");
      } 
      else if(judge(t)){//部分解,进入下一行
        t++;
      }
    }
    //回到上一个点,并把当前t行的列位置置为0
    x[t]=0;
    t--;

  }
}
int main(int argc, char const *argv[])
{
  
  scanf("%d",&n);
  
  backtrack(1);
  
  printf("%d后问题共有%d种摆放方案",n,sum);
  return 0;
}

素数圈

任务描述

本关任务:把从 1 到 n 这 n 个数摆成一个环,要求相邻的两个数的和是一个素数。

编程要求

请在右侧编辑器Begin-End处补充代码,完成本关任务,输出格式请参考测试集。

测试说明

平台会对你编写的代码进行测试,比对你输出的数值与实际正确数值,只有所有数据全部计算正确才能通过测试:

测试输入:20

预期输出:围成的圈是:1 2 3 4 7 6 5 8 9 10 13 16 15 14 17 20 11 12 19 18


开始你的任务吧,祝你成功!

思路:写一个素数判断方法。注意:素数圈中的数字不可重复出现,用flag标记重复出现的数字。

重点注意:回溯遍历的结果不止一个,我们只要一个,所以当出现第一个结果时退出。exit(0);

#include 
#include 

/**********  Begin  **********/
int n;
int x[100]={0};
int flag[100]={0};

int sushu(int k){//判断该数字是否为素数
    for(int i=2;i<=(int)sqrt(k);i++)
    {
        if(k%i==0) return 0;
    }
    return 1;
}

void backtrack(int t){//t代表第t个数
    
    if(t>n){//t为n+1,输出素数圈  
           if(sushu(x[n]+x[1])){          
            printf("围成的圈是:");
            for(int i=1;i<n;i++) printf("%d ",x[i]);
            printf("%d",x[n]);
            
            exit(0);
            }
            return ;                
    }
    else{
        
        for(int i=2;i<=n;i++){
            
            if(sushu(i+x[t-1])&&flag[i]==0){
                flag[i]=1;
                x[t]=i;
                backtrack(t+1);
                flag[i]=0;
            }
        }
    }    
    return ;
}
int main()
{
    scanf("%d",&n);
    x[1]=1;//环的第一个元素位置固定
    flag[1]=1;
    backtrack(2);
   
    return 0;
}
/**********  End  **********/

你可能感兴趣的:(算法)