蓝桥杯国赛--四阶幻方清晰易懂(dfs+剪枝,stl)

四阶幻方--蓝桥杯国赛(dfs全排列+剪枝)

题目描述

标题:四阶幻方

把1~16的数字填入4*4的方格中,使得行、列以及两个对角线的和都相等,满足这样的特征时称为:四阶幻方。

四阶幻方可能有很多方案。如果固定左上角为1,请计算一共有多少种方案。 比如: 1 2 15 16 12 14 3 5 13 7 10 4 8 11 6 9

以及: 1 12 13 8 2 14 7 11 15 3 10 6 16 5 4 9

就可以算为两种不同的方案。

请提交左上角固定为1时的所有方案数字,不要填写任何多余内容或说明文字。

看上去这是道国赛的题,但是说实话这道题和省赛的题目可以说是如出一辙,都是全排列加check一下,有必要的地方要提前剪枝,否则可能会超时。不信的话可以百度一下“ 寒假作业 ”这道题 ,具体是哪一年的省赛我也记不太清了。所以,当我一看到本题,就有种熟悉的感觉。废话不多说,给铁子们看看代码:

  1. 这是通过dfs的方法来生成全排列,可以中途剪枝,优化代码。

 
  //用stl的next_permutation()会超时,还是得用dfs生成全排列加提前剪枝降低时间复杂度
 #include<iostream>
 #include<algorithm>
 #include<cstring>
 using namespace std;
 #define key 34 //key是四阶幻方的每行(列,对角线)上数的和,这个可以通过简单的数学运算得出
 
 bool vis[17];//用来判断1-16个数是否有被访问到
 int elem[17];//用来存放全排列的整个序列,就不用二位数组来存放了,
 long ans;
 
 void dfs(int x)//x表示第几个位置上,而不是代表某个数字
 {
  //剪枝
  if(x==5)//已经生成了四个数,可以先做判断,如果不符合条件,就可以提前退出了,节省时间,加快效率
  {
  int sum=0;
  for(int i=1;i<5;i++)
  {
  sum+=elem[i];
  }
  if(sum!=key)
  return;
  }
  if(x==9)//又生成四个数,又可以判断一波
  {
  int sum=0;
  for(int i=5;i<9;i++)
  {
  sum+=elem[i];
  }
  if(sum!=key)
  return;
  }
  if(x==13)//判断
  {
  int sum=0;
  for(int i=9;i<13;i++)
  {
  sum+=elem[i];
  }
  if(sum!=key)
  return;
  }
  if(x==17)//最后来个总的判断,如果满足条件,计数器+1
  {
  if(elem[1]+elem[2]+elem[3]+elem[4]==key&&
  elem[5]+elem[6]+elem[7]+elem[8]==key&&
  elem[9]+elem[10]+elem[11]+elem[12]==key&&
  elem[13]+elem[14]+elem[15]+elem[16]==key&&
  elem[1]+elem[5]+elem[9]+elem[13]==key&&
  elem[2]+elem[6]+elem[10]+elem[14]==key&&
  elem[3]+elem[7]+elem[11]+elem[15]==key&&
  elem[4]+elem[8]+elem[12]+elem[16]==key&&
  elem[1]+elem[6]+elem[11]+elem[16]==key&&
  elem[4]+elem[7]+elem[10]+elem[13]==key
  )
  ans++;
  }
  for(int i=2;i<=16;i++)//生成全排列了
  {
  if(!vis[i])//如果当前数字没有被访问,就使用该数字
  {
  vis[i]=1;
  elem[x]=i;//第x个位置上放入数字i
  dfs(x+1);//接着就放第x+1个位置了
  vis[i]=0;//回溯时要对vis标记复原
  }
  }
  return;
 }
 
 int main()
 {
  memset(vis,0,sizeof(vis));
  elem[1]=1;//默认1号位置固定死了
  vis[1]=1;
  dfs(2);//一号位置已经放好,就从第二个位置开始dfs
  cout<<ans<<endl;
  return 0;
 }
  1. 这是使用c++标准模板库STL中的next_permutation()函数,这种会超时。

 #include<iostream>
 using namespace std;
 #include<algorithm>
 #include<cstring>
 #include<set>//判重,看各条线上的元素和是否相同
 int map[4][4];
 long ans=0;
 int main()
 {
  memset(map,0,sizeof(map));
  //固定1,就是15个数的全排列了
  string s ="cdefghijklmnopq";//用c来代表2
  map[0][0] = 1;
  do
  {
  for(int i=0;i<s.size();i++)
  {
  map[(i+1)/4][(i+1)%4] = s[i]-'a';
  }//将字符串转换为二位数组,便于check
  int sum3=0,sum4=0;
  set<int>ss;
  for(int i=0;i<4;i++)//控制行
  {
  int sum1=0,sum2=0;
  for(int j=0;j<4;j++)//控制列
  {
  sum1+=map[i][j];
  sum2+=map[j][i];
  if(i==j)
  {
  sum3+=map[i][j];
  }
  if(i+j==3)
  {
  sum4+=map[i][j];
  }
  }
  ss.insert(sum1);
  ss.insert(sum2);
  if(sum1!=sum2)
  {
  break;
  }
  }
  ss.insert(sum3);
  ss.insert(sum4);
  if(s.size()==1)
  ans++;
  }while(next_permutation(s.begin(),s.end()));
  cout<<ans<<endl;
  return 0;
 }

最后,我们可以做个简要的分析,15的阶乘是1,307,674,368,000,也就是最外层循环要执行1,307,674,368,000次,假设计算机1秒可以处理10^8条指令,那么这样下来我们需要13076秒,接近4个小时。注意,这还只是最外层循环,所以整个程序耗费的时间远不止此,归根结底的原因就是,next_permutation()产生的每个全排列我们都去做了判断,并且在全排列产生的过程种我们不能及时地判断不满足并做出提前终止的反应。 那么第一种方法就做到了,但是第一种方法相较于第二种会难想一点,这就是好懂和效率的矛盾吧^_^.

 

最后的运行截图:

原文地址:https://www.cnblogs.com/yuanshixiao/p/13903755.html