实用算法实现-第 14 篇 启发式搜索

14.1    A*搜索

A*(A star)算法是一种很好的树搜索策略,在许多人工智能的书籍中有所介绍。比如《人工智能,一种现代方法》和《人工智能,复杂问题求解的结构和策略》。

[i]文介绍到,A*算法使用估价函数F(N)来估算从初始状态S到当前状态N,再到目标状态T需要的最小步数。有公式:

F(N) = G(N) + H(N)

其中,G(N)是起始状态S到当前状态的实际代价G(N),它比最佳路径的代价G*(N)大。

H(N)是从当前状态到目标状态T的实际代价的估算值H*(N),它比实际代价H(N)大。

对于一个起始状态到当前状态的实际路径,G(N)是已经决定的。H(N)的选择则至关重要。有两个限制:

1. H(N)一定要小于等于当前结点N至目标结点T的实际代价。

2. 任意结点的F值必须大于其父辈状态的F值,即F单调递增。

当估价函数F满足以上两个限制条件,则启发式搜索算法一定能得出问题的最优解。

14.1.1   实例

PKU JudgeOnline, 1077, Eight.

14.1.2   问题描述

15数码游戏就是完成类似下面的转换的一种游戏:

1  2 3  4    1 2  3  4   1  2  3 4    1  2 3  4

5  6 7  8    5 6  7  8   5  6  7 8    5  6 7  8

9  x 10 12   9 10  x 12    9 10 11 12    9 10 11 12

1314 11 15   13 14 11 15   13 14 x 15   13 14 15  x

           r->           d->           r->

    8数码游戏类似,只不过只有8个数码而已。

    给出8数码游戏的一个初始状态,求达到目标状态的一个解,不要求是最优解。如果没有解,输出“unsolvable”。

14.1.3   输入

2 3  4  1 5  x  7 6  8

1.1.4   输出

ullddrurdllurdruldr

14.1.5   分析

《人工智能,一种现代方法》对使用A*算法解决八数码问题进行了深入的分析。提出了两种启发函数的策略:

1. h1 = 不在位的棋子的总数。

2. h2 = 所有棋子到其目标位置的曼哈顿距离的总和。

并且通过实验,得到h2的性能总是优于h1。

具体实现中,用到最小堆实现的优先队列。

这里还需要用到一个排列到整数的映射方法,以判断一种状态是不是已经遍历过。一个具体的映射方法如下:

把9看成插到8的排列中,那么有9种插法,可以把362880等分成9段,分别代表一个位置,每段递归处理。

令pos(a)为数字a在排列中排在a前面并且小于a的数字的个数,那么有伪代码:

int n=1;

for (int i=1; i<10; i++)

{

     n+=pos(i)*(i-1)!;

}

     下面程序中int calPos1(node *temp)和int calPos(node *temp)就是两种映射方法。

也许[ii]中介绍的排列的“反序”概念,能很好地用来解释上面这个算法。

14.1.6   程序

#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
bool visited[362880];
#define maxNum     10000000
#define MAX        0x7F7F7F7F
struct tile{
     int x;
     int y;
};
#define maxLength 100//貌似最数码游戏的解长度为...
struct node{
     tile pos[3][3];
     int key;
     charop[maxLength];
     int blankX;
     int blankY;
};
nodeminHeap[maxNum];
int heapSize;
void minHeapify(int i)
{
     int l;
     int r;
     int min;
     node temp;
     l = i * 2;
     r = i * 2 + 1;
     min = i;
     if((l <=heapSize)&&
     (minHeap[l].key < minHeap[min].key)){
         min = l;
     }
     if((r <=heapSize)&&
     (minHeap[r].key < minHeap[min].key)){
         min = r;
     }
     if(min !=i)
     {
         temp = minHeap[min];
         minHeap[min] = minHeap[i];
         minHeap[i] = temp;
         minHeapify(min);
     }
}
void buildMinHeap()
{
     int i;
     for(i =heapSize / 2; i > 0; i--){
         minHeapify(i);
     }
}
nodeheapExtractMin()
{
     node min;
     if(heapSize< 1)
     {
         cout << "ERROR:no more" << endl;
     }
     min = minHeap[1];
     minHeap[1] = minHeap[heapSize];
     heapSize--;
     minHeapify(1);
     return min;
}
void heapIncreaseKey(int i, int key)
{
     node temp;
     if(key >minHeap[i].key)
     {
         cout << "ERROR:new key is smaller than current key" << endl;
     }
     minHeap[i].key = key;
     while(i> 1 && minHeap[i/2].key > minHeap[i].key)
     {
         temp = minHeap[i];
         minHeap[i] = minHeap[i/2];
         minHeap[i/2] = temp;
         i = i / 2;
     }
}
void minHeapInsert(node temp)
{
     heapSize++;
     minHeap[heapSize] = temp;
     minHeap[heapSize].key = MAX;
     heapIncreaseKey(heapSize, temp.key);
}
inline int solved(node *temp)
{
     int i, j;
     for(i = 0;i < 3; i++){
         for(j =0; j < 3; j++){
              if((*temp).pos[i][j].y!= i || (*temp).pos[i][j].x != j)
              {
                   return0;
              }
         }
     }
     return 1;
}
int adj[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
char operation[4] = {'l', 'r', 'u', 'd'};
int calKey(node *temp, int blankX2, int blankY2)
{
     int key;
     int x1;
     int y1;
     int x2;
     int y2;
     int blankX1= (*temp).blankX;
     int blankY1= (*temp).blankY;
     key = 1;
     x1 = abs((*temp).pos[blankY1][blankX1].x -blankX1);
     y1 = abs((*temp).pos[blankY1][blankX1].y -blankY1);
     x2 = abs((*temp).pos[blankY2][blankX2].x -blankX2);
     y2 = abs((*temp).pos[blankY2][blankX2].y -blankY2);
     key += - (x1 + y1 + x2 + y2);
     x1 = abs((*temp).pos[blankY2][blankX2].x -blankX1);
     y1 = abs((*temp).pos[blankY2][blankX2].y -blankY1);
     x2 = abs((*temp).pos[blankY1][blankX1].x -blankX2);
     y2 = abs((*temp).pos[blankY1][blankX1].y -blankY2);
     key += x1 + y1 + x2 + y2;
     return key;
}
int factorial[9] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320};
int calPos1(node *temp)
{
     int i;
     int j;
     int x, y;
     int x1, y1;
     int temp1,temp2;
     int num;
     int sum;
     sum = 0;
     for(i = 0;i < 9; i++)
     {
         y = i / 3;
         x = i % 3;
         temp1 = (*temp).pos[y][x].x +(*temp).pos[y][x].y * 3;
         num = 0;
         for(j =0; j < i; j++)
         {
              y1 = j / 3;
              x1 = j % 3;
              temp2 = (*temp).pos[y1][x1].x +(*temp).pos[y1][x1].y * 3;
              if(temp2< temp1)
              {
                   num++;
              }
         }
         sum += (temp1 - num) * factorial[9 - i- 1];
        
     }
     return sum;
}
int calPos(node *temp)
{
    inti,j,k=0,b[9],ret=0,num=0;
    for(i=0;i<3;i++)
        for(j=0;j<3;j++)
            b[k++]=(*temp).pos[i][j].x +(*temp).pos[i][j].y * 3;
    for(i=0;i<9;i++)
    {
        num=0;
        for(j=0;j<i;j++)
            if(b[j]>b[i])  num++;
        ret+=factorial[i]*num;
    }
    return ret;
}
int main()
{
     int i;
     int j;
     int x;
     int y;
     int blankX;
     int blankY;
     char c;
     int length;
     node insertNode;
     node temp;
     int pos;
     bool find;
     insertNode.key = 0;   
     for(i = 0;i < 3; i++){
         for(j =0; j < 3; j++){
              cin >> c;
              if(c== 'x'){
                   insertNode.blankY = i;
                   insertNode.blankX = j;
                   insertNode.pos[i][j].x = 2;
                   insertNode.pos[i][j].y = 2;
              }else{
                   x = c - '1';
                   y = x / 3;
                   x = x % 3;
                   insertNode.pos[i][j].x = x;
                   insertNode.pos[i][j].y = y;
              }
              insertNode.key +=abs(insertNode.pos[i][j].x - j);
              insertNode.key +=abs(insertNode.pos[i][j].y - i);
         }
     }
     heapSize = 0;
     for(i = 0;i < maxLength; i++)
     {
         insertNode.op[i] = '\0';
     }
     memset(visited, 0, sizeof(visited));
     pos = calPos(&insertNode);
     visited[pos] = 1;
     minHeapInsert(insertNode);
     find = 0;
     int count =0;
     while(heapSize> 0){
         if(heapSize> maxNum)
         {
              cout <<endl<< "Too large" << endl;
              return-1;
         }
         temp = heapExtractMin();
         if(solved(&temp)== 1)
         {
              find = 1;
              cout << temp.op <<endl;
              break;
         }
         for(i =0; i < 4; i++){
              blankX = temp.blankX;
              blankY = temp.blankY;
              y = blankY + adj[i][0];
              x = blankX + adj[i][1];
              count++;
              if(x< -1 || x > 3 || y < -1 || y > 3)
              {
                   while(1)
                   {}
              }
              if(x< 0 || y < 0 || x > 2 || y > 2)
              {
                   continue;
              }
              insertNode = temp;
              insertNode.pos[y][x].x =temp.pos[blankY][blankX].x;
              insertNode.pos[y][x].y =temp.pos[blankY][blankX].y;
              insertNode.pos[blankY][blankX].x =temp.pos[y][x].x;
              insertNode.pos[blankY][blankX].y =temp.pos[y][x].y;
              pos = calPos(&insertNode);
              if(visited[pos]!= 0){
                   continue;
              }
              visited[pos] = 1;
              insertNode.key +=calKey(&temp, x, y);
              insertNode.blankX = x;
              insertNode.blankY = y;
              length = strlen(temp.op);
              insertNode.op[length] =operation[i];
              minHeapInsert(insertNode);
         }
     }
     if(find ==0)
     {
         printf("unsolvable\n");
     }
     return 1;
}

14.2    IDA*搜索

A*算法减少存储需求的最简单的方法就是将迭代深入的思想用在启发式搜索上,形成迭代深入A*算法,即IDA*。

使用BFS搜索需要实用一个队列,这个队列如果过大就可能超出内存限制。用DFS代替BFS,可以减少存储需求。

IDA*算法实际上是在迭代加深的深度优先搜索的基础上进行的一种改进。迭代加深的深度优先搜索使用当前搜索深度来判断是否停止搜索,而IDA*算法则使用A*算法的估价函数F(N) = G(N) + H(N)来判断是否停止搜索。可以知道,此时的G(N)就是当前搜索深度,H(N)则是对于从当前结点N至目标结点T的实际代价一种乐观估计。说H(N)乐观,是因为在设计估价函数的时候,H(N)被约束为小于等于实际代价。由此可知,相对于朴素的迭代加深的深度优先搜索,IDA*算法省去了一些不必要的搜索。

14.2.1   实例

PKU JudgeOnline, 2286, The Rotation Game.

14.2.2   问题描述


如上图一种游戏,给定初始状态,通过拉动A到H的,对该行或列进行旋转。目标是使得中间的八个数字相等。

输出字典序最小的最优解的旋转过程,及其最终状态时中间的数字。

14.2.3   输入

11 1 1 3 2 3 2 3 1 3 2 2 3 1 2 2 2 3 1 2 1 3 3

11 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3

0

14.2.4   输出

AC

2

DDHH

2

14.2.5   分析

对搜索的深度给一个步数限制。每次在状态空间里进行DFS搜索的时候,如果遇见当前状态决定了已经不可能在限制内找到解,就返回,而不再搜索其子状态。如果这次DFS没找到,就继续放宽限制,重新对状态空间进行DFS。如此反复直到找到一个解。

显然这个解是最优的解。

确保解字典序最小的办法是DFS中顺次地从A到H中选择子状态。

14.2.6   程序

#include <iostream>
using namespace std;
 
int a[25];
int res[1000];
int ans;
int limit;
 
int state[8][7][2] = {
     { {1,23}, {3,1}, {7,3}, {12,7}, {16,12},{21,16}, {23,21} },
     { {2,24}, {4,2}, {9,4}, {13,9}, {18,13},{22,18}, {24,22} },
     { {11,5}, {10,11}, {9,10}, {8,9}, {7,8},{6,7}, {5,6} },
     { {20,14}, {19,20}, {18,19}, {17,18},{16,17}, {15,16}, {14,15} },
     { {24,2}, {22,24}, {18,22}, {13,18},{9,13}, {4,9}, {2,4} },
     { {23,1}, {21,23}, {16,21}, {12,16},{7,12}, {3,7}, {1,3} },
     { {14,20}, {15,14}, {16,15}, {17,16},{18,17}, {19,18}, {20,19} },
     { {5,11}, {6,5}, {7,6}, {8,7}, {9,8},{10,9}, {11,10} }
 
};
 
 
int isSuccess()
{
     int ll =a[7];
     if(a[8]!=ll|| a[9]!=ll || a[12]!=ll || a[13]!=ll ||
         a[16]!=ll || a[17]!=ll || a[18]!=ll)
         return0;
     return ans= ll;
}
 
int getval()
{
     int i;
     int cnt[4];
     memset(cnt,0,sizeof(cnt));
     for(i = 7;i <=9 ;i++)cnt[a[i]]++;
     for(i = 12;i <=13 ;i++)cnt[a[i]]++;
     for(i = 16;i <=18 ;i++)cnt[a[i]]++;
     return8-max(cnt[1],max(cnt[2],cnt[3]));
}
 
int dfs(int t)
{
     inttemp[25];
     if(t==limit)
         returnisSuccess();
     if(t +getval() > limit)return 0;
     for(int i = 0 ; i < 8 ; i++)
     {
         res[t] = i;
         memcpy(temp,a,sizeof(a));
         for(int j = 0 ; j < 7 ; j++)
              a[state[i][j][1]] =temp[state[i][j][0]];
         int ret= dfs(t+1);
         memcpy(a,temp,sizeof(temp));
         if(ret!=0)return ret;
     }
     return 0;
}
 
 
int main()
{
     while(true)
     {
         scanf("%d",&a[1]);
         if(!a[1])
              break;
         for(int i = 2; i <=24; i++)
              scanf("%d",&a[i]);
         if(isSuccess())
         {
              cout<<"No moves needed"<<endl<<a[7]<<endl;
              continue;
         }
         limit = 1;
         while(dfs(0)== 0)limit++;
         for(int i = 0 ; i < limit ;i++)putchar((char)(res[i]+'A'));
         printf("\n%d\n",ans);
     }
     system("pause");
     return 0;
}
本文章欢迎转载,请保留原始博客链接http://blog.csdn.net/fsdev/article


[i] 实用算法的分析与程序设计。吴文虎,王建德。

[ii] The Art of Computer Programming, Volume 3, Sorting and Searching.Donald E. Knuth.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值