研究!rsc公司

关于编程的想法和链接,通过

RSS(RSS)

最小布尔公式
发布于2011年5月18日,星期三。

28. 这是AND或or运算符的最小数目你需要写任何五个变量的布尔函数。亚历克斯·希利我在2010年4月计算过。在那之前,我相信没有人知道这个小事实。这篇文章描述了我们是如何计算的以及我们是如何差点被挖走的Knuth的第4A卷它考虑了AND、OR和XOR的问题。

一种天真的暴力方法

两个变量的任意布尔函数最多可以使用3个AND或or运算符写入:奇偶函数在两个变量上,XXOR Y是(X AND Y')OR(X'AND Y),其中X'表示“不是X。”我们可以通过写AND和OR来缩短符号类似乘法和加法:XXOR Y=X*Y'+X'*Y。

对于三个变量,奇偶校验也是最难的函数,需要9个运算符:XXOR Y XOR Z=(X*Z'+X'*Z+Y')*(X*Z+X'*Z’+Y)。

对于四个变量,奇偶校验仍然是最难的函数,需要15个运算符:W XOR X XOR Y XOR Z=(X*Z'+X'*Z+W'*Y+W*Y')*(X*Z+X'*Z’+W*Y+W'*Y’)。

到目前为止,这个序列提出了几个问题。奇偶性总是最难的函数吗?操作员的最少数量是否在2人之间交替n个−1和2n个+1?

我在2001年1月听证会后计算了这些结果尼尔·斯隆(Neil Sloane)提出的问题,他建议将其作为一种变体克劳德·香农(Claude Shannon)首次研究的类似问题。

我编写的计算a(4)的程序计算n个变量的每个布尔函数的运算符为了找到所有函数的最大最小值。有2个4=四个变量和每个函数的16个设置可以为每个设置选择自己的值,因此有2个16不同的功能。更糟糕的是,您构建了新的函数通过使用成对的旧函数并用and或or连接它们。216不同功能意味着216·216= 232成对的函数。

我写的程序是对Floyd-Warshall的修改全对最短路径算法。该算法是:

//Floyd-Warshall所有配对最短路径func计算():对于每个节点i对于每个节点jdist[i][j]=直接距离,或∞对于每个节点k对于每个节点i对于每个节点jd=距离[i][k]+距离[k][j]如果d<dist[i][j]dist[i][j]=d返回

该算法首先将距离表dist[i][j]设置为如果i与j相连,则为实际距离,否则为无穷大。然后每轮更新表以说明路径穿过节点k:如果从i到k再到j更短,它在表格中节省了较短的距离。这些节点是从0到n编号,所以变量i、j、k只是整数。因为只有n个节点,我们知道我们将在之后完成外环结束。

我编写的用于查找最小布尔公式大小的程序是一种适应,用公式大小代替距离。

//算法1func计算()对于每个函数f尺寸[f]=∞对于每个单变量函数f=v尺寸[f]=0已更改=假对于每个函数f对于每个函数gd=尺寸[f]+1+尺寸[g]如果d<大小[f或g]大小[f或g]=d已更改=true如果d<尺寸[f和g]尺寸[f和g]=d已更改=true如果未更改返回

算法1运行与Floyd-Warshall算法相同的迭代更新循环,但你什么时候能停下来就不那么明显了,因为你没有事先知道最大配方尺寸。所以它一直运行到一轮没有找到任何新的函数为止,迭代直到找到一个固定点。

上面的伪代码掩盖了一些细节,例如每个函数的循环可以在已知函数队列的大小有限,因此每个函数循环省略了但已知。这只是一个持续的因素改善,但它很有用。

上面缺少的另一个重要细节是函数的表示。最方便的表示是一个二进制真值表。例如,如果我们在计算二元函数的复杂性,有四种可能的输入,我们可以将其编号如下。

X(X)Y(Y)价值
002= 0
真的012= 1
真的102= 2
真的真的112= 3

这些函数是4位数字,给出每个输入的函数。例如,函数13=11012除X=假Y=真外,所有输入均为真。三个变量函数对应于生成8位真值表的3位输入,等等。

这种表示有两个关键优势。第一个是编号是密集的,因此您可以实现一个键控地图by函数使用简单数组。第二是操作“f AND g”和“f OR g”可以使用实现按位运算符:“f AND g”的真值表是按位的f和g的真值表的AND。

该程序在2001年运行良好,足以计算写入任何1-、2-、3-和4-变量布尔函数。每一轮取渐近O(22n个·22n个)=O(22n+1)时间和数量所需的轮数是O(最终答案)。n=4的答案为15,因此所需的计算顺序为15·225=15·232最内层循环的迭代。这在我使用的电脑上是合理的时间,但n=5的答案可能在30左右,需要30·264要计算的迭代,其中似乎遥不可及。当时,平价似乎总是合理的最难的函数,并且最小大小将继续在2之间交替n个−1和2n个+1。这是一个很好的图案。

利用对称性

五年后,亚历克斯·希利和我开始讨论这个序列,亚历克斯用这个理论的结果推翻了这两个猜想电路复杂性。(理论家!)尼尔·斯隆将此注释添加到这个序列的条目在他的整数序列在线百科全书中:

%E A056287 Russ-Cox推测X1异或。。。XOR X(异或X)n个总是最坏的f,a(5)=33,a(6)=63。但是(2006年1月27日)亚历克斯·希利指出,对于大n来说,这个猜想肯定是错误的。那么,a(5)是什么?

的确。什么是(5)?没有人知道,也不清楚如何找出答案。

2010年1月,亚历克斯和我开始研究如何加快a(5)的计算速度。30·264太多了但也许我们可以找到方法来减少这个数字。

一般来说,如果我们能确定一类函数f保证成员具有相同的复杂性,然后我们可以只保留一个班级代表只要我们在循环体中重新创建整个类。过去是什么:

对于每个函数f对于每个函数g访问f和g访问f OR g

可以重写为

对于每个正则函数f对于每个正则函数g对于每个相当于f的ff每个gg相当于g访问ff和gg访问ff或gg

这看起来并不是一个进步:它在做所有的事情同样的工作。但它可以为新的优化打开大门取决于所选的等效项。例如,功能“f”和“\f”得到保证具有相同的复杂性,通过德摩根定律.如果我们只保留一个列表中“for each function”迭代的那两个,我们可以展开内部的两个循环,生成:

对于每个正则函数f对于每个正则函数g访问f OR g访问f和g访问OR g访问-f和g访问f OR-g访问f和g访问访问

这仍然没有改善,但也没有更糟。两个循环中的每一个都考虑一半的函数但内部迭代要长四倍。现在我们可以注意到,有一半的测试不是值得一做:“f和g”是对“\fOR-g”等等,所以只有一半其中一些是必要的。

假设在“f”和“-f”之间选择时当所有的输入都为真时,我们保留一个为假。(这有一个很好的特性f^(整数32(f)>>31)是标准形式的真值表(f).)然后我们可以知道上述哪些组合将产生f和g已经规范时的规范函数:

对于每个正则函数f对于每个正则函数g访问f OR g访问f和g访问-f和g访问f和g

这是原始循环的两个改进因素。

另一个观察结果是排列函数的输入不会改变其复杂性:“f(V,W,X,Y,Z)”和“f(Z,Y,X,W,V)”将具有相同的最小尺寸。对于复杂函数,每个5! = 120个排列将产生不同的真值表。储存量减少120倍固然不错,但再次我们遇到了在迭代。这一次,有一个不同的减少最内部迭代中的工作。因为我们只需要生成一个成员等价类,对将输入排列到f和g,固定g时只排列f的输入保证至少击中一名成员排列f和g的每个类。所以我们在循环中获得120倍的因子并在迭代中丢失一次,以实现净节省第120页。(在某些方面,这与我们在“f”和“-f”中使用的技巧相同。)

最后一个观察结果是否定任何输入函数不会改变其复杂性,因为X和X'具有相同的复杂性。我们用于排列的相同论点也适用这里,对于另一个常数因子25= 32.

代码为每个等价类存储一个函数然后重新计算f的等效函数,而不是g的等效函数。

对于每个正则函数f对于每个函数ff等价于f对于每个正则函数g访问ff OR g访问ff和g访问和g访问ff和g

总之,我们只节省了2·120·32=7680,将总迭代次数从30.2减少64= 5×1020至7×1016.如果你认为我们可以109每秒迭代次数,仍然需要800天的CPU时间。

此时的完整算法是:

//算法2func计算():对于每个函数f尺寸[f]=∞对于每个单变量函数f=v尺寸[f]=0已更改=假对于每个正则函数f对于每个函数ff等价于f对于每个正则函数gd=尺寸[ff]+1+尺寸[g]已更改|=就诊(d,ff或g)更改|=访问(d,ff和g)已更改|=就诊(d,ff和g)已更改|=就诊(d、ff和g)如果未更改返回功能访问(d,fg):如果大小[fg]!=∞返回false将fg记录为规范对于每个函数,ffgg等效于fg大小[ffgg]=d返回true

helper函数“visit”不仅必须设置其参数fg的大小也包括输入的置换或反转下的所有等效函数,以便将来的测试可以看到它们已经被计算出来了。

方法探索

我们可以做最后的改进。循环直到事情停止改变的方法多次考虑每个功能对随着尺寸的减小。相反,我们可以考虑函数按照复杂性的顺序,使主循环首先构建复杂性最低的所有函数1,然后是最小复杂度为2的所有函数,如果我们这样做,我们最多只考虑一次每个函数对。当所有功能都被考虑后,我们可以停止。

将此思想应用于算法1(在规范化之前)可以得到:

//算法3func计算()对于每个函数f尺寸[f]=∞对于每个单变量函数f=v尺寸[f]=0对于k=1到∞对于每个函数f对于尺寸k−尺寸(f)−1的每个函数g如果尺寸[f和g]=∞尺寸[f和g]=k调整大小++如果尺寸[f或g]=∞大小[f或g]=k调整大小++如果尺寸==22n个返回

将该思想应用于算法2(在规范化之后)可以得到:

//算法4func计算():对于每个函数f大小[f]=∞对于每个单变量函数f=v尺寸[f]=0对于k=1到∞对于每个正则函数f对于每个函数ff等价于f对于每个标准函数g的大小k−大小(f)−1访问(k,ff或g)访问(k,ff和g)访问(k、ff和g)访问(k、ff和g)如果未访问==22n个返回功能访问(d,fg):如果尺寸[fg]!=∞返回将fg记录为规范对于每个函数,ffgg等价于fg如果大小[ffgg]!=∞大小[ffgg]=d未访问+=2//计数ffgg和-ffgg返回

算法1和2中的原始循环考虑了每个循环的迭代。算法3和4中的新循环只考虑每对f、g一次,当k=尺寸(f)+尺寸(g)+1时。这将删除领先因子30(我们的次数期望运行第一个循环)运行时的。现在,预期的迭代次数约为264/7680 = 2.4×1015.如果我们能做到109迭代每秒只有28天的CPU时间,如果你能等一个月的话,我可以送你。

我们的估计不包括并非所有函数对都需要待考虑。例如,如果最大大小为30,则大小为14的函数永远不需要与大小为16的函数配对,因为任何结果的大小都是14+1+16=31。所以即使是2.4×1015是高估了,但大致正确。(事后来看,我只能报告1.7×1014需要考虑成对而且我们估计有10个9迭代每秒都很乐观。实际计算持续了20天,平均约108每秒迭代次数。)

结局:定向搜索

一个月的等待时间仍然很长,我们可以做得更好。接近尾声时(在k大于22之后),我们正在探索相当大的函数对空间,希望找到一个剩下的函数数量相当少。在这一点上,从自下而上“把东西组合在一起,看看我们做了什么”自上而下“尝试将其作为这些特定功能之一。”也就是说,当前搜索的核心是:

对于每个正则函数f对于每个函数ff等价于f对于每个标准函数g的大小k−大小(f)−1访问(k,ff或g)访问(k、ff和g)访问(k、ff和g)访问(k、ff和g)

我们可以将其更改为:

对于每个缺失的函数fg对于每个正则函数g为了所有可能的f,其中一个保持*fg=f或g*fg=f和g*fg=\f和g*fg=f和g如果尺寸[f]==k−尺寸(g)−1访问(k,fg)下一个前景

当我们到达终点时,探索所有可能的f缺失的函数——定向搜索比探索所有组合的野蛮力量。

例如,假设我们正在寻找fg=f OR g的f。只有当fg OR g==fg时,方程才可能满足。也就是说,如果g有任何多余的1位,那么f将不起作用,所以我们可以继续。否则,剩下的条件是f和g==fg和g。也就是说,对于g为0的位位置,f必须匹配fg。f的其他位(g有1的位)可以取任何值。我们可以通过递归地尝试all来枚举可能的f值“不在乎”位的可能值。

函数查找(x,any,xsize):如果大小(x)==xsize返回x任何时候!=0bit=任意AND−任意//任意中最右边的1位any=任意AND位如果f=find(x OR bit,any,xsize)成功返回f返回故障

我们为递归选择哪个位并不重要,但找到最右边的1位很便宜:它由(诚然令人惊讶)表达“any AND−any”

鉴于找到,上面的循环可以尝试以下四种情况:

公式条件基数x“任意”位
fg=f或gfg或g==fgfg和\g
fg=f或gfg或-g==fgfg和g
fg=f或gfg或g==fg前景与前景
fg=f或gfg或g==fg前景和g

重写布尔表达式以仅使用四种OR形式意味着我们只需要编写find的“添加位”版本。

最后的算法是:

//算法5func计算():对于每个函数f大小[f]=∞对于每个单变量函数f=v尺寸[f]=0//生成函数。对于k=1到max_generate对于每个正则函数f对于每个函数ff等价于f对于每个标准函数g的大小k−大小(f)−1访问(k,ff或g)访问(k、ff和g)访问(k、ff和g)访问(k、ff和g)//搜索函数。对于k=max_generate+1到∞对于每个缺失的函数fg对于每个正则函数gfsize=k−尺寸(g)−1如果fg或g==fg如果f=find(fg AND­g,g,fsize)成功访问(k,fg)下一个前景如果fg或g==fg如果f=find(fg AND g,\g,fsize)成功访问(k,fg)下一个前景如果-fg或g==-fg如果f=find(\fg AND \g,g,fsize)成功访问(k,fg)下一个前景如果-fg或-g==-fg如果f=find(fg AND g,g,fsize)成功访问(k,fg)下一个前景如果未访问==22n个返回功能访问(d,fg):如果尺寸[fg]!=∞返回将fg记录为规范对于每个函数,ffgg等价于fg如果大小[ffgg]!=∞大小[ffgg]=d未访问+=2//计数ffgg和-ffgg返回函数查找(x,any,xsize):如果大小(x)==xsize返回x任何时候!=0bit=任意AND−任意//任意中最右边的1位any=任意AND位如果f=find(x OR bit,any,xsize)成功返回f返回故障

为了了解这里的速度,检查我的工作,我使用这两种算法运行程序在2.53 GHz Intel Core 2 Duo E7200上。

-----函数数---------时间----
大小典型的全部全部,累计生成搜索
011010
128292<0.1秒3.4分钟
22640732<0.1秒7.2分钟
744205152<0.1秒12.3分钟
4192527629696<0.1秒30.1分钟
544117440147136<0.1秒1.3小时
6142515040662176<0.1秒3.5小时
7436199960826617840.2秒11.6小时
81209659840092601840.6秒1.7天
9330719577332288375161.7秒4.9天
10774150822560796600764.6秒[10天?]
111725711461926419427934010.8秒[20天?]
123185122130100841558034821.7秒[50天?]
135390137470477679028512438.5秒[80天?]
1475248533594528132387965258.7秒[100天?]
159457266765364219915332941.5分钟[120天?]
169823769722876026887620542.1分钟[120天?]
178934262858944033173514944.1分钟[90天?]
186695146855289637859043909.1分钟[50天?]
1941664287647616407355200623.4分钟[30天?]
2021481144079832421763183857.0分钟[10天?]
2186805553822442731700622.4小时2.5天
2227301609956842892696305.2小时11.7小时
239374428800429369843011.2小时2.2小时
24228959328429465775822.0小时33.2分钟
2510328320042949409581.7天4.0分钟
26212222442949631822.9天42秒
2710360242949667844.7天2.4秒
285124294967296[7天?]0.1秒

括号内的时间是根据所涉及的工作估算得出的:我没有等待中间搜索步骤这么久。搜索算法比生成算法差得多,直到剩下的函数非常少。然而,它在最有用的时候就派上了用场:当生成算法已经慢到爬行。如果我们运行generate遍历大小为22的公式,然后切换要继续搜索23个,我们可以在中运行整个计算仅仅超过半天的CPU时间。

a(5)的计算确定了所有616126的大小5个输入的标准布尔函数。相比之下,有6个输入的200万亿正则布尔函数.无论我们使用什么聪明的技巧,通过暴力计算都不太可能确定a(6)。

添加XOR

我们假设使用“与”和“或”作为我们的布尔公式的基础。如果我们还允许XOR,函数可以使用更少的运算符编写。特别是,1、2、3和4输入的最难函数病例平价现在是微不足道的。Knuth研究了五输入布尔函数的复杂性在中详细使用AND、OR和XOR计算机编程艺术,第4A卷.第7.1.2节的算法L与上述算法3相同,用于计算4个输入函数。Knuth提到,要使其适应5输入功能,必须仅处理正则函数并给出5个输入函数的结果允许XOR。因此,检查我们工作的另一种方法是将XOR添加到我们的算法4中并检查我们的结果是否与Knuth的相符。

由于最小配方尺寸较小(最多12个)使用XOR计算大小比以前快得多:

-----函数数-----
大小典型的全部全部,累计时间
011010
1102112<0.1秒
2511401252<0.1秒
201157012822<0.1秒
493109826122648<0.1秒
536693644010590880.1秒
61730723688082959680.7秒
7878247739088560350564.5秒
84029725067432030670937624.0秒
9141422955812256126252163295.5秒
1027327719453839363207905568200.7秒
1114570710559126084263818176121.2秒
12442331149120429496729665.0秒

Knuth没有讨论任何类似算法5的内容,因为搜索特定功能不适用于AND、OR和XOR基础。XOR是非单调的功能(它既可以打开位又可以关闭位),因此没有什么测试能像我们这样”如果fg或g==fg而且没有一小部分“不在乎”的比特来调整f的搜索。在XOR案例中搜索合适的f尝试所有正确大小的f,这正是算法4所做的。

第4A卷还考虑了建立最小电路的问题,它们类似于公式,但可以免费使用公共子表达式,以及建造尽可能浅的电路的问题。有关所有详细信息,请参阅第7.1.2节。

代码和网站

网站boolean-oracle.swtch.com网站允许您键入布尔表达式并返回其最小公式。它使用运行算法5时生成的表;那些桌子和本文中描述的程序还包括现场可用.

后记:生成所有排列和反转

上述算法主要取决于步骤对于每个函数ff等价于f,”它生成通过置换或反转f的输入而获得的所有ff,但我没有解释怎么做。我们已经看到我们可以操纵二进制真值表表示直接转弯(f)进入之内?f并计算功能组合。我们还可以直接操作二进制表示反转特定输入或交换一对相邻输入。使用这些操作,我们可以循环使用所有等效函数。

要反转特定输入,让我们考虑一下真值表的结构。真值表中某位的索引对该条目的输入进行编码。例如,索引的低位给出第一个输入的值。因此,索引0、2、4、6…处的偶数位-对应于第一个输入为假,而索引1、3、5、7…处的奇数位-符合第一个输入为true。仅更改索引中的该位对应于更改单个变量,因此索引0、1仅在第一个输入的值上有所不同,2、3、4、5、6、7等等。给定f(V,W,X,Y,Z)的真值表,我们可以计算通过交换相邻的比特对,得到f(-V,W,X,Y,Z)的真值表在原始真值表中。更好的是,我们可以使用位并行进行所有交换操作。为了反转不同的输入,我们交换较大的位运行。

功能 Truth表((f)=f(V、W、X、Y、Z))
f(V、W、X、Y、Z)(f&0x5555555)<<1|(f>>1)&0x555555
f(V、W、X、Y、Z)(f&0x3333333)<<2|(f>>2)&0x333333
f(V、W、X、Y、Z)(f&0x0f0f0f)<<4|(f>>4)&0x0f0f0f
f(V、W、X、Y、Z)(f&0x00ff00ff)<<8|(f>>8)&0x00ff
f(V、W、X、Y、Z)(f&0x0000ffff)<<16|(f>>16)&0x0000xff

能够反转特定输入让我们考虑所有可能通过一次建立一个反转。这个格雷码让我们枚举所有可能的5位输入代码,同时只更改1位当我们从一个输入移到下一个输入时:

0, 1, 3, 2, 6, 7, 5, 4,
12, 13, 15, 14, 10, 11, 9, 8,
24, 25, 27, 26, 30, 31, 29, 28,
20, 21, 23, 22, 18, 19, 17, 16

这将最小化我们需要的反转数:要考虑所有32个案例,我们只需要需要31次反转操作。相反,访问通常二进制顺序为0、1、2、3、4…的5位输入码。。。通常需要更改多个位,例如从3更改为4。

要交换一对相邻的输入,我们可以再次利用真值表。对于一对输入,有四种情况:00、01、10和11。我们可以离开00和11个案例,因为它们在交换下是不变的,集中精力交换01位和10位。前两个输入在真值表中变化最频繁:每次运行4位与这四种情况相对应。在每次运行中,我们都想让第一个和第四个单独运行,并交换第二个和第三个。对于以后的输入,这四种情况由比特段而不是单个比特组成。

功能 Truth表((f)=f(V、W、X、Y、Z))
(f)(W、 V(V)、X、Y、Z)f&0x99999999|(f&0x2222222)<<1|(f>>1)&0x22225222
f(V,X、 周、Y、Z)f&0xc3c3c3 |(f&0x0c0c0c)<<1|(f>>1)&0x0c0 c0c
f(V、W,Y、 X(X),Z)f&0xf00ff00f|(f&0x00f000f0)<<1|(f>>1)&0x00f000f0
f(V、W、X、,Z、 Y(Y))f&0xff0000ff|(f&0x0000ff00)<<8|(f>>8)&0x0000x00

能够交换一对相邻的输入使我们可以考虑通过一次建立一个可能的排列。同样,通过以下方式访问所有排列也是很方便的一次只应用一个交换。第4A卷来了。第7.2.1.2节标题为“生成所有排列”,Knuth交付许多算法都可以做到这一点。对于我们的目的来说,最方便的是算法P,它生成一个只考虑一次所有排列的序列在步骤之间仅交换一次相邻输入。Knuth将其称为Algorithm P,因为它对应于使用的“普通更改”算法17世纪英国的敲钟人在所有可能的排列中敲响一组钟。1653年左右的一篇手稿中描述了该算法!

我们可以通过以下方式检查所有可能的排列和反转在所有反转的循环内的所有排列上嵌套循环,事实上,这就是我的程序所做的。但Knuth做得更好:他的练习7.2.1.2-20表明有可能建立所有的可能性只使用第一个输入的相邻交换和反转。然而,否定任意输入并不难,而且仍然如此最少的工作,所以代码坚持使用Gray代码和Plain更改。

(评论最初通过Blogger发布。)

  • 狼群7 (2011年5月19日2:56 AM)那么什么是最难用和/或有5个输入来表示的布尔函数?

  • 亚历克斯 (2011年5月19日4:11 AM) 这篇文章已被作者删除。

  • 亚历克斯 (2011年5月19日4:12 AM)wolke7你好,

    “与/或”(最多相当于“与/或”)上最难的一个函数是:

    v+w+x+y+z=0、1或3

    即,输入中没有或只有一个或三个输入为真。还有另外两个最难的函数(直到等价物:排列输入,否定一些输入,否定整个函数),这是对这个函数的扭曲。

    请参见http://boolean-oracle.swtch.com/(在博客条目的“代码和网站”部分中有简要介绍)了解更多详细信息,包括其他两个最难的功能的描述。

    亚历克斯

  • 白鲨 (2011年8月21日下午4:04)这非常有趣,也很有教育意义。一个47岁的老人可以学习什么来理解这里使用的词汇?最内部的循环、真输入、规范、置换。我从哪里开始(某些书籍或其他教育书籍?)学习这些数学表达式的含义,以便我获得更深入的理解?我开始研究二进制数和公式(布尔and、OR、NOR、XOR、XNOR等)。这一切都很吸引人,但我并没有像高中学习Trig时那样快速地捕捉到。

    我太老了吗??

    提前感谢您的指导。

  • 拉斯·考克斯 (2011年8月23日上午7:37)@baitshark——对于低级计算的一般介绍,我强烈推荐Charles Petzold的书Code:The Hidden Language of Computer Hardware and Software。这并不会讨论你提到的数学术语,但它确实讨论了很多布尔基本知识,读起来很有趣。

    我不知道有任何类似的数学或编程书籍。维基百科和MathWorld在数学方面都很强大,所以查找排列或规范之类的单词可能是一个很好的起点。如果你已经知道一点编程,乔恩·本特利的《编程珍珠》是一个很好的例子。

  • 厄尔尼 (2011年12月22日12:46 AM)已复制..:)
    谢谢你。。。

    很好的提示…:)