暴力求解-路径寻找

在此之前介绍过图的遍历,很多问题可以归结为图的遍历,但这些问题中的图却不是事先给定的、从程序读入的,而是由程序动态生成的,称为隐式图。本节和前面介绍的回溯法不同,回溯法一般是要找一个(或者所有)满足约束的解(或者某种意义下的最优解),而状态空间搜索一般是要找到一个从初始状态到终止状态的路径

八数码问题。编号为1-8的8个正方形滑块被摆成3行3列(有一个格子留空),如图所示。每次可以把与空格相邻的滑块(有公共边才算相邻)移到空格中,而它原来的位置就成为了新的空格。给定初始局面和目标局面(用0表示空格),你的任务是计算出最少的移动步数。如果无法到达目标局面,则输出-1。

图片

样例输入:

2 8 3 1 6 4 7 0 5

1 2 3 8 0 4 7 6 5

样例输出:

5

运行结果:

暴力求解-路径寻找_第1张图片

不难把八数码问题归结为图上的最短路问题,图中的结点就是9个格子中的滑块编号(从上到下,从左到右把它们放到一个包含9个元素的数组中)。无权图上的最短路问题可以用BFS求解。

首先是辅助宏的定义:

typedef int Status[9]; //定义状态类型
const int maxstatus=1000000;
Status st[maxstatus],goal; //状态数组,所有状态保存在这里
int dist[maxstatus];
const int dx[4]={-1,1,0,0};
const int dy[4]={0,0,-1,1};

核心代码:

int bfs()
{
    //BFS,返回目标状态在st数组下标
    init_lookup_table(); //初始化查找表
    int front=1,rear=2,i,x,y,z,newx,newy,newz; //不用下标0,因为0被当做不存在
    while(front

注意,此处用到了cstring中的memcmp和memcpy完成整块内存的比较和复制,比用循环比较和循环赋值快。

主程序很容易实现:

int main()
{
    int i;
    for(i=0;i<9;i++)
        scanf("%d",&st[1][i]); //起始状态
    for(i=0;i<9;i++)
        scanf("%d",&goal[i]); //目标状态
    int ans=bfs();
    if(ans>0)
        printf("%d\n",dist[ans]);
    else
        printf("-1\n");
}

注意,应在调用bfs函数之前设置好st[1]和goal。上面的代码几乎是完整的,唯一没有涉及的是init_lookup_table()和try_to_insert(rear)的实现。为什么会有这两项呢?还记得BFS中的判重操作么?在DFS中可以检查idx来判断结点是否已经访问过:在求最短路的BFS中用d值是否为-1来判断结点是否访问过,不管用哪种方法,作用是相同的:比米娜同一个结点访问多次。树的BFS不需要判重,因为根本不会重复:但对于图来说,如果不判重,时间和空间都将产生极大的浪费。

如何判重呢?难道要声明一个9维数组vis 然后执行 if(vis[s[0][s[1]][s[2]]...[s[8]])?无论程序好不好看,9维数组的每维都要包含9个元素,一

共有9^9=387420489项,太多了,数组开不下。实际的节点数并没有这么多,0-8的排列总共只有9!=362880个,为什么9维数组开不下呢?原因在于,这样的用法存在大量的浪费-数组中有很多项都没有被用到,但却占据了空间。

一种方法是:把排列变成整数,然后只开一个一维数组,也就是说,设计一套排列的编码和解码函数,把0-8的全排列和0-362879的整数一一对应起来。原理很巧妙,时间效率也很高,但编码解码法的适用范围并不大:如果隐式图的总结点数非常大,编码也会很大,数组还是开不下。

还有一种方法是用STL集合t。把状态转化为9为十进制整数,就可以用set判重了。使用STL集合的代码最简单,但时间效率也最低(若此时不用-O2优化则速度劣势更加明显)。

我们来重点介绍一下使用哈希(Hash)技术。哈希表的执行效率高,适用范围也很广。

简单的说,就是要把结点变成整数,但不必是一一对应,换句话说,只需要设计一个所谓的哈希函数h(x),然后将任意结点x映射到某个给定范围[0,M-1]的整数接口,其中M是程序员根据可用内存大小自选的。在理想情况下,只需开一个大小为M的数组就能完成判重,但此时往往会有不同结点的哈希值相同,因此需要把哈希值相同的的状态组织成立链表。

const int hashsize=1000003;
int next[maxstatus],head[hashsize]; //距离数组

bool legal(int x,int y) //是否合法
{
    return x>=0&&x<3&&y>=0&&y<3;
}

void init_lookup_table()
{
    memset(head,0,sizeof(head));
}

int hash(Status s)
{
    //确保hash函数值是不超过hash表大小的非负整数
    int i,v=0;
    for(i=0;i<9;i++)
      v=v*10+s[i];
    return v%hashsize;
}

bool try_to_insert(int rear)
{
    //是否重复
    //其中head为链表表头,链表中保存的是相同Hash值的元素在st中的位置。
    int u,h;
    h=hash(st[rear]);
    u=head[h]; //从表头开始查找链表
    while(u)
    {
        if(memcmp(st[u],st[rear],sizeof(st[rear]))==0) //找到了,查找失败
            return false;
        u=next[u]; //顺着链表继续找
    }
    next[rear]=head[h]; //插入到链表中
    head[h]=rear;
    return true;
}

除了BFS中的结点判重外,还可以用到其他需要快速查找的地方。不过需要注意的是:哈希表中,对效率起到关键作用的是哈希函数。如果哈希函数选取得当,几乎不会有结点的哈希值相同,且此时链表查找的速度也较快;但如果冲突严重,整个哈希表会退化成少数几条长长的链表看,查找的速度将非常缓慢。

某些特定的STL实现中还有hash_set,它正式基于前面的哈希表,但它并不是标准C++的一部分,因此不是所有情况下都可用。

你可能感兴趣的:(算法)