蓝桥杯 C/C++ 大学A组 2016年省赛真题之“方格填数”(全排列)

本文为武汉大学郑未《蓝桥杯历届真题解析》学习笔记。
添加链接描述

方格填数

(第三题)
有如下十个方格:
在这里插入图片描述
**要求:**连续的两个数不能相邻。
(左右、上下、对角都算相邻)
求有多少种填法。

**考点:**全排列

  • 如14年的扑克排序
    A A 2 2 3 3 4 4,一共4对扑克牌,请把它们排成一列。
    要求:两个A中间有1张牌,两个2中间有2张牌,两个3之间有3张牌,两个4之间有4张牌。
    请写出符合要求的排列中,字典序最小的那个。
  • 如2015年的手链样式
    全排列+特殊去重

解法一:
使用递归来对0-9这10个数进行全排列,再检查排列的结果是否符合题目要求。

/*方格填数*/
#include 
#include 
using namespace std;

int a[10]={0,1,2,3,4,5,6,7,8,9};
int ans=0;
int main(int argc,const char *argv[])
{
    void f(int k);
    f(0);
    cout<<ans<<endl;
    return 0;
}

bool check()
{
    if(abs(a[0]-a[1])==1 ||
       abs(a[0]-a[3])==1 ||
       abs(a[0]-a[4])==1 ||
       abs(a[0]-a[5])==1 ||
       abs(a[1]-a[2])==1 ||
       abs(a[1]-a[4])==1 ||
       abs(a[1]-a[5])==1 ||
       abs(a[1]-a[6])==1 ||
       abs(a[2]-a[5])==1 ||
       abs(a[2]-a[6])==1 ||
       abs(a[3]-a[4])==1 ||
       abs(a[3]-a[7])==1 ||
       abs(a[3]-a[8])==1 ||
       abs(a[4]-a[5])==1 ||
       abs(a[4]-a[7])==1 ||
       abs(a[4]-a[8])==1 ||
       abs(a[4]-a[9])==1 ||
       abs(a[5]-a[6])==1 ||
       abs(a[5]-a[8])==1 ||
       abs(a[5]-a[9])==1 ||
       abs(a[6]-a[9])==1 ||
       abs(a[7]-a[8])==1 ||
       abs(a[8]-a[9])==1)
       return false;
    else
        return true;
}

/*用递归来进行全排列*/
/*考虑第k个位置,一般从0开始*/
void f(int k)
{
    if(k==10){
        bool b=check();
        if(b)
            ans++;
        return;
    }

    for(int i=k;i<10;++i){
        /*尝试将位置i与位置k交换,以此确定k位的值*/
        {
            int t=a[i];
            a[i]=a[k];
            a[k]=t;
        }
        f(k+1);
        /*回溯*/
        {
            int t=a[i];
            a[i]=a[k];
            a[k]=t;
        }
    }
}

解法二:
用一个如下图所示的5*6的二维数组中间部分的空间来存储0-9的全排列,数组中的其他元素填-10(如图中填-1的那些单元格),这样对所有0-9的元素,都可用一个双重的for循环来检测其处于中心位置的九宫格中,是否存在数值与它是相连的。
在这里插入图片描述

#include 
#include 
using namespace std;
int a[5][6];
int vis[10];
int ans;

int main()
{
    void init();
    void f(int x,int y);
    init();
    f(1,2);
    cout<<ans<<endl;
    return 0;
}

/*检查是否合法*/
bool check(int i,int j)
{
    int x,y;
    for(x=i-1;x<i+2;x++)
        for(y=j-1;y<j+2;y++)
            if(abs(a[i][j]-a[x][y])==1)
                return false;
    return true;
}

/*进行全排列*/
void f(int x,int y)
{
    if(x==3 && y==4){
        ans++;
        return;
    }
    int i;
    for(i=0;i<10;i++){
        if(vis[i]==0){ //没有被用过
            a[x][y]=i; //填数
            if(!check(x,y)){ //检查(在过程中即进行)不合法,恢复并continue
                a[x][y]=-10;
                continue;
            }
            vis[i]=1; //标记为已访问
            /*递归*/
            if(y==4)
                f(x+1,1); //如果y值已到边界,换行
            else
                f(x,y+1); //继续填右侧的格子
            {vis[i]=0;  //回溯
            a[x][y]=-10;}
        }
    }
}

/*初始化*/
void init()
{
    int i,j;
    for(i=0;i<5;i++)
        for(j=0;j<6;j++)
            a[i][j]=-10;
}

解法三:
调用头文件中的next_permutation()函数来进行全排列操作,沿用解法二的check()操作。

/*方格填数*/
#include 
#include 
#include 
#include 
#include 
using namespace std;

int a[10]={0,1,2,3,4,5,6,7,8,9};
int b[5][6];
int ans=0;
int main()
{
    bool check(int i,int j);
    void init();
    //memset(b,-1,sizeof(b));  //为何不能初始化为-2,-10等
    int i,j,k,Count=0;
    bool flag=true;

    for(i=0;i<5;i++)  //对 b[][]中所有的元素进行初始化
        for(j=0;j<6;j++)
            b[i][j]=-10;

    while(next_permutation(a,a+10)){ //调用next_permutation函数对0-9进行全排列
        i=1;
        j=2;
        for(k=0;k<10;k++){
            b[i][j]=a[k]; //将全排列后的元素填入b[][]中特定空间
            if(!check(i,j)){
                init();  //此时,一定要将已填入值的元素初始化为-10
                flag=false;
                break;
            }
            j++;
            if(j==5){
                i++;
                j=1;
            }
        }
        if(flag)
            Count++;
        else
            flag=true;
    }
    cout<<Count<<endl;
    return 0;
}

/*对已填入数的元素进行初始化*/
void init()
{
    int x,y;
    for(x=1;x<=3;x++)
        for(y=1;y<=4;y++)
            b[x][y]=-10;
}

/*检查新填入的数据是否满足题目需求*/
bool check(int i,int j)
{
    int x,y;
    for(x=i-1;x<i+2;x++){
        for(y=j-1;y<j+2;y++)
            if(abs(b[x][y]-b[i][j])==1)
                return false;
    }
    return true;
}

注意点:
(1)每次check()数据不合格后,需要对所有已填入0-9的数组元素进行初始化,否则下次check()时,上次填入的元素会影响判断。我最开始没想到这个问题,最后输出的ans值为0。
(2)用next_permutation()函数对数组元素进行全排列时,需先将该数组中的元素按从小到大的顺序排好序,因为该函数默认当数组中元素降序排序时,全排列操作完成,退出执行。

next_permutation()用法示例:

#include 
#include 
using namespace std;

int a[3]={0,1,2};
int main()
{
    int i;
    while(next_permutation(a,(a+3))){
        for(i=0;i<3;i++)
            cout<<a[i]<<' ';
        cout<<endl;
    }
    return 0;
}

输出结果:
在这里插入图片描述

寒假作业

(第六题)
现在小学的数学题目也不是那么好玩的。
看看这个寒假作业:
□ + □ = □
□ - □ = □
□ × □ = □
□ ÷ □ = □
每个方块代表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-13个数进行全排列

解法一:
用递归进行全排列

#include 
#include 
using namespace std;

int a[13]={1,2,3,4,5,6,7,8,9,10,11,12,13};
int ans=0;

bool 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;
}

/*用递归实现全排列*/
void f(int k)
{
    if(k==13){
        if(check())
            ans++;
    }
    int i,t;
    for(i=k;i<13;++i){
        {t=a[i];a[i]=a[k];a[k]=t;} //交换
        f(k+1);
        {t=a[i];a[i]=a[k];a[k]=t;} //回溯
    }
}

int main()
{
    f(0);
    cout<<ans<<endl;
    return 0;
}

由于是对13个数进行全排列,程序运行较慢:
在这里插入图片描述
可增加一些检验条件,及时终止部分不合要求的排列,可对f函数稍作更改:

/*用递归实现全排列*/
void f(int k)
{
    if(k==13){
        if(check())
            ans++;
    }
    int i,t;
    for(i=k;i<13;++i){
        {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]) ||(k==8 && a[6]*a[7]!=a[8])){
            {t=a[i];a[i]=a[k];a[k]=t;}  //如不合要求,撤销前一步的交换操作
            continue;
        }
        f(k+1);
        {t=a[i];a[i]=a[k];a[k]=t;} //回溯
    }
}

另外,在这道题上我尝试用next_permutation()算法来进行全排列,运行结果很慢很慢。
又分析了上面next_permutation()进行全排列的规律(如下图),来增加适当的检验条件,程序没运行出来。可能是我的程序本身有问题,暂时将这个失败的程序记在这里。
在这里插入图片描述

#include 
#include 
using namespace std;

int a[13]={1,2,3,4,5,6,7,8,9,10,11,12,13};

int main()
{
    int i,j,k,Count=0;

    while(next_permutation(a,(a+13))){
        /*如果a[0]==k排列不合题目要求,则开始a[0]==k+1的新一轮的排列*/
        if((a[0]+a[1]!=a[2]) || (a[3]-a[4]!=a[5]) ||(a[6]*a[7]!=a[8])){
            k=a[0];
            k++;
            a[0]=k;
            j=1;
            for(i=1;i<13;i++){
                if(i!=k)
                    a[j++]=i;
            }
        }

        if(a[2]==a[0]+a[1] && a[5]==a[3]-a[4] && a[8]==a[6]*a[7] && a[8]%a[9]==0 && a[11]==a[8]/a[9])
            Count++;
    }

    cout<<Count<<endl;
    return 0;
}

剪格子

如【图1.jpg】, 有12张连在一起的12生肖的邮票。
现在你要从中剪下5张来,要求必须是连着的。
(仅仅连接一个角不算相连)
比如,下面两张图中,粉红色所示部分就是合格的剪取。
蓝桥杯 C/C++ 大学A组 2016年省赛真题之“方格填数”(全排列)_第1张图片
蓝桥杯 C/C++ 大学A组 2016年省赛真题之“方格填数”(全排列)_第2张图片
请你计算,一共有多少种不同的剪取方法。

此题与13年剪格子有相似之处,但那个题的限制条件是格子数值之和为总和的一半,此题限制只能是5个格子。

注意点:
单纯的dfs()无法解决T字型连通方案(不能同时往水平和垂直两个方向走)。
本题的解决方法是,找出任意5个格子,判断是否连通。
(1)从12个格子里面选出5个(全排列,但是是对重复的元素进行全排列)。
(2)在二维数组中检测连通性(经典问题),可用dfs快速解决。

解法一:
在对12个元素进行全排列后再检测其是否符合题目要求,运行较慢,运行时间为1300多秒。

/*剪格子:在排列之后再做重复性判断*/
#include 
#include 
#include 
using namespace std;

/*它的每一个排列代表12选5的一个方案,排好后将1所在的下标转为二维数组的下标,再检测连通性*/
int a[]={0,0,0,0,0,0,0,1,1,1,1,1};
int ans=0;

void dfs(int g[3][4],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<=3 && 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<=4 && g[i][j+1]==1) dfs(g,i,j+1);
}

/*连通性检测*/
bool check()
{
    int g[3][4]; //代表图
    int i,j;
    /*将某个排列映射到二维矩阵上*/
    for(i=0;i<3;++i){
        for(j=0;j<4;++j){
            if(a[i*4+j]==1)
                g[i][j]=1;
            else
                g[i][j]=0;
        }
    }
    int cnt=0; //连通块数目
/*g中有5个格子被标记为1后,用dfs做连通性检验,要求只有一个连通块*/
    for(i=0;i<3;++i)
        for(j=0;j<4;++j){
            if(g[i][j]==1){
                dfs(g,i,j);
                cnt++;
            }
        }
        return(cnt==1);
}

set<string> s1; //引入set,将数组变成字符串,去除重复元素

void a2s(string &s)
{
    int i;
    for(i=0;i<12;++i){
        s.insert(s.end(),a[i]+'0');
    }
}

/*因为是对12个0或1进行全排列,有大量的重复结果,所以需要检验这样的结果之前是否存在过*/
bool isExist()
{
    string a_str;
    a2s(a_str);
    /*如果没找着a_str,就将其存在s1中,并返回false*/
    if(s1.find(a_str)==s1.end()){
        s1.insert(a_str);
        return false;
    }
    else
        return true;
}

void f(int k)
{
    int i,t;
    if(!isExist() && check()==12){
        ans++;
    }

    for(i=k;i<12;i++){
        {t=a[i];a[i]=a[k];a[k]=t;}
        f(k+1);
        {t=a[i];a[i]=a[k];a[k]=t;}
    }
}

int main()
{
    f(0);
    cout<<ans<<endl;
    /*string s;
    a2s(s);
    cout<
    return 0;
}

下图是对代码中涉及的深度遍历的一个图示:
从一个非0的格子起,对它进行dfs,如果能走通(有相连为1者),将该格子置为0(标记访问过)
蓝桥杯 C/C++ 大学A组 2016年省赛真题之“方格填数”(全排列)_第3张图片
解法二:
抓取法
从12个元素中去抓取5个来构成path,是全排列的另一种方案,运行结果较快。

#include 
using namespace std;

int a[]={0,0,0,0,0,0,0,1,1,1,1,1};
int ans=0;

void dfs(int g[3][4],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);
}
/*连通性检测*/
bool check(int path[12])
{
    int g[3][4]; //代表图
    int i,j;
    /*将某个排列映射到二维矩阵上*/
    for(i=0;i<3;++i){
        for(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(i=0;i<3;++i)
        for(j=0;j<4;++j){
            if(g[i][j]==1){
                dfs(g,i,j);
                cnt++;
            }
        }
        return(cnt==1);
}


bool vis[12];
void f(int k,int path[12])
{
    int i;
    if(k==12){
        if(check(path))
            ans++;
    }
    for(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]; //填入到path
            f(k+1,path);  //递归
            vis[i]=false; //回溯
        }
    }
}

int main()
{
    int path[12];
    f(0,path);
    cout<<ans<<endl;
    return 0;
}

解法三:
使用next_permutation()进行全排列。

#include 
#include 
using namespace std;

int ans;

void dfs(int g[3][4],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);
}
/*连通性检测*/
bool check(int path[12])
{
    int g[3][4]; //代表图
    int i,j;
    /*将某个排列映射到二维矩阵上*/
    for(i=0;i<3;++i){
        for(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(i=0;i<3;++i)
        for(j=0;j<4;++j){
            if(g[i][j]==1){
                dfs(g,i,j);
                cnt++;
            }
        }
        return(cnt==1);
}

int main()
{
    int pre[]={0,0,0,0,0,0,0,1,1,1,1,1};
    do{
        if(check(pre))
            ans++;
        }while(next_permutation(pre,(pre+12)));
    cout<<ans<<endl;
    return 0;
}

你可能感兴趣的:(蓝桥杯)