借助计算机快速解决数论问题.docx
《借助计算机快速解决数论问题.docx》由会员分享,可在线阅读,更多相关《借助计算机快速解决数论问题.docx(16页珍藏版)》请在冰豆网上搜索。
借助计算机快速解决数论问题
借助计算机快速解决数论问题
一、解数独
引言与问题背景:
下面是一道信息学奥赛NOIP的提高组复赛题
靶形数独的方格同普通数独一样,在9格宽×9格高的大九宫格中有9个3格宽×3格高的小九宫格(用粗黑色线隔开的)。
在这个大九宫格中,有一些数字是已知的,根据这些数字,利用逻辑推理,在其他的空格上填入1到9的数字。
每个数字在每个小九宫格内不能重复出现,每个数字在每行、每列也不能重复出现。
但靶形数独有一点和普通数独不同,即每一个方格都有一个分值,而且如同一个靶子一样,离中心越近则分值越高。
上图具体的分值分布是:
最里面一格为10分,外面的一圈每个格子为9分,再外面一圈每个格子为8分,外面一圈每个格子为7分,最外面一圈每个格子为6分,如上图所示。
比赛的要求是:
每个人必须完成一个给定的数独(每个给定数独可能有不同的填法),而且要争取更高的总分数。
而这个总分数即每个方格上的分值和完成这个数独时填在相应格上的数字的乘积的总和。
分析问题
对于这种解数独,我认为应该用穷举算法(如果有更好的算法请告诉我)。
也就是每次在格内由1~9列出来,然后进行检测是否可以放置,然后递归到下一个格子。
就这一题而言,我开始想到了贪心算法,就是每个格子由9~1递减,但是格子的顺序是由中间开始螺旋向外伸开,这样只要完成数独一次,就可以达到目的。
但是,这样所使用的寻路算法在实际编译过程中占用了大量时间,于是贪心算法失败,改用穷举法。
使用穷举,就免了寻路这一步,但是要做很多次才能达到目的。
说一下程序的理念。
首先由第一格开始,每一格用for循环1~9,如果其中有一位数可以填入这个格子,那就向右递归到下一个格子。
重复刚才的动作,直到最后一个格子都完成了,那么整个数独就完成了。
经过测试,这样的算法快很多。
下面将展示完整C语言程序代码。
完整代码如下经运行通过
#include"stdio.h"
intnum[9][9]={0};
intfindx(intx,inty)
{
if(y==8)returnx+1;
elsereturnx;
}
intfindy(intx,inty)
{
if(y==8)return0;
elsereturny+1;
}
voidprint()
{
intx,y;
for(x=0;x<9;x++)
{
for(y=0;y<9;y++)
{
printf("%-2d",num[y][x]);
}
printf("\n");
}
printf("\n");
}
intcando(intk,intx,inty)
{
inti,j;
for(i=0;i<9;i++)
{
if(k==num[i][y]||k==num[x][i])return0;
}
for(i=0;i<3;i++)
{
for(j=0;j<3;j++)
{
if(num[x/3*3+i][y/3*3+j]==k)return0;
}
}
return1;
}
intwork(intn,intx,inty)
{
inti;
if(num[x][y]!
=0)
{
if(n==80){print();if(i==1)return0;
}
work(n+1,findx(x,y),findy(x,y));
return0;
}
for(i=9;i>0;i--)
{
if(cando(i,x,y)==1)
{
num[x][y]=i;
if(n==80){print();if(i==1)return0;
}
work(n+1,findx(x,y),findy(x,y));
}
}
num[x][y]=0;
}
intmain()
{
//freopen("1.txt","r",stdin);
//freopen("2.txt","w",stdout);
intx=4,y=4,i,t;
for(i=0;i<9;i++)
{
for(t=0;t<9;t++)
{
scanf("%d",&num[t][i]);
}
}
work(0,0,0);
}
注意:
以上代码可以实现输出全部可行的数独阵。
例如输入:
900006700
000100089
005037020
000070000
050000000
000000961
570800090
002000630
800009004
将会输出:
923486715
764152389
185937426
219674853
658391247
347528961
576843192
492715638
831269574
(因为只有一个可行的数独阵,所以只输出一个)
注意:
这个程序并不是解答上面那道题目,只是解决了数独算法而已。
但是要解决上面拿到题目,只需要在程序中略改几处即可(由于比较简单,这里就不写了,留给读者思考)。
二、解九宫
引言与问题背景:
九宫格相传为唐代书法家欧阳询所创制。
欧阳询书“九成宫醴泉铭”,严谨峭劲,法度完备,是其晚年的得意之作,向来被学者赞誉为“正书第一”,仿习者甚多。
为方便习字者练字,欧阳询根据汉字字形的特点,创制了“九宫格”的界格形式。
九宫格,中间一小格称为“中宫”,上面三格称为“上三宫”,下面三格称为“下三宫”,左右两格分别称为“左宫”和“右宫”,用以在练字时对照碑帖的字形和点画安排适当的部位,或用作字体的缩小与放大。
至元代,书法家陈绎曾进一步发展了九宫结构。
他在《翰林要诀》中说,为适应临字时点画疏密、各有停分、界画匀布的要求,改横竖三宫为横竖九宫,成九九八十一宫,这样更便于精确临摹。
到了清朝,书法家蒋骥在其所著《读书法论》中,又根据汉字字体结构特点和形体的不同,删繁就简,变九宫格为四种形式:
一、把原九九八十一宫,横竖各去掉三宫,变为六六三十六宫。
二、把三十六宫的左、右两行十二宫去掉,成二十四宫,适宜书写长方体字;或把上、下两行十二宫去掉,亦成二十四宫,宜于习练扁平体字。
三、将三十六宫形变成双回字形,用以写方体字。
四、将三十六宫形变成田字形,并在上二宫从同一顶点各画条对角线,形成一个人字,用以书写盖似人字形的字。
这样似乎还不简便,通过长期实践,后人又改进了两种简明实用的习字格:
一为田字格,一为米字格。
田字格是在方框中画一“十”字,分成四格,按此格习字,便于安排字的间架结构、重心和笔画的斜正疏密。
米字格是在田字格的基础上再画两条对角线,形如米字,此种方格类同蛛网,习字时便于判断全字和各单笔的位置。
上述九宫格和变九宫格,今天初学字者仍然使用。
不仅适于学习毛笔字,也适于学习硬笔书法。
待到基本上掌握了字的点画、结构、气势等等,即可脱离“九宫格”等界格,纵笔自由驰骋了。
“九宫格”还指诗钟的分咏格。
上下两句诗意绝不相类,而字面又紧密配对,内容与形式错杂交互,有如古之明堂九宫,故名。
清莫友堂《平龙草堂诗话》卷四引清孙抎《余墨偶谈续集》:
“分举不类两物,撰成二语,名九宫格。
如走马灯对蟹菊云:
‘投足火中犹善走,寄人篱下也横行’;‘夕阳门外探消息,寒食墦间乐倡随。
’之类是也。
名以九宫者,盖取其错杂交互之意。
”
九宫格为数独的“前身”,最早起源于中国。
数千年前,我们的祖先就发明了洛书,其特点较之现在的数独更为复杂,要求纵向、横向、斜向上的三个数字之和等于15,而非简单的九个数字不能重复。
儒家典籍《易经》中的“九宫图”也源于此,故称“洛书九宫图”。
而“九宫”之名也因《易经》在中华文化发展史上的重要地位而保存、沿用至今。
完整代码如下经运行通过
/*九宫格源码(有砖请拍)*/
#include
intnumber[65][4];
intselect[65];
intarray[3][4];
intcount;
intselecount;
intlarray[1][65];
intlcount[1];
main()
{
inti,k,flag,cc=0,i1,i3;
printf("Therearemagicsquareswithinvertableprimesasfollow:
\n");
for(i=123;i<=987;i++)
{
if(num(i))
{
number[count][0]=i;
process(count++);
select[selecount++]=count-1;
}
}
larray[0][lcount[0]++]=number[0][0]/10;
for(i=1;i {
if(larray[0][lcount[0]-1]!
=number[i][0]/10)
larray[0][lcount[0]++]=number[i][0]/10;
}
for(i1=0;i1 {
array[0][0]=select[i1];
copy_num(0);
for(array[1][0]=0;array[1][0] {
copy_num
(1);
if(!
comp_num
(2))
continue;
for(i3=0;i3 {
array[2][0]=select[i3];
copy_num
(2);
for(flag=1,i=1;flag&&i<=3;i++)
{
if(!
find1(i))
flag=0;
}
if(flag&&find2())
{
printf("No.%d\n",++cc);
p_array();
}
}
}
}
}
num(intnumber)
{
inti,j,k;
i=number/100;
j=number/10%10;
k=number%10;
if((i+j+k)==15&&i!
=j&&i!
=k&&j!
=k)
return1;
else
return0;
}
process(inti)
{
intj,num;
num=number[i][0];
for(j=3;j>=1;j--,num/=10)
{
number[i][j]=num%10;
}
}
copy_num(inti)
{
intj;
for(j=1;j<=3;j++)
{
array[i][j]=number[array[i][0]][j];
}
}
comp_num(intn)
{
staticintii;
staticintjj;
inti,num,k,*p;
int*pcount;
pcount=&lcount[0];
p=ⅈ
for(i=1;i<=3;i++)
{
for(num=0,k=0;k {
num=num*10+array[k][i];
}
if(num<=larray[n-2][*p])
for(;*p>=0&&num ;
else
for(;*p<*pcount&&num>larray[n-2][*p];(*p)++)
;
if(*p<0||*p>=*pcount)
{
*p=0;
return0;
}
if(num!
=larray[n-2][*p])
return0;
}
return1;
}
find1(inti)
{
intnum,j;
for(num=0,j=0;j<3;j++)
{
num=num*10+array[j][i];
}
returnfind0(num);
}
find2(void)
{
intnum1,num2,j,i;
for(num1=0,j=0;j<3;j++)
{
num1=num1*10+array[j][j+1];
}
for(num2=0,j=0,i=3;j<3;j++,i--)
{
num2=num2*10+array[j][i];
}
if(find0(num1))
returnfind0(num2);
else
return0;
}
find0(intnum)
{
staticintj;
if(num<=number[j][0])
for(;j>=0&&num ;
else
for(;jnumber[j][0];j++)
;
if(j<0||j>=count)
{
j=0;
return0;
}
if(num==number[j][0])
return1;
else
return0;
}
p_array(void)
{
inti,j;
for(i=0;i<3;i++)
{
for(j=1;j<=3;j++)
{
printf("%6d",array[i][j]);
}
printf("\n");
}
}
思路二:
上面的方法比较复杂,还有简洁一些的算法,比如
/*简洁算法*/
#defineN3
#include
voidmain()
{
inta[N][N],i,j,k;
for(i=0;ifor(j=0;j{
a[i][j]=0;
}
j=(N-1)/2;
a[0][j]=1;
for(k=2;k<=N*N;k++)
{
i=i-1;
j=j+1;
if((i<0)&&(j==N))
{
i=i+2;
j=j-1;
}
else
{
if(i<0)/*当行数减到第一行,返回到最后一行*/
i=N-1;
if(j>N-1)/*当列数加到最后一行,返回到第一行*/
j=0;
}
if(a[i][j]==0)
a[i][j]=k;
else
{
i=i+2;
j=j-1;
a[i][j]=k;
}
}
for(i=0;i{
for(j=0;jprintf(\"%5d\",a[i][j]);
printf(\"\\n\\n\");
}
}
/*下面还有更高级的思路和算法*/
问题扩展:
一般地,定义阶数N,即有一个N*N阶的宫格,该如何填数呢?
比如令N=99,则形成一个99*99宫格,要填的数字从1到9801,算法和思路如下
#defineN99
#include
voidmain()
{inta[N][N]={0},i=0,j,k;j=(N-1)/2; i=0;
for(k=1;k<=N*N;)
{if((i<0)&&(j==N))
{i=i+2;j=j-1;}
elseif(i<0)
i=N-1;
elseif(j>N-1)
j=0;
elseif(!
a[i][j])
{a[i][j]=k++;
i=i-1;j=j+1;}
else
{i=i+2;j=j-1;}
}
for(i=0;i{for(j=0;jprintf(\"\\n\\n\");
}
}
/*这个算法简洁得让人感动,可以轻松算出任意阶数的宫格*/
其他思路简述:
基础摒除法基础摒除法就是利用1~9的数字在每一行、每一列、每一个九宫格都只能出现一次的规则进行解题的方法。
基础摒除法可以分为行摒除、列摒除、九宫格摒除。
实际寻找解的过程为:
寻找九宫格摒除解:
找到了某数在某一个九宫格可填入的位置只余一个的情形;意即找到了该数在该九宫格中的填入位置。
寻找列摒除解:
找到了某数在某列可填入的位置只余一个的情形;意即找到了该数在该列中的填入位置。
寻找行摒除解:
找到了某数在某行可填入的位置只余一个的情形;意即找到了该数在该行中的填入位置。
唯一解法当某行已填数字的宫格达到8个,那么该行剩余宫格能填的数字就只剩下那个还没出现过的数字了。
成为行唯一解.当某列已填数字的宫格达到8个,那么该列剩余宫格能填的数字就只剩下那个还没出现过的数字了。
成为列唯一解.当某九宫格已填数字的宫格达到8个,那么该九宫格剩余宫格能填的数字就只剩下那个还没出现过的数字了。
成为九宫格唯一解.
唯余解法唯余解法就是某宫格可以添入的数已经排除了8个,那么这个宫格的数字就只能添入那个没有出现的数字.
区块摒除法区块摒除法是基础摒除法的提升方法,是直观法中使用频率最高的方法之一.
余数测试法所谓余数测试法就是在某行或列,九宫格所填数字比较多,剩余2个或3个时,在剩余宫格添入值进行测试的解题方法.
隐性唯一候选数法当某个数字在某一列各宫格的候选数中只出现一次时,那么这个数字就是这一列的唯一候选数了.这个宫格的值就可以确定为该数字.这时因为,按照数独游戏的规则要求每一列都应该包含数字1~9,而其它宫格的候选数都不含有该数,则该数不可能出现在其它的宫格,那么就只能出现在这个宫格了.对于唯一候选数出现行,九宫格的情况,处理方法完全相同。
三链数删减法找出某一列、某一行或某一个九宫格中的某三个宫格候选数中,相异的数字不超过3个的情形,进而将这3个数字自其它宫格的候选数中删减掉」的方法就叫做三链数删减法。
隐性三链数删减法在某行,存在三个数字出现在相同的宫格内,在本行的其它宫格均不包含这三个数字,我们称这个数对是隐形三链数.那么这三个宫格的候选数中的其它数字都可以排除.当隐形三链数出现在列,九宫格,处理方法是完全相同的.
矩形顶点删减法矩形顶点删减法和直观法讲到的矩形摒除法分析方法是一样的。
矩形顶点删减法在识别时比较不容易找到,所以最好先使用其它的方法。
三链列删减法三链列删减法是矩形顶点删减法的扩展,如果不清除矩形顶点删减法,可以参考矩形顶点删减法,以便于更容易理解本节内容。
利用“找出某个数字在某三列仅出现在相同三行的情形,进而将该数字自这三行其他宫格候选数中删减掉”;或“找出某个数字在某三行仅出现在相同三列的情形,进而将该数字自这三列其他宫格候选数中删减掉”的方法就叫做三链列删减法。
关键数删减法在进入到解题后期,利用前面讲到的唯一候选数法、隐性唯一候选数法、区块删减法、数对删减法、隐性数对删减法、三链数删减法、隐性三链数删减法、矩形顶点删减法、三链列删减法都无法有进展的时候,可以考虑使用关键数删减法。
关键数删减法就是在后期找到一个数,这个数在行(或列,九宫格)仅出现两次的数字。
我们假定这个数在其中一个宫格类,继续求解,如果发生错误,则确定我们的假设错误。
如果继续求解仍然出现困难,不妨假设这个数在另外一个宫格,看能不能得到错误。
这就是关键数删减法.