先看一下题目啊,caioj的题目,所以的话只能直接看,要是可以找到其他网站的原题的话当然最好
1420: [视频]【状态压缩】滑块
时间限制: 1 Sec 内存限制: 64 MB
提交: 317 解决: 102
[提交] [状态] [讨论版] [命题人:admin]题目描述
题意
在3×3的魔板里面有数字1-8的八个可以滑动的方块块,还有一个格子是空格。
任意一个相邻着空着的滑块都可以滑向空格。每滑动一个滑块算一步
就像图那样。
现在给你魔板的初始状态和结束状态,问最少多少步可以让魔板从初始状态变成结束状态
输入
输入共六行
前三行每行共三个整数,表示魔板的初始状态。
后三行每行共三个整数,表示魔板的初始状态。
1-8表示8个滑块的编号,0表示空格
输出
输出共有一个整数表示最少多少步可以让魔板从初始状态变成结束状态(若无解不输出)
样例输入
1 2 3
4 0 6
7 8 5
1 2 3
4 6 0
7 8 5
样例输出
1
这个状态可以说是对我的一个比较大的挑战,首先是码量上面,其次就是因为我的dp脑子实在动不起来,所以的话这个状态压缩确实是对我来说是一个挺大的挑战来的,所以接下来就看我特别特别水的嗯讲...话
从题意上来看不是特别的难理解,就是和我们平时玩的那些是一样的,但是我们要怎么把它转化状态这就是我们要处理的问题,显然你总不可能用三维的一行一行去判断吧,时间一定会超限,所以我们就要用到状态压缩 以下简称状压
我们先了解以下状压的定义!!!
状压的话感觉常理上一般是搭配dp的,当然并不代表其他算法不可以,只是说状压在dp当中出现的频率比较多,而且的话状压出现的姿态一般是二进制,(没错就是你想到的那些———超级恶心的位运算)
而且的话一般来说要压缩的,要省时间,省内存的一定是那些超级无敌麻烦的东西,比如说棋盘问题什么的,
所以综上所述,状压确实没什么好介绍的,所以我在这里放两篇大佬的博客,第一篇,第二篇,因为他里面有比较详细的例题,可以深刻的让大家感受到dp+状压的魅力
然后说完了超级超级没有什么好介绍的状压之后,我们看到这道题目,
首先第一感觉我说了三维,第二感觉就......
这道题的话,用到了一个康托展开,因为康托展开可以很好的帮我们把一个方阵变成一个序列,这样无论从空间亦或时间都有很大的帮助,
康托展开(一篇很好的博客)是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。
康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。(源由:百度百科)
所以我们用康托展开就可以把我们的状态可以到达的情况都可以保存到,这样我们在搜索的时候就可以直接按照他康托之后的状态来直接搜索了,在这道题当中/*康托展开,就是把一个方阵换成全排列,如
1 2 3
4 0 6
7 8 9 换成 1 2 3 4 0 6 7 8 9 ,通过康托展开来全出从小到大的全排列的排序
通过康托展开看看我们当前这个状态是在全排列中排第几个
公式要运用到阶乘,如果在这里阶乘会很慢,所以在主函数预处理阶乘
*/这样的话,所以这个康托就起到了很大的作用,因为我们在搜索的时候可以直接判断他的状态有没有出现过
稍微放一下scy的康托文档,和我康托的代码(文档在下面)
老实说scy的文档还是超级超级好用的
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define ll long long 5 using namespace std; 6 ll n; 7 ll a[17],d[17]; 8 ll kangtuo1() 9 { 10 bool bo[17]; memset(bo,false,sizeof(bo)); 11 ll sum=0; 12 for(int i=1;i<=n-1;i++) 13 { 14 ll k=0; 15 for(int j=1;j<a[i];j++) if(bo[j]==false) k++; 16 sum+=k*d[n-i]; 17 bo[a[i]]=true; 18 } 19 sum++; 20 return sum; 21 } 22 void kangtuo2(ll sum) 23 { 24 sum--; 25 bool bo[17]; memset(bo,true,sizeof(bo)); 26 for(int i=1;i<=n;i++) 27 { 28 ll k=sum/d[n-i]; 29 sum-=k*d[n-i]; 30 ll j; 31 for(j=1;j<=n;j++) if(bo[j]==true) 32 { 33 if(k==0) break; 34 k--; 35 } 36 bo[j]=false; 37 a[i]=j; 38 } 39 for(int i=1;i<n;i++) printf("%lld ",a[i]); 40 printf("%lld ",a[n]); 41 } 42 int main() 43 { 44 scanf("%lld",&n); 45 for(int i=1;i<=n;i++) scanf("%lld",&a[i]); 46 d[0]=1; for(int i=1;i<=15;i++) d[i]=d[i-1]*i; 47 printf("%lld ",kangtuo1()); 48 ll k; scanf("%lld",&k); 49 kangtuo2(k); 50 return 0; 51 }在往下的话,因为我们展开之后相当于直接变成了一个队列,所以可以直接用队列来处理,就是把每个状态都处理以下就好了
康托展开和康托展开的逆运算
By_scy 2010-4-8
八数码问题不用康托展开判断重复8s,用康托展开判断重复30MS。康托展开最大最明显的作用就是在判断状态是否重复方面了,其实属于hash的一个技巧。
一、康托展开
【问题背景】对于一个有n个不同元素的集合{1,2,3,4,...,n}的从小到大排序(从大到小 同理)的全排列 显然它有n!项。如n=4,那么就有4!=4×3×2×1=24项。
与自然数1,2,3,4,-----n!与之一一对应。比如 1~4四个数的全排列按字典序如下:
1234:第1个
1243:第2个
1324:第3个
1342:第4个
1423:第5个
1432:第6个2134:第7个
2143:第8个
2314: 第9个
2341:第10个
2413:第11个
2431:第12个3124:第13个
3142:第14个
3214:第15个
3241:第16个
3412:第17个
3421:第18个4123:第19个
4132:第20个
4213:第21个
4231:第22个
4312:第23个
4321:第24个
【主要问题】
例1:求4132是第几个排列? 看上面就知道答案就是:20。 那么是怎么算的呢?
解:总共4个数,所以n=4.ans:=0;
第一个数是4,研究比4小的并且还没有出现过的数有3个:1,2,3。那么ans:=ans+ 3*(n-1)!
所以 ans:= ans+ 3* 3*2*1 =18
第二个数是1,研究比1小的并且还没有出现过的数为 0个。那么ans:=ans+ 0 * (n-2)!,那么ans不变。
第三个数是3,研究比3小的并且还没有出现过的数为1个:2。那么ans:=ans+ 1* (n-3)!,那么ans:=18+1* 1=19
第四个数是2,研究比2小的并且还没有出现过的数为0个。那么ans不变。其实最后一个可以不研究了,比它大和比它小的全都出现过了。 最后ans怎么等于19啊??代表它前面有19个排列嘛,那么4132自己就是第20个罗( 最后ans:=ans+1)
例2:问45231是第几个排列?
4 5 2 3 1
ans:= 3*4! + 3*3! + 1*2! + 1*1! + 0*0! + 1 =94
练习
第一题:找出35142在1~5从小到大全排序中的位置?
第二题:找出64827315在1~8从小到大全排列中的位置?
第三题:找出35142在1~5从大到小全排序中的位置?
第四题:找出64827315在1~8从大到小全排列中的位置?
二、康托展开的逆运算
我把之前的div改为/,div是pascal语言的运算符号,/是C++的,意思都一样就是求两个整数除法运算后的商(不理会余数)
例3:1~5从小到大全排列中,找出第96个排列?
解:首先设x1x2x3x4x5, (x1等于?不知道),用96-1得到95,表示x1x2x3x4x5前面有95个序列。
第一个数x1,假设x1目前有k个比x1小的并且还没有出现过的数,那么
k:= 95 / (n-1)! = 95 / 24=3, 也就是有3个比x1小并且没有出现过的数,那么x1=4.
95变成95-3×24=23
第二个数x2,假设x2目前有k个比x2小的并且还没有出现过的数,那么
k:= 23 / (n-2)! = 23 / 6 = 3, 也就是有3个比x2小并且没有出现过的数,那么x2=5.(有3个数比它小的数是4,但4已经在之前出现过了,所以是5)
23变成 23 – 3 * 6 = 5
第三个数用5去除2! 得到2余1,那第三个数就是还没有出现过的第3小的数3(5变成1)
第四个数用1去除1! 得到1余0,那第四个数就是还没有出现过的第2小的数2
第五个数就是最后还没有出现的那个。
所以这个数是45321
练习:
第五题:8个数从小到大排序,求第28017个排列?
解:首先设x1x2x3x4x5x6x7x8, (x1等于?不知道),用28017-1得到28016,表示x1x2x3x4x5x6x7x8前面有28016个序列。
第一个数x1,假设x1目前有k个比x1小的并且还没有出现过的数,那么 k= 28016 / (8-1)! = 28016 / 5040=5(28016除以5040等于5,不理余数), 也就是有5个比x1小并且没有出现过的数,那么x1=6.28016变成28016 - 5040*5=2816
第二个数x2,假设x2目前有k个比x2小的并且还没有出现过的数,那么k= 2816/ (n-2)! = 2816 / 720 = 3, 也就是有3个比x2小并且没有出现过的数,那么x2=4.2816变成 2816– 3 * 720 = 656
关键是理解好第三个数,为什么是8.
第三个数x3,假设x3目前有k个比x3小的并且还没有出现过的数,那么k= 656/ (n-2)! = 656 / 120 = 5, 也就是有5个比x3小并且没有出现过的数,那么x3=?.
1(还没出现过)
2(还没出现过)
3(还没出现过)
4(出现过)
5(还没出现过)
6(出现过)
7(还没出现过)
8(还没出现过)
1、2、3、5、7(这五个还没有出现过),所以答案是x3=8
现在已经得到648~~~~~~剩下自己来,然后用电脑的程序(我发了邮件的)验证一下。
第六题:8个数从大到小排序,求第14302个排列?注意 8!= 40320
最后的话差不多也要结束这道题啦,啊这么简单的一道题我竟然写了这么久的博客
代码的实现
(注释版,看不看都差不多,可以理解)
1 #include<cstdio> 2 #include<cstring> 3 #include<cstdlib> 4 #include<algorithm> 5 #include<cmath> 6 #include<iostream> 7 using namespace std; 8 struct node 9 { 10 int a[4][4],x,y,dep;/*a[4][4]表示的是存着3*3的魔板的状态(存数字) x,y表示的是空格的位置(存空格) 11 dep表示的是递归到dep需要多少步 */ 12 }list[400010]; 13 int dx[4]={0,1,0,-1}; 14 int dy[4]={1,0,-1,0}; 15 int d[11];/*康托中使用*/ 16 int head,tail; 17 bool v[400010];/*v表示这个状态是否来过 */ 18 int kangtuo(node tno)/*康托展开模版*/ 19 /*康托展开,就是把一个方阵换成全排列,如 20 1 2 3 21 4 0 6 22 7 8 9 换成 1 2 3 4 0 6 7 8 9 ,通过康托展开来全出从小到大的全排列的排序、 23 通过康托展开看看我们当前这个状态是在全排列中排第几个 24 公式要运用到阶乘,如果在这里阶乘会很慢,所以在主函数预处理阶乘 25 */ 26 { 27 int b[10]; bool bo[10]; memset(bo,false,sizeof(bo)); int k,len,sum; len=0,sum=0; 28 for(int i=1;i<=3;i++) 29 { 30 for(int j=1;j<=3;j++) 31 { 32 len++; 33 b[len]=tno.a[i][j]+1; 34 } 35 }/*把模版中的变化从1开始方便康托展开*/ 36 for(int i=1;i<=9;i++) 37 { 38 k=0; 39 for(int j=1;j<b[i];j++) if(bo[j]==false) k++; 40 sum=sum+k*d[9-i];/*这里就是康托展开的公式 41 x=an*(n-1)!+an-1*(n-2)!+...+a1*0!*/ 42 bo[b[i]]=true; 43 } 44 return sum+1;/*我们的初始状态是有0的存在*/ 45 } 46 int main() 47 { 48 int tt,k; node tno; 49 d[0]=1; for(int i=1;i<=9;i++) d[i]=d[i-1]*i;/*预处理阶乘*/ 50 for(int i=1;i<=3;i++) 51 { 52 for(int j=1;j<=3;j++) 53 { 54 scanf("%d",&list[1].a[i][j]);/*读入初始状态*/ 55 if(list[1].a[i][j]==0) 56 { 57 list[1].x=i; 58 list[1].y=j;/*保存空格的坐标*/ 59 } 60 } 61 } 62 list[1].dep=0;/*初始化步数为0*/ 63 for(int i=1;i<=3;i++) 64 for(int j=1;j<=3;j++) 65 scanf("%d",&list[0].a[i][j]);/*读入目标状态*/ 66 int ed=kangtuo(list[0]);/*转化目标状态*/ 67 head=tail=1; 68 memset(v,false,sizeof(v));/*初始化所有状态不存在*/ 69 bool bk=false;/*表示暂时还没有找到合适的可以满足最终状态的状态*/ 70 while(head<=tail) 71 { 72 for(int i=0;i<=3;i++)/*四个方向*/ 73 { 74 tno=list[head]; 75 tno.x=list[head].x+dx[i]; 76 tno.y=list[head].y+dy[i];/*假设空格向上走*/ 77 if(tno.x>=1 && tno.x<=3 && tno.y>=1 && tno.y<=3)/*满足魔板的范围*/ 78 { 79 swap(tno.a[tno.x][tno.y],tno.a[list[head].x][list[head].y]);/*调换空格和魔板的位置*/ 80 k=kangtuo(tno);/*k代表在全排列中排第几个,通过v来查看有没有这个状态 */ 81 tno.dep=list[head].dep+1;/*转化了之后步数要增加1,也就是dep增加1*/ 82 if(v[k]==false)/*k就是他的状态,表示我当前这个状态没有存在过 */ 83 { 84 v[k]=true;/*搜索过,所以更新为存在过,打上标记*/ 85 tail++; 86 list[tail]=tno;/*记录这个状态*/ 87 if(k==ed)/*满足最终状态*/ 88 { 89 printf("%d ",tno.dep);/*直接输出步数*/ 90 bk=true;/*退出*/ 91 break; 92 } 93 } 94 } 95 } 96 if(bk==true) break; 97 head++; 98 } 99 return 0; 100 }
(非注释版,依旧可以理解的,就看起来行数压力没有这么大)
1 #include<cstdio> 2 #include<cstring> 3 #include<cstdlib> 4 #include<algorithm> 5 #include<cmath> 6 #include<iostream> 7 using namespace std; 8 struct node 9 { 10 int a[4][4],x,y,dep; 11 }list[400010]; 12 int dx[4]={0,1,0,-1}; 13 int dy[4]={1,0,-1,0}; 14 int d[11]; 15 int head,tail; 16 bool v[400010]; 17 int kangtuo(node tno) 18 { 19 int b[10]; bool bo[10]; memset(bo,false,sizeof(bo)); int k,len,sum; len=0,sum=0; 20 for(int i=1;i<=3;i++) 21 { 22 for(int j=1;j<=3;j++) 23 { 24 len++; 25 b[len]=tno.a[i][j]+1; 26 } 27 } 28 for(int i=1;i<=9;i++) 29 { 30 k=0; 31 for(int j=1;j<b[i];j++) if(bo[j]==false) k++; 32 sum=sum+k*d[9-i]; 33 bo[b[i]]=true; 34 } 35 return sum+1; 36 } 37 int main() 38 { 39 int tt,k; node tno; 40 d[0]=1; for(int i=1;i<=9;i++) d[i]=d[i-1]*i; 41 for(int i=1;i<=3;i++) 42 { 43 for(int j=1;j<=3;j++) 44 { 45 scanf("%d",&list[1].a[i][j]); 46 if(list[1].a[i][j]==0) 47 { 48 list[1].x=i; 49 list[1].y=j; 50 } 51 } 52 } 53 list[1].dep=0; 54 for(int i=1;i<=3;i++) 55 for(int j=1;j<=3;j++) 56 scanf("%d",&list[0].a[i][j]); 57 int ed=kangtuo(list[0]); 58 head=tail=1; 59 memset(v,false,sizeof(v)); 60 bool bk=false; 61 while(head<=tail) 62 { 63 for(int i=0;i<=3;i++) 64 { 65 tno=list[head]; 66 tno.x=list[head].x+dx[i]; 67 tno.y=list[head].y+dy[i]; 68 if(tno.x>=1 && tno.x<=3 && tno.y>=1 && tno.y<=3) 69 { 70 swap(tno.a[tno.x][tno.y],tno.a[list[head].x][list[head].y]); 71 k=kangtuo(tno); 72 tno.dep=list[head].dep+1; 73 if(v[k]==false) 74 { 75 v[k]=true; 76 tail++; 77 list[tail]=tno; 78 if(k==ed) 79 { 80 printf("%d ",tno.dep); 81 bk=true; 82 break; 83 } 84 } 85 } 86 } 87 if(bk==true) break; 88 head++; 89 } 90 return 0; 91 }