目录
介绍
回溯是一种编程技术,用于解决没有已知解决方案的问题。已知解问题的一个例子是计算从1开始的前N个整数的和。它很容易实现为:
int sum = 0;
for ( int i = 1; i <= N; ++i )
{
sum += i;
}
当然,一种计算总和的更有效方法是使用公式N * (N + 1) / 2,可以通过对N进行归纳证明。
魔方
魔方是N x N的数字数组,范围为1、2,... N 2,,这样数组的每个元素都包含一个唯一的数字(无重复),以及每一行,每一列,每一条主对角线的和都是一样的。下图取自Wikipedia,显示了一个3 x 3的魔方,总和等于15:
该总和称为幻数。这是一个尚无现成解决方案的问题,必须通过反复试验解决。
解决魔方的一个天真的方法:排列
解决魔方的一种可能方法是针对N x N平方生成范围为1、2,...,N 2的排序。一般的想法是生成一个排列,将其逐行转储到一个正方形数组中,并测试数组中的数字是否满足给定幻数的行,列和对角线总和要求。这个想法看起来是合理的,但是问题是,在最坏的情况下,必须生成范围的所有排列,因为很可能魔方的解是范围的最后排列。
N 2个数的所有排列都在N 2中生成!时间,其中“!” 表示阶乘函数。随着自变量变大,阶乘函数的增长速度快于指数函数。对于3 x 3的平方,9!= 362,880个排列;4 x 4平方,16!= 20,922,789,888,000排列; 而对于5 x 5平方,则为25!= 1.551121E25,其中“E25”表示1551121后跟19个零。显然,必须用另一种试错法,即回溯,来替换生成排列以解决魔方。
C#程序通过回溯解决魔方
给定N x N魔方和幻数M,一般过程是将数字1、2,...,N 2放置在数组元素中,并验证放置是否满足行,列和对角线总和都等于M。如果满足要求,则问题得以解决;如果不是,则必须通过取消最后一个放置并放置不同的编号来尝试另一种布置。因此,根据需要,对解决方案的尝试和回溯步骤本质上都是递归的。
程序的模块将以分段方式进行描述,即从下至上。完整的程序(作为C#控制台应用程序实现)位于文章所附的ZIP文件中。首先,必须定义要使用的所有数据结构和变量。下列类在MagicSquare命名空间内定义,可用于同时定义不同的魔方。
// Class to encapsulate the parameters of a magic square
public class Parameters
{
// Properties
public int N { get; set; } // Size of the square.
public int Nsquare { get; set; } // Upper limit of the range of numbers.
public int MagicNumber { get; set; } // The magic number for the square.
public bool ShowStep { get; set; } // Control whether or not to display
// the placement/removal of numbers
// Standard constructor
public Parameters( int n, int magicNumber, bool showStep = true )
{
N = n;
Nsquare = N * N;
MagicNumber = magicNumber;
ShowStep = showStep;
}
// Abbreviated constructor
public Parameters( int n, bool showStep = true )
{
N = n;
Nsquare = N * N;
// Compute magic number if not provided
MagicNumber = N * ( Nsquare + 1 ) / 2;
ShowStep = showStep;
}
// Function to validate the magic number
public bool IsValidMagicNumber()
{
return MagicNumber == N * ( Nsquare + 1 ) / 2;
}// IsValidMagicNumber
}// Parameters (class)
在Program类中定义了以下变量:
static int[ , ] square; // Array to store the solution.
static int row, // Row index into 'square'.
col; // Column index into 'square'.
static bool done; // Flag to signal successful termination.
static ulong steps; // Count of steps taken toward the solution.
static Queue<int> numbers; // Queue to hold numbers that have not been placed
// and to recycle numbers that are removed.
// Variables to collect timing information.
static DateTime startTime, endTime;
接下来要描述的所有功能也在该Program类中。定义了魔方后,想到的第一个功能是在幻数有效的情况下尝试求解魔方。
static void SolveSquare( Parameters _params )
{
if ( !_params.IsValidMagicNumber() )
{
Console.WriteLine( "\nNo solution for N = {0} and invalid Magic number = {1}\n",
_params.N, _params.MagicNumber );
}
else
{
square = new int[ _params.N, _params.N ];
numbers = new Queue<int>();
InitializeSquare( _params );
// For no particular reason, insert the numbers in the range 1 .. N ^ 2
// in reverse order.
for ( int i = _params.Nsquare; i > 0; --i )
{
numbers.Enqueue( i );
}
steps = 0L;
row = col = 0;
done = false;
startTime = DateTime.Now;
PlaceNumber( _params );
endTime = DateTime.Now;
if ( !done )
{
Console.WriteLine( "\n\n{0} steps", steps );
Console.WriteLine( "\nNo solution for N = {0} and Magic number = {1}\n",
_params.N, _params.MagicNumber );
}
ReportTimes( startTime, endTime );
}
}// SolveSquare
函数InitializeSquare将正方形中的所有元素都设置为零,以指示未放置任何数字,因此此处不会复制。关键函数是PlaceNumber负责尝试解决方案时放置数字,回收已放置的数字以及在回溯期间删除数字。如果PlaceNumber函数的参数中的ShowStep字段设置为true,则在该Console.Write语句的调用中,符号“.{0}”表示已在正方形中放置了一个数字,而符号“<{0}”表示已从该正方形中删除了一个数字。
static void PlaceNumber( Parameters _params )
{
int k, n, m, e;
// As long as there are numbers to be placed, and the
// (row, col) coordinates are within the limits of the
// square (ValidPosition == true), attempt to place a number.
if ( ( numbers.Count > 0 ) && ValidPosition( _params ) )
{
n = numbers.Count;
k = 0;
do
{
m = numbers.Dequeue(); // Remove number at the head.
if ( ( e = square[ row, col ] ) != 0 )
{
numbers.Enqueue( e ); // Recycle the current number.
}
square[ row, col ] = m; // Put the dequeued number in its place.
if ( _params.ShowStep )
{
Console.Write( ".{0} ", m ); // number placed
}
++k;
++steps;
if ( SquareOK( _params ) )
{
if ( numbers.Count == 0 )
{
done = true;
ShowSquare( _params );
}
else
{
NextPosition( _params ); // Compute next (row, col) coordinates.
PlaceNumber( _params ); // Recursively place another number.
}
}
} while ( k < n && !done );
// k == n || done
if ( !done ) // backtrack
{
numbers.Enqueue( square[ row, col ] ); // Recycle the number at the
// current position.
if ( _params.ShowStep )
{
Console.Write( "<{0} ", square[ row, col ] ); // number removed
}
square[ row, col ] = 0;
PreviousPosition( _params ); // Compute previous (row, col) coordinates.
}
}
// numbers.Count == 0 || !ValidPosition( _params )
}// PlaceNumber
函数SquareOK检查所有行(HorizontalCheck),所有列(VerticalCheck)以及主要对角线(Diagonal_1_Check)和(Diagonal_2_Check)中的总和是否等于幻数,即正方形是否为魔方。如果是,则将变量done设置为true表示信号终止,并且通过函数ShowSquare显示魔方。如果正方形不是魔术,则递归撤消最后的数字放置(回溯)以尝试将其他数字放置在队列数字的前面。
static bool SquareOK( Parameters _params )
{
int row = 0, col = 0, n = _params.N;
while ( row < n && HorizontalCheck( _params, row ) )
{
++row;
}
while ( col < n && VerticalCheck( _params, col ) )
{
++col;
}
return row == n && col == n
&& Diagonal_1_Check( _params ) && Diagonal_2_Check( _params );
}// SquareOK
四个总和检查函数返回调用IndexAndSumOK函数的结果,该结果确定是否没有放置数字,放置正在进行或放置在各自方向上。
static bool IndexAndSumOK( Parameters _params, int index, int sum )
{
int n = _params.N, magicNr = _params.MagicNumber;
return sum == 0 // No numbers have been placed in a particular direction
||
( index < n && sum < magicNr ) // Placement of numbers in progress
||
( index == n && sum == magicNr ); // Placement complete
}// IndexAndSumOK
作为两个示例,函数HorizontalCheck和Diagonal_1_Check定义如下:
static bool HorizontalCheck( Parameters _params, int row )
{
int col = 0, e, s = 0;
while ( col < _params.N && ( e = square[ row, col ] ) != 0 )
{
s += e;
++col;
}
return IndexAndSumOK( _params, col, s );
}// HorizontalCheck
static bool Diagonal_1_Check( Parameters _params )
{
int row = 0, e, s = 0;
while ( row < _params.N && ( e = square[ row, row ] ) != 0 )
{
s += e;
++row;
}
return IndexAndSumOK( _params, row, s );
}// Diagonal_1_Check
函数NextPosition和PreviousPosition,由函数PlaceNumber调用,分别在放置数字和放置回溯期间遵循正方形的行主要遍历。
static void NextPosition( Parameters _params )
{
if ( col == _params.N - 1 )
{
col = 0;
++row;
}
else // col < _params.N - 1
{
++col;
}
}// NextPosition
static void PreviousPosition( Parameters _params )
{
if ( col == 0 )
{
col = _params.N - 1;
--row;
}
else // col > 0
{
--col;
}
}// PreviousPosition
ZIP文件中完整的程序很容易遵循此处未复制的所有功能。为了演示程序的操作,请在Program类的顶部考虑以下变量的定义。
static Parameters _3x3_params_1 = new Parameters( 3, 15 ); // solution
static Parameters _3x3_params_2 = new Parameters( 3, 17 ); // no solution
static Parameters _4x4_params = new Parameters( 4, false ); // solution
然后,通过函数SolveSquare在每个函数上执行Main函数:
static void Main( string[] args )
{
SolveSquare( _3x3_params_1 );
SolveSquare( _3x3_params_2 );
SolveSquare( _4x4_params );
}// Main
产生以下输出:
.9 .8 .7 .6 .5 .4 .3 .2 .1 .8 .7 .6 .4 .3 .2 .8 .7 .6 .2 <2 .7 .6 <6 .2 .8 .7 .6 .3 <3
.8 .7 .6 <6 .3 .2 .8 .7 .6 .4 <4 .8 .7 .6 .4 .2 .8 .7 .6 .2 <2 .7 .6 <6 <4 .2 .8 .7 .6
.4 .3 .8 .7 .6 <6 .3 .8 .7 .6 .4 <4 <3 <2 .8 .7 .6 <6 .4 .3 .2 .1 .8 .7 .6 .5 .3 .8 .7
.6 <6 .3 .8 .7 .6 .5 <5 <3 .8 .7 .6 .5 .3 .1 .8 .7 .6 .1 .8 <8 .6 <6 .1 .8 .7 .6 .3 <3
.8 .7 .6 <6 .3 .1 .8 .7 .6 .5 <5 .8 .7 .6 .5 .1 .8 .7 .6 .1 .8 <8 .6 <6 <5 <3 .1 .8 .7
.6 .5 <5 .3 .2 .1 .8 .7 .6 .5 .4 <4 .2 .1 .8 .7 .6 .5 .4 .3 .1 .8 .7 .6 .5 <5 .8 .7 .6
.5 .1 .8 .7 .6 .1 .8 <8 .6 <6 <5 .1 .8 .7 .6 .5 .3 .8 .7 .6 <6 .3 .8 .7 .6 .5 <5 <3 .8
.7 .6 .5 .3 .1 .8 .7 .6 .1 .8 <8 .6 <6 .1 .8 .7 .6 .3 <3 .8 .7 .6 <6 <5 .3 <3 .1 .8 .7
.6 .5 .4 .3 .2 .8 .7 .6 .2 <2 .7 .6 <6 .2 .8 .7 .6 .3 <3 .8 .7 .6 <6 .3 .2 .8 .7 .6 .4
<4 .8 .7 .6 .4 .2 .8 .7 .6 .2 <2 .7 .6 <6 <4 .2 .8 .7 .6 .4 .3 .8 .7 .6 <6 .3 .8 .7 .6
.4 <4 <3 .8 .7 .6 <6 .4 .3 .2 <2 <1 .8 .7 .6 .5 .4 .3 .2 .1 .9 .7 .5 .4 .3 .2 .9 .7 <7
.3 .2 .9 .7 .4 .2 .9 <9 .4 <4 .2 .9 .7 .4 .3 <3 .9 .7 <7 .4 .3 .2 .9 .7 .5 <5 .2 .9 .7
.5 .3 <3 .7 .5 .3 <3 .9 .7 .5 .3 .2 .9 .7 <7 <5 .3 .2 .9 .7 .5 .4 <4 .9 .7 .5 .4 .2 .9
.7 .4 .2 .9 <9 <7 .4 .2 .9 .7 .5 <5 <4 .2 .9 .7 .5 .4 .3 .9 .7 <7 .4 .3 .9 .7 .5 .3 <3
.7 .5 <5 .3 .9 .7 .5 .4 <4 <3 <2 .9 .7 <7 .5 .4 .3 .2 .1 .9 .7 .6 .4 .3 .9 .7 <7 .4 .3
.9 .7 .6 <6 .3 .9 .7 .6 .4 <4 <3 .9 .7 .6 .4 .3 .1 .9 .7 <7 .3 .1 .9 .7 .4 <4 .1 .9 .7
.4 .3 <3 .9 .7 <7 .4 .3 .1 .9 .7 .6 <6 .1 .9 .7 .6 .3 <3 .9 .7 .6 .3 .1 .9 .7 <7 <6 .3
.1 .9 .7 .6 .4 <4 .9 .7 .6 .4 .1 .9 .7 <7 .4 .1 .9 .7 .6 <6 <4 <3 .1 .9 .7 .6 <6 .4 .3
.2 .1 .9 .7 .6 .5 <5 .9 .7 .6 .5 .1 .9 .7 .5 .1 .9 <9 <7 .5 .1 .9 .7 .6 <6 <5 .1 .9 .7
.6 .5 .2 .9 .7 <7 .5 .2 .9 .7 .6 .2 <2 .7 .6 <6 .2 .9 .7 .6 .5 <5 <2 .9 .7 .6 .5 .2 .1
.9 .7 <7 .2 .1 .9 .7 .5 .1 .9 <9 .5 <5 .1 .9 .7 .5 .2 <2 .9 .7 <7 .5 .2 .1 .9 .7 .6 <6
.1 .9 .7 .6 .2 <2 .7 .6 .2 <2 .9 .7 .6 .2 .1 .9 .7 <7 <6 <5 .2 .1 .9 .7 .6 .5 <5 .3 .2
.1 .9 .7 .6 .5 .4 .2 .1 .9 .7 .6 .5 <5 .9 .7 .6 .5 .1 .9 .7 .5 .1 .9 <9 .1 .9 <9 <7 .5
.1 .9 .7 .6 <6 <5 .1 .9 .7 .6 .5 .2 .9 .7 <7 .5 .2 .9 .7 .6 .2 .7 .2
N = 3
Magic number = 15
8 3 4
1 5 9
6 7 2
567 steps
End time: 7:09:04 AM
Start time: 7:09:04 AM
Elapsed time: 0 0:0:0 136 ms
No solution for N = 3 and invalid Magic number = 17
N = 4
Magic number = 34
16 15 2 1
6 3 14 11
8 9 5 12
4 7 13 10
237,300 steps
End time: 7:09:04 AM
Start time: 7:09:04 AM
Elapsed time: 0 0:0:0 455 ms
Press any key to continue . . .
通过生排列换来解决魔方
只是为了与回溯进行比较,本节描述了通过生成魔方范围内数字的排列来实现魔方的方法。所有处理排列的代码都在ReportTimes函数之后。生成排序所需的变量如下:
static ulong count; // Count of permutations generated.
static int[] source; // Array to generate permutations in-situ.
static ulong countAtSolution; // Value of 'count' when first solution found.
static DateTime timeAtSolution; // Time when first solution found.
static bool abort; // Flag to stop recursive function 'Permute'.
与回溯时的驱动程序函数SolveSquare类似,函数排列驱动排列的生成。它初始化使用的变量,特别是用于存储排列的数组,然后调用函数Permute。
static void Permutations( Parameters _params )
{
int n = _params.N;
count = 0L;
InitializeSource( _params.Nsquare );
square = new int[ n, n ];
abort = false;
startTime = DateTime.Now;
Permute( _params, source, 0, _params.Nsquare );
endTime = DateTime.Now;
Console.WriteLine( "\n{0} permutations\n", count );
ShowSquare( _params );
ReportTimes( startTime, endTime );
}// Permutations
如果不停止,函数Permute使用数组源生成原位(即就地)存储在数组中的数字的所有排列。由于目标是找到魔方的第一个解,因此使用变量中止来停止该函数。
// Function 'Permute', if not stopped, recursively generates ALL the
// permutations of the elements in its 'array' argument.
static void Permute( Parameters _params, int[] array, int k, int n )
{
int i;
if ( k == n ) // Permutation complete.
{
++count;
PrintArray( array, n );
ArrayToSquare( _params, array, n );
if ( SquareOK( _params ) )
{
countAtSolution = count;
timeAtSolution = DateTime.Now;
abort = true; // Only one solution is necessary.
}
}
else // k < n
{
for ( i = k; i < n; ++i )
{
Iswap( array, i, k ); // swap array[ i ] and array[ k ]
Permute( _params, array, k + 1, n );
Iswap( array, i, k ); // restore array[ i ] and array[ k ]
if ( abort )
{
goto end; // Abort the recursive process.
}
}
}
end: ;
}// Permute
函数PrintArray,ArrayToSquare和Iswap非常简单,此处不再赘述。现在可以修改Main函数以一次找到3x3和4x4魔方的解。
static void Main( string[] args )
{
/*
SolveSquare( _3x3_params_1 );
SolveSquare( _3x3_params_2 );
SolveSquare( _4x4_params );
*/
Permutations( _3x3_params_1 );
// Permutations( _4x4_params );
}// Main
对于3x3的魔方,程序将产生以下输出(仅显示命令提示符窗口的末尾):
68292: 2 7 6 9 5 1 4 3 8
68292 permutations
N = 3
Magic number = 15
2 7 6
9 5 1
4 3 8
End time: 6:54:52 AM
Start time: 6:52:55 AM
Elapsed time: 0 0:1:56 610 ms
Press any key to continue . . .
经过的时间是1分钟,56秒和610毫秒,这比回溯代码所花费的136毫秒要大得多。请注意,此解决方案是垂直(向上或向下)翻转然后向左水平翻转的回溯解决方案。
使用代码
在Visual Studio中,创建一个名为MagicSquare的Windows控制台应用程序,该应用程序将是Program.cs文件中的命名空间。将本文附带的文件解压缩到某个目录中,然后打开文件Program.cs。在该文件中,突出显示整个MagicSquare名称空间并进行复制。在解决方案的Program.cs文件中,突出显示整个MagicSquare名称空间,然后粘贴复制的代码。生成解决方案并在没有调试的情况下启动它。
注意事项
对于5x5或更高维度的正方形执行回溯代码,对于4x4或更高维度的正方形执行排列代码将花费很长时间,大约几天。该程序在装有AMD A4-5000 APU x64处理器的Toshiba Satellite L75D-A7283笔记本电脑上运行。操作系统是64位Windows 10。