回溯:解魔方

目录

介绍

魔方

解决魔方的一个天真的方法:排列

C#程序通过回溯解决魔方

通过生排列换来解决魔方

使用代码

注意事项


介绍

回溯是一种编程技术,用于解决没有已知解决方案的问题。已知解问题的一个例子是计算从1开始的前N个整数的和。它很容易实现为:

int sum = 0;

for ( int i = 1; i <= N; ++i )
{
   sum += i;
}

当然,一种计算总和的更有效方法是使用公式N * (N + 1) / 2,可以通过对N进行归纳证明。

魔方

魔方是N x N的数字数组,范围为12... N 2,这样数组的每个元素都包含一个唯一的数字(无重复),以及每一行,每一列,每一条主对角线的和都是一样的。下图取自Wikipedia,显示了一个3 x 3的魔方,总和等于15

该总和称为幻数。这是一个尚无现成解决方案的问题,必须通过反复试验解决。

解决魔方的一个天真的方法:排列

解决魔方的一种可能方法是针对N x N平方生成范围为12...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,一般过程是将数字12...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

作为两个示例,函数HorizontalCheckDiagonal_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

函数NextPositionPreviousPosition,由函数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

函数PrintArrayArrayToSquareIswap非常简单,此处不再赘述。现在可以修改Main函数以一次找到3x34x4魔方的解。

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中,创建一个名为MagicSquareWindows控制台应用程序,该应用程序将是Program.cs文件中的命名空间。将本文附带的文件解压缩到某个目录中,然后打开文件Program.cs。在该文件中,突出显示整个MagicSquare名称空间并进行复制。在解决方案的Program.cs文件中,突出显示整个MagicSquare名称空间,然后粘贴复制的代码。生成解决方案并在没有调试的情况下启动它。

注意事项

对于5x5或更高维度的正方形执行回溯代码,对于4x4或更高维度的正方形执行排列代码将花费很长时间,大约几天。该程序在装有AMD A4-5000 APU x64处理器的Toshiba Satellite L75D-A7283笔记本电脑上运行。操作系统是64Windows 10

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值