最近研究了马踏棋盘的问题,分享一下心得。
问题描述:将国际象棋的骑士放置任意位置,使其按规则不重复走完所有的棋格。
该问题的实质应该是:哈密顿路径的遍历。
首先画图分析一下具体环境:
如图所示,每个棋格有且仅有八个前进方向,标记为“1…8”,每个方向不一定合法(超出棋盘)
这时可以用递归回溯的思想对路径进行探索,思路是:
从方向1开始探索路径,一条路走到黑,发现走不了就回头走其他的路
再具体点的思路就是:
for i →1 to 8 //依次对八个方向进行探寻
if travel finish return //如果探索完毕返回
position→ new positon //获取新的棋格x,为原来棋格的儿子棋格
if position legal && new //如果新棋格x没超出棋盘 并且没有被探索过
travel(position) //对新棋格x进行探索
position reset //从探索中回到棋格x中
travel time reset //探索次数重置
更具体的可行性代码如下(第一次写,很随意很垃圾):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 马塔棋盘
{
class Program
{
static int tag = 1;
static int maxtri = 7;
//static int tempx, tempy;
static int[,] way = new int[8,8];
static void Main(string[] args)
{
//int x = int.Parse(Console.ReadLine());
//int y = int.Parse(Console.ReadLine());
way[0,0] = tag;
travel(0,0);
//foreach (var item in way)
//{
// Console.Write(item + "\t");
//}
Console.ReadKey();
}
public static void nextStep(int step,ref int x,ref int y)//棋格前进的方向
{
switch (step)
{
case (0):
{
x += 1;
y += 2;
break;
}
case (1):
{
x += 2;
y += 1;
break;
}
case (2):
{
x += 2;
y += -1;
break;
}
case (3):
{
x += 1;
y += -2;
break;
}
case (4):
{
x += -1;
y += -2;
break;
}
case (5):
{
x += -2;
y += -1;
break;
}
case (6):
{
x += -2;
y += 1;
break;
}
case (7):
{
x += -1;
y += 2;
break;
}
default:
break;
}
}
public static void travel(int x, int y)
{
if (tag>=64)//探索路径完毕,进行打印输出
{
Console.WriteLine("travel is finished");
int count = 0;
foreach (var item in way)
{
if (count==8)
{
count = 0;
Console.WriteLine();
}
Console.Write(item + "\t");
count++;
}
return;
}
for (int i = 0; i <= maxtri; i++)
{
//tempx = x; tempy = y;
int a = x; int b = y;
nextStep(i, ref a, ref b);// Console.WriteLine("{0},{1},{2}", i, x, y);
if ((a <= maxtri&&a>=0) && (b <= maxtri&&b>=0))//判断新的路径是否合法
{
//Console.WriteLine("{0},{1},{2}", i, x, y);
if ((way[a, b] == 0))
{
tag++;
way[a, b] = tag;//棋格x标记已探索,探索第几步
travel(a, b);//对棋格x进行更新一步探索
tag--;//对棋格x探索完成后,不管成功还是失败,把探索位重置
way[a, b] = 0;//对棋格x探索完成后,不管成功还是失败,把探索位重置
}
}
}
}
}
}
运行结果:
然后发现这种写法很垃圾,很多问题…
缺点1:跑得慢 ,要跑好几秒才能跑出来
缺点2:某些点很难跑出结果 ,比如顶点(0,0)跑得比较快,(2,2)就跑不出来
缺点3:肯定有,不过阿拉已经对它没兴趣了
仔细分析下,发现这所谓的递归回溯其实就是高级一点的穷举法
穷举法要跑8的64次方种可能出来
递归回溯只是添加了一个策略,“不能跑就不跑了” 判断依据是——棋格是否非法
想要更短的响应时间,应该添加更严格的判断策略–贪心策略
在实际生活中有很多贪心策略的应用,比如:
(1)家里有一堆快过期的大米 ,有一堆 新的大米,为了不浪费大米,先把快过期的大米吃完再吃新的大米,这样就可以吃到所有的大米了
(2)商贩找零钱,先50 再20 10块这样…
阿拉认为贪心策略应该是专注在最恶劣条件或最优越条件中取值。
本题适用最恶劣条件取值。思路:
比较棋格的孙子棋格,数量最少的孙子棋格的方向最先进行探索。
为什么最少的优先,因为最少的通路说明它很容易“死掉”,越到后面越容易找不到。
整体思路在原来的框架下加多一个最优选择方向的策略就可以了
for i →1 to 8 //依次对八个方向进行探寻
if travel finish return //如果探索完毕返回
position→ new positon //**依照贪心策略**获取新的棋格x,为原来棋格的儿子棋格
if position legal && new //如果新棋格x没超出棋盘 并且没有被探索过
travel(position) //对新棋格x进行探索
position reset //从探索中回到棋格x中
travel time reset //探索次数重置
怎么实行这个策略,其实只要添加相应的函数getDirection(),为每个棋格的八个方向进行排序,孙子越少搜索优先级越高
这时需要一些额外的数据存储结构
waycount[x,y] // 记录坐标为x,y的棋格可通路的儿子棋格数目
dir[x,y,i] //记录坐标为x,y的棋格的方向(有8个方向,i取值1~8,i越小,搜索优先级越高
for i 1→8
temp[i] = way[x,y] //记录坐标为x,y的棋格的儿子棋格数
sort(temp,dir[,,]// 按照结点少优先排序原则进行排序
具体代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 骑士周游列国__贪心算法
{
class Program
{
static int row = 8;
static int column = 8;
static int step = 1;//步调
static int no = 0; //方案数目
static int[,] board = new int[row,column];
static int[,] waycount = new int[row, column];//保存每个棋位联通的子棋位个数
static int[, ,] direction = new int[row, column, 8];//保存每个棋位最优前进的方向
static int[,] destination ={{1,2},{2,1},{2,-1},{1,-2},{-1,-2},{-2,-1},{-2,1},{-1,2}};//保存每个棋位的方向
static void Main(string[] args)
{
getDirection();
board[0, 0] = 1;
travel(0, 0);
Console.ReadKey();
}
public static void travel(int x, int y)//棋子周游列国
{
for (int i = 0; i < 8; i++)
{
if (step==row*column)
{
print();
no++;
Console.ReadKey();
return;
}
int x1 = x + destination[direction[x,y,i], 0];
int y1 = y + destination[direction[x,y,i], 1];
if (check(x1, y1))
{
if (board[x1,y1] == 0)
{
board[x1, y1] = ++step;
travel(x1, y1);
--step;
board[x1, y1] = 0;
}
}
}
}//棋子周游列国
public static void getDirection()//得到每个棋位的方向 ,在调用travel函数之前一定要调用,可以在awake()函数里调用
{
getWayCount();//得到每个棋位的通路数,这个可以放在star()函数里面
int[] count = new int[8];//保存每个棋位的孙棋位个数
for (int x = 0; x < row; x++)
{
for (int y = 0; y < column; y++)
{
for (int i = 0; i < 8; i++)
{
int x1 = x + destination[i, 0];
int y1 = y + destination[i, 1];
if (check(x1,y1))
{
count[i] = waycount[x1, y1];
}
else
{
count[i] = 100;
}
}
for (int i = 0; i < 8; i++)
{
direction[x, y, i] = getMin(count, 0);
}
}
}
}
public static void getWayCount()//得到每个棋位的子通路数 这个函数只能运行一次,每运行多一次,waycount的值翻一倍 目前在getDirection()中被调用
{
for (int x = 0; x < row; x++)
{
for (int y = 0; y < column; y++)
{
for (int i = 0; i < 8; i++)
{
int x1 = x + destination[i, 0];
int y1 = y + destination[i, 1];
if (check(x1,y1))
{
waycount[x, y]++;
}
}
}
}
}
public static int getMin(int[] array, int j)//取得数组最小值
{
int min = 999;
int minNo = j;
for (int i = j; i < array.Length; i++)
{
if (array[i] <= min)
{
min = array[i];
minNo = i;
}
}
array[minNo] = 999;
return minNo;
}
public static bool check(int x, int y)//检查棋子是否超出棋盘边界
{
if ((x <= row - 1 && x >= 0) && (y <= column-1 && y >= 0))
return true;
return false;
}
public static void print()//打印周游列国踪迹
{
Console.WriteLine("The travel is finished!");
Console.WriteLine("Plan {0} is :", no);
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
Console.Write(board[i, j]+"\t");
}
Console.WriteLine();
}
}
}
}
运行结果: