原题网址:http://hihocoder.com/problemset/problem/1312

时间限制:10000ms

单点时限:1000ms

内存限制:256MB

 

描述

在小Ho的手机上有一款叫做八数码的游戏,小Ho在坐车或者等人的时候经常使用这个游戏来打发时间。

游戏的棋盘被分割成3x3的区域,上面放着标记有1~8八个数字的方形棋子,剩下一个区域为空。

游戏过程中,小Ho只能移动棋子到相邻的空区域上。当小Ho将8个棋子都移动到如下图所示的位置时,游戏就结束了。

小Hi:小Ho,你觉得如果用计算机来玩这个游戏应该怎么做?

小Ho:用计算机来玩么?我觉得应该是搜索吧,让我想一想。

提示:启发式搜索

输入

第1行:1个正整数t,表示数据组数。1≤t≤8。

接下来有t组数据,每组数据有3行,每行3个整数,包含0~8,每个数字只出现一次,其中0表示空位。

输出

第1..t行:每行1个整数,表示该组数据解的步数。若无解输出"No Solution!"

样例输入

3

1 2 3

4 5 6

7 8 0

1 2 3

4 5 6

8 7 0

8 0 1

5 7 4

3 6 2

样例输出

0

No Solution!

25

提示:启发式搜索

小Ho:这个问题和上一次一样嘛,用宽度优先搜索来求解。

然后把这个3x3的二维数组拉伸成一个长度为9的数组,将长度为9的数组作为状态。

那么最终状态就是{1,2,3,4,5,6,7,8,0}。

由于每一个位置的有9种可能,所以我建立一个9维数组来判重进行搜索就好了。

小Hi:9维数组,每一维的大小为9。小Ho,你确定这不会超过内存限制么?

小Ho:9的9次方等于387420489,好像是挺大的。不过应该没问题吧。

小Hi:怎么可能没问题!这个数据已经很大了好么!

小Ho:那该怎么办啊?

小Hi:小Ho,你仔细观察题目的状态。由于每个数字一定只会出现一次,每个状态对应的恰好是1~9的一个排列。

那么1~9的全排列有多少种呢?

小Ho:这个我知道,是9!,一共362880种。

小Hi:没错,总共只有不到40万种不同的情况。如果我们能够使用一个方法来表示不同排列的状态,那么是不是就可以把判重的状态数量压缩到40万以内了呢?

小Ho:恩,没错。但是有什么好的方法么?

小Hi:当然有啦,这里我们需要用的事全排列的知识。小Ho你知道全排列是有顺序的么?

小Ho:恩,知道。比如3个数的全排列,按顺序就是:

123, 132, 213, 231, 312, 321

小Hi:没错,那么第二个问题:假如我给你一个全排列,你能计算出它是第几个排列么?

小Ho:(⊙v⊙),这个我不知道。

小Hi:我就知道你不知道,让我来告诉你吧。这里我们需要用到一个叫做康托展开的方法。

对于一个长度为n的排列num[1..n],其序号X为:

X = a[1]*(n-1)!+a[2]*(n-2)!+...+a[i]*(n-i)!+...+a[n-1]*1!+a[n]*0!
其中a[i]表示在num[i+1..n]中比num[i]小的数的数量

举个例子,比如213:

num[] = {2, 1, 3}
a[] = {1, 0, 0}
X = 1 * 2! + 0 * 1! + 0 * 1! = 2

我们如果将3的全排列从0开始编号,2号对应的正是213。

其写做伪代码为:

Cantor(num[])
        X = 0
        For i = 1 .. n
               tp = 0
               For j = i + 1 .. n
                       If (num[j] < num[i]) Then
                               tp = tp + 1
                       End If
               End For
               X = X + tp * (n - i)!
        End For
        Return X

那么接下来,第三个问题!

小Ho:你说吧!

小Hi:已知X,如何去反向求解出全排列?

小Ho:我觉得应该还是从康托展开的公式入手。

< 小Ho拿出草稿纸,在上面推算了一会儿 >

根据X的求值公式,可以推断出对于a[i]来说,其值一定小于等于n-i。那么有:

a[i]≤n-i, a[i]*(n-i)!≤(n-i)*(n-i)!<(n-i+1)!

也就是说,对于a[i]来说,无论a[i+1..n]的值为多少,其后面的和都不会超过(n-i)!

那么也就是说,如果我用X除以(n-1)!,得到商c和余数r。其中c就等于a[1],r则等于后面的部分。

这样依次求解,就可以得到a[]数组了!

比如求解3的全排列中,编号为3的排列:

3 / 2! = 1 ... 1 => a[1] = 1
1 / 1! = 1 ... 0 => a[2] = 1
0 / 0! = 0 ... 0 => a[3] = 0

然后就是根据a[]来求解num[],让我想一想。

...

我知道了!由于a[i]表示的是num[i+1..n]中比num[i]还小的数字。

那么只需要从num[1]开始,依次从尚未使用的数字中选取第a[i]+1小的数字填入就可以了!

紧接着上面的例子:

a[] = {1, 1, 0}
unused = {1, 2, 3}, a[1] = 1, num[1] = 2
unused = {1, 3}, a[2] = 1, num[2] = 3
unused = {1}, a[3] = 0, num[3] = 1
=> 2, 3, 1

231也确实是3的全排列中编号为3的排列。

小Hi:小Ho,你真棒!你使用的这个方法也被称为逆康托展开,写作代码的话:

unCantor(X):
        a = []
        num = []
        used = [] // 长度为n的boolean数组,初始为false
        For i = 1 .. n
               a[i] = X / (n - i)!
               X = X mod (n - i)!
               cnt = 0
               For j = 1 .. n
                       If (used[j]) Then
                               cnt = cnt + 1
                               If (cnt == a[i] + 1) Then
                                      num[i] = j
                                      used[j] = true
                                      Break
                               End If
                       End If
               End For
        End For
        Return num

通过康托展开以及康托逆展开,我们就将该问题的状态空间压缩到了9!,在空间复杂度上得到了优化。

小Ho:那么这次的问题不就解决了!

小Hi:远远没那么简单哦,其实这个问题还有一个时间上的优化。

小Ho:但是宽度优先搜索不就是最快寻找到解的方法了么?还有更好的方法么?

小Hi:当然有了,我们有一种叫做启发式搜索的方法。

在启发式搜索的过程中,不再是一定按照步数最优的顺序来搜索。

首先在启发式搜索中,我们每次找到当前“最有希望是最短路径”的状态进行扩展。对于每个状态的我们用函数F来估计它是否有希望。F包含两个部分:

F = G + H

G:就是普通宽度优先搜索中的从起始状态到当前状态的代价,比如在这次的问题中,G就等于从起始状态到当前状态的最少步数。

H:是一个估计的值,表示从当前状态到目标状态估计的代价(步数)。

H是由我们自己设计的,H函数设计的好坏决定了A*算法的效率。H值越大,算法运行越快。

但是在设计评估函数时,需要注意一个很重要的性质:评估函数的值一定要小于等于实际当前状态到目标状态的代价(步数)

否则虽然你的程序运行速度加快,但是可能在搜索过程中漏掉了最优解。相对的,只要评估函数的值小于等于实际当前状态到目标状态的代价,就一定能找到最优解

在这个问题中可以表述为:评估函数得到的从当前状态到目标的状态需要行动的步数一定不能超过实际上需要行动的步数。

所以,我们可以将评估函数设定为:1-8八数字当前位置到目标位置的曼哈顿距离之和。(为什么这样设计留给读者思考。当然也有其他符合条件的估计函数,不同估计函数效率如何也留给读者自行比较。)

F:评估值和状态值的总和。

同时在启发式搜索中将原来的一个队列变成了两个队列:openlist和closelist。

在openlist中的状态,其F值还可能发生变化。而在closelist中的状态,其F值一定不会再发生改变。

整个搜索解的流程变为:

  1. 计算初始状态的F值,并将其加入openlist
  2. 从openlist中取出F值最小的状态u,并将u加入closelist。若u为目标状态,结束搜索;
  3. 对u进行扩展,假设其扩展的状态为v:若v未出现过,计算v的f值并加入openlist;若v在openlist中,更新v的F值,取较小的一个;若v在closelist中,抛弃该状态。
  4. 若openlist为空,结束搜索。否则回到2。

利用这个方法可以避免搜索一些明显会远离目标状态的状态,从而缩小搜索空间,早一步搜索到目标结果。

在启发式搜索中,最重要的是评估函数的选取,一个好的评估函数能够更快的趋近于目标状态。

将上述过程写做伪代码为:

search(status):
        start.status = status
        start.g = 0    // 实际步数
        start.h = evaluate(start.status)
        start.f = start.g + start.h
        
        openlist.insert(start)
        
        While (!openlist.isEmpty()) 
               u = openlist.getMinFStatus()
               closelist.insert(u)
               For v is u.neighborStatus
                       If (v in openlist) Then
                               // 更新v的f值
                               If (v.f > v.h + u.g + 1) Then
                                      v.f = v.h + u.g + 1
                               End If
                       Else If (v in closelist)
                               continue
                       Else 
                               v.g = u.g + 1
                               v.h = evaluate(v.status)
                               v.f = v.g + v.h
                               openlist.insert(v)
                       End If
               End For
        End While

其中openlist.getMinFStatus()可以使用堆来实现。

启发式搜索在某些情况下并不一定好用,一方面取决于评估函数的选取,另一个方面由于在选取状态时也会有额外的开销。而快速趋近目标结果所减少的时间,能否弥补这一部分开销也是非常关键的。

所以根据题目选取合适的搜索方法才是最重要的。

提示已经讲得很全面了。

一个方面是空间上的优化,用康托展开逆康托展开来表示八数码的状态。

另一方面是时间上的优化,用启发式搜索(A*),其中评估函数设定为1-8八数字当前位置到目标位置的曼哈顿距离之和。

#include <algorithm>
#include <cstring>
#include <string.h>
#include <iostream>
#include <list>
#include <map>
#include <set>
#include <stack>
#include <string>
#include <utility>
#include <queue>
#include <vector>
#include <cstdio>
#include <cmath> #define LL long long
#define N 100005
#define INF 0x3ffffff using namespace std; int factor[]={,,,,,,,,};
vector<int>stnum; //八数码某个状态序列
int destination; //目标状态康托展开值 struct state
{
int f; //评估值和状态值的总和。
int g; //从起始状态到当前状态的最少步数
int h; //评估函数
int k; //该状态康托展开值
state(int f,int g,int h,int k):f(f),g(g),h(h),k(k){};
friend bool operator<(state a,state b)
{
if(a.f!=b.f)
return a.f>b.f;
else return a.g>b.g;
}
}; int cantor(vector<int>num) //康托展开
{
int k = ;
int n = ;
for(int i=;i<n;i++) {
int tp = ;
for(int j = i+; j < n; j++) {
if(num[j] < num[i]) {
tp++;
}
}
k += tp * factor[n--i];
}
return k;
} vector<int> recantor(int k) //逆康托展开
{
vector<int>num;
int a[];
int n = ;
bool used[];
memset(used,false,sizeof(used));
for(int i=;i<n;i++){
a[i] = k / factor[n--i];
k %= factor[n--i];
int cnt = ;
for(int j=;j<n;j++){
if(!used[j]) {
cnt++;
if(a[i] + ==cnt) {
num.push_back(j);
used[j] = true;
break;
}
}
}
}
return num;
} int pos[]={,,,,,,,,}; int getdis(int a,int b) //曼哈顿距离
{
return (abs(a/-b/)+abs(a%-b%));
} int get_evaluation(vector<int>num) //评估函数
{
//评估函数设定为1-8八数字当前位置到目标位置的曼哈顿距离之和。
int h=;
for(int i=;i<;i++)
{
h+=getdis(i,pos[num[i]]);
}
return h;
} void solve()
{
priority_queue<state>openlist;
set<int>closelist; //在closelist中,抛弃该状态。
while(!openlist.empty()) openlist.pop();
closelist.clear(); int h=get_evaluation(stnum);
openlist.push(state(h,,h,cantor(stnum)));
int step=; //统计步数
bool flag=false; //标记是否能找到目标状态
while(!openlist.empty())
{
state cur=openlist.top(); //从openlist中取出F值最小的状态
openlist.pop();
int k=cur.k;
closelist.insert(k); //将该状态加入closelist
if(destination==k) {//若该状态为目标状态,结束搜索
flag=true; //找到目标状态
step=cur.g; //步数
break;
}
vector<int> st = recantor(k); //当前状态的八数码序列
for(int i=;i<;i++) if(st[i]==) //找到0的位置
{
if(i%!=) { //不在行末,0位可以和右边相邻位置交换
swap(st[i],st[i+]);
int x = cantor(st);
int g = cur.g+;
int h = get_evaluation(st);
int f = g + h;
if(closelist.find(x)==closelist.end()) {
openlist.push(state(f,g,h,x));
}
swap(st[i],st[i+]);
}
if(i%!=) { //不在某行开头,可以和左边相邻位置交换
swap(st[i],st[i-]);
int x = cantor(st);
int g = cur.g+;
int h = get_evaluation(st);
int f = g + h;
if(closelist.find(x)==closelist.end()) {
openlist.push(state(f,g,h,x));
}
swap(st[i],st[i-]);
}
if(i<) {
swap(st[i],st[i+]);
int x = cantor(st);
int g = cur.g+;
int h = get_evaluation(st);
int f = g + h;
if(closelist.find(x)==closelist.end()) {
openlist.push(state(f,g,h,x));
}
swap(st[i],st[i+]);
}
if(i>) {
swap(st[i],st[i-]);
int x = cantor(st);
int g = cur.g+;
int h = get_evaluation(st);
int f = g + h;
if(closelist.find(x)==closelist.end()) {
openlist.push(state(f,g,h,x));
}
swap(st[i],st[i-]);
}
}
}
if(!flag) cout << "No Solution!" << endl;
else cout<<step<<endl; } int main() {
vector <int> des;
for(int i=;i<;i++) des.push_back(i+);
des.push_back();
destination=cantor(des);
int T;
cin >> T;
while(T--)
{
int a;
stnum.clear();
for(int i=;i<;i++) {cin>>a; stnum.push_back(a);}
solve();
}
return ;
}

hihoCoder #1312 : 搜索三·启发式搜索(A*, 康托展开)的更多相关文章

  1. hihoCoder 1312:搜索三·启发式搜索(A* + 康托展开)

    题目链接 题意 中文题意 思路 做这题的前置技能学习 康托展开 这个东西我认为就是在排列组合问题上的Hash算法,可以压缩空间. A*搜索. 这里我使用了像k短路一样的做法,从最终状态倒回去预处理一遍 ...

  2. 【hihocoder 1312】搜索三·启发式搜索(启发式搜索写法)

    [题目链接]:http://hihocoder.com/problemset/problem/1312?sid=1092363 [题意] [题解] 定义一个A*函数 f = step+val 这里的v ...

  3. 【hihocoder 1312】搜索三·启发式搜索(普通广搜做法)

    [题目链接]:http://hihocoder.com/problemset/problem/1312?sid=1092352 [题意] [题解] 从末状态的123456780开始逆向搜; 看它能到达 ...

  4. hdu 1430(BFS+康托展开+映射+输出路径)

    魔板 Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submiss ...

  5. 康托展开:对全排列的HASH和还原,判断搜索中的某个排列是否出现过

    题目:http://acm.hrbust.edu.cn/index.php?m=ProblemSet&a=showProblem&problem_id=2297 前置技能:(千万注意是 ...

  6. 双向广搜+hash+康托展开 codevs 1225 八数码难题

    codevs 1225 八数码难题  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 钻石 Diamond   题目描述 Description Yours和zero在研究A*启 ...

  7. HDU 1430 魔板(康托展开+BFS+预处理)

    魔板 Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submis ...

  8. hdu1430魔板(BFS+康托展开)

    做这题先看:http://blog.csdn.net/u010372095/article/details/9904497 Problem Description 在魔方风靡全球之后不久,Rubik先 ...

  9. poj1077(康托展开+bfs+记忆路径)

    题意:就是说,给出一个三行三列的数组,其中元素为1--8和x,例如: 1 2 3 现在,需要你把它变成:1 2 3 要的最少步数的移动方案.可以右移r,左移l,上移u,下移dx 4 6 4 5 67 ...

随机推荐

  1. 在React组件unmounted之后setState的报错处理

    最近在做项目的时候遇到一个问题,在 react 组件 unmounted 之后 setState 会报错.我们先来看个例子, 重现一下问题: class Welcome extends Compone ...

  2. Features (OCMock 2)

    This page describes the features present in OCMock 2.x, using the traditional syntax. All these feat ...

  3. php安装扩展步骤(redis)

    星哥让装一个扩展,解决PDF抓PNG的问题,功能没有实现,有点小悲伤,但是还是学到点东西的. php安装扩展步骤(以redis为例) 前提注意:在自己的LINUX本机上一定要安装有redis软件,我之 ...

  4. osgconv使用指南(转)

    osgconv是一种用来读取3D数据库以及对它们实施一些简单的操作的实用应用程序,同时也被称作 一种专用3D数据库工具. 用osgconv把其他格式的文件转换为OSG所支持的格式 osgconv是一种 ...

  5. Elasticsearch教程(九) elasticsearch 查询数据 | 分页查询

    Elasticsearch  的查询很灵活,并且有Filter,有分组功能,还有ScriptFilter等等,所以很强大.下面上代码: 一个简单的查询,返回一个List<对象> ..    ...

  6. block传值以及利用block封装一个网络请求类

    1.block在俩个UIViewController间传值 近期刚学了几招block 的高级使用方法,事实上就是利用block语法在俩个UIViewController之间传值,在这里分享给刚開始学习 ...

  7. 开发ionic准备之安卓模拟器设置(2)

    发现这个安卓模拟器设置屏幕还不能太大,太大显示不全,然后整个模拟器不能拖动,所以尽量不要设置太大的分辨率 ,如下即可 如果选安卓4.4然后勾选了其他下面的ok还不能点击的话,这下要去sdk manag ...

  8. Android 软键盘的监听(监听高度,是否显示)

    Android官方本身没有提供一共好的方法来对软键盘进行监听,但我们实际应用时.非常多地方都须要针对软键盘来对UI进行一些优化. 下面是整理出来的一个不错的方法.大家能够使用. public clas ...

  9. 谁动了我的cpu——oprofile使用札记(转)

    引言 cpu无端占用高?应用程序响应慢?苦于没有分析的工具? oprofile利用cpu硬件层面提供的性能计数器(performance counter),通过计数采样,帮助我们从进程.函数.代码层面 ...

  10. crm使用soap取消用户訪问记录权限

    //取消訪问权限 function demo() {     //操作记录的id     var targetId = "A8A46444-BA10-E411-8A04-00155D002F ...