洛谷P1379【八数码问题】【九宫重排】算法优化

题目描述

洛谷P1379 八数码难题 (本博客的代码和描述都是针对洛谷这题)

问题 1426: [蓝桥杯][历届试题]九宫重排 (与洛谷那题 [基本] 一样,输入有所不同)

在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。

输入格式

输入初始状态,一行九个数字,空格用0表示

输出格式

只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数(测试数据中无特殊无法到达目标状态数据)

输入输出样例

前言

最近在学数据结构,重新看回刘佳汝《算法竞赛入门经典》里面的八数码问题。我发现,洛谷、蓝桥杯、和我学校oj (scnuoj)上都有这个题。于是这个周末,我闲着无聊,我打了好几个版本的代码。3个oj上我都提交过,都过了。趁现在脑还热就感觉下一篇博客记录一下。

先说明一下,下面我只会说算法思路,具体的代码细节我就不赘述了,我贴出AC代码,希望能给各位一点帮助!由于我在不同oj上提交,代码会有些改动,我不知道会不会搞混了。如有错误,请各位指正。

版本1

单向bfs + stl set容器判重

(洛谷)总用时:7.53s

但过不了蓝桥杯那题,那题的数据点比洛谷强。。。

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;

const int d[][2]={
   
   {-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左 

//bfs的状态结点 
typedef struct State
{
	char state[15];
	int step; 
	
	State() { step=0; }
	State(char* s, int cnt)
	{
		strcpy(state, s);
		step=cnt;
	}
}State;
State origin; //初始状态
char dest[]="123804765"; //最终状态 

set<string> st;

void bfs()
{
	queue<State> q;
	q.push(origin);
	
	while(!q.empty())
	{
		State head=q.front();
		q.pop();
		
		//判断是否已经达到最终状态
		if(!strcmp(head.state, dest))
		{
			printf("%d\n", head.step);
			return;
		} 
		
		//找到空格的位置 
		int pos;
		for(int i=0; head.state[i]!='\0'; i++)
			if(head.state[i]=='0')
			{
				pos=i;
				break;
			}
		
		int x=pos/3;
		int y=pos%3;
		for(int i=0;i<4;i++)
		{
			int x1=x+d[i][0];
			int y1=y+d[i][1];
			
			if(x1>=0 && x1<3 && y1>=0 && y1<3)
			{
				int pos1=x1*3+y1;
			
				char s[15]; //扩展的新节点 
				strcpy(s, head.state);
				swap(s[pos], s[pos1]);
				
				if(!st.count(string(s))) //判断扩展的新状态是否已经访问过 
				{
					st.insert(string(s));
					q.push(State(s, head.step+1));
				}
			}
		}				
	}
	printf("-1\n");
}

int main()
{
	scanf("%s", origin.state);
	
	bfs();
	
	return 0;
}

 版本2

单向bfs + 字典树判重 

将判重和插入分开:(洛谷)总用时:3.38s

在判重的同时实现插入:(洛谷)总用时:2.59s 

《算法竞赛入门经典》里面是以整数形式存储每种状态,我以字符串形式存储,感觉操作方便一点

手写字典树,如非必要,不要装×。在确保了我的bfs主算法正确后,我才试着手写字典树的,虽然心里还是有点虚。但很庆幸,调了2次就过了。之前师兄曾给我们展示过一个用数组实现字典树的模板,但由于这学期学数据结构,老师介绍了树的左兄弟右孩子表示法,于是我就试着用链表实现字典树。 

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;

const int d[][2]={
   
   {-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左 

//bfs的状态结点 
typedef struct State
{
	char state[15];
	int step; 
	
	State() { step=0; }
	State(char* s, int cnt)
	{
		strcpy(state, s);
		step=cnt;
	}
}State;
State origin; //初始状态
char dest[]="123804765"; //最终状态 

typedef struct Node
{
	char c;
	int cnt; //以该字符结尾的[前缀]出现的次数
	Node* child; //左孩子有兄弟表示法 
	Node* brother;
	Node()
	{
		cnt=0;
		child=NULL;
		brother=NULL;
	}
}Node;

class Trie
{
	public:
		Trie(); 
		int insert(char* s);
		int find(char* s);
	private:
		Node* root;
};

Trie::Trie()
{
	root=new Node;
}

int Trie::insert(char* s)
{
	Node* u=root;
	Node* v=NULL;
	int success=0;
	for(int i=0; s[i]!='\0'; i++)
	{
		int flag=0;
		for(v=u->child; v!=NULL; v=v->brother)
			if(v->c==s[i])	
			{
				v->cnt+=1;
				flag=1;
				break;
			}
		
		if(!flag)
		{
			success=1;
			
			v=new Node;
			v->c=s[i];
			v->child=NULL;
			v->brother=u->child;
			v->cnt=1;
			
			u->child=v;
		}
		u=v;	
	}
	return success;
}

int Trie::find(char* s)
{
	Node* u=root;
	Node* v=NULL;
	for(int i=0; s[i]!='\0'; i++)
	{
		int flag=0;
		for(v=u->child; v!=NULL; v=v->brother)
			if(v->c==s[i])	
			{
				flag=1;
				break;
			}
				
		if(!flag)
			return 0;
		u=v;	
	}
	return u->cnt;
}

Trie trie;

void bfs()
{
	queue<State> q;
	q.push(origin);
	
	while(!q.empty())
	{
		State head=q.front();
		q.pop();
		
		//判断是否已经达到最终状态
		if(!strcmp(head.state, dest))
		{
			printf("%d\n", head.step);
			return;
		} 
		
		//找到空格的位置 
		int pos;
		for(int i=0; head.state[i]!='\0'; i++)
			if(head.state[i]=='0')
			{
				pos=i;
				break;
			}
		
		int x=pos/3;
		int y=pos%3;
		for(int i=0;i<4;i++)
		{
			int x1=x+d[i][0];
			int y1=y+d[i][1];
			
			if(x1>=0 && x1<3 && y1>=0 && y1<3)
			{
				int pos1=x1*3+y1;
			
				char s[15]; //扩展的新节点 
				strcpy(s, head.state);
				swap(s[pos], s[pos1]);
				
				if(trie.insert(s)) //在判重的同时实现插入
					q.push(State(s, head.step+1));
				
				/*
				if(!trie.find(s)) //先判重
				{
					trie.insert(s); //再插入
					q.push(State(s, head.step+1));
				}
				*/
			}
		}
					
	}
	printf("-1\n");
	
}


int main()
{
	scanf("%s", origin.state);
	
	bfs();
	
	return 0;
}

版本3

单向bfs + 手写哈希表判重

(洛谷)总用时:2.67s

在判重的同时进行插入,队列我用数组模拟,但效率没有明显提高,建议都用stl提供的队列。

额,是不是觉得我很无聊。又手写哈希表。。。这学期学Java,了解了HashSet的底层实现,于是就自己模仿Java的实现原理尝试用C++写个简单的哈希表。其实这也不是我第一次手写哈希表,23333。。。

在3个oj上实测,效率一般来说比字典树高,但不太稳定。哈希表的效率主要取决于哈希函数的优略和哈希表的大小。我用的这个字符串的哈希函数是从网上找的,别人测试过的。另外哈希表的大小1000003,最好不要动它,我试过我一旦动了它,用时就边长了。至少,假如你用这个哈希函数,这个哈希表的大小就建议用1000003!针对其他哈希函数我不知道。

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;

const int d[][2]={
   
   {-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左 

//bfs的状态结点 
typedef struct State
{
	char state[15];
	int step; 
	
	State() { step=0; }
	State(char* s, int cnt)
	{
		strcpy(state, s);
		step=cnt;
	}
}State;
State origin; //初始状态
char dest[15]="123804765"; //最终状态 

//set<string> st;

//哈希表的结点 
const int maxn=1000003; 
typedef long long ll;
typedef struct Node
{
	char str[15];
	Node* next;
	Node() { }
	Node(char* s, Node* nt)
	{
		strcpy(str, s);
		next=nt;
	}
}Node;
Node* hashTable[maxn]; //哈希表

//求哈希值并映射到哈希表的坐标 
int BKDRHash(char *str)
{
    ll seed = 131; 
    ll hash = 0;
 
    while (*str)
        hash = hash * seed + (*str++);
    
    return (int)((hash & 0x7FFFFFFF)%maxn);
}

//0:表示该字符串已存在,插入失败  1:字符串不存在,插入成功 
int tryInsert(char* s)
{
	int hash=BKDRHash(s);
	
	Node* p=hashTable[hash];
	if(p==NULL)
		hashTable[hash]=new Node(s, NULL); //注意不能写成 p=  
	else
	{
		while(p->next!=NULL)
		{
			if(!strcmp(p->str, s)) //已存在 
				return 0;
			p=p->next;
		}
		p->next=new Node(s, NULL);
	}
	
	return 1;
}

State* q[maxn]; //模拟队列
int front=-1; 
int rear=-1; 

void bfs()
{
	//queue<State> q;
	//q.push(origin);
	q[++rear]=&origin;
	
	while(front<rear)
	{
		//State head=q.front();
		//q.pop();
		State* head=q[++front];
		
		//判断是否已经达到最终状态
		if(!strcmp(head->state, dest))
		{
			printf("%d\n", head->step);
			return;
		} 
		
		//找到空格的位置 
		int pos;
		for(int i=0; head->state[i]!='\0'; i++)
			if(head->state[i]=='0')
			{
				pos=i;
				break;
			}
		
		int x=pos/3;
		int y=pos%3;
		for(int i=0;i<4;i++)
		{
			int x1=x+d[i][0];
			int y1=y+d[i][1];
			
			if(x1>=0 && x1<3 && y1>=0 && y1<3)
			{
				int pos1=x1*3+y1;
			
				char s[15]; //扩展的新节点 
				strcpy(s, head->state);
				swap(s[pos], s[pos1]);
				
				if(tryInsert(s)) //不存在,插入成功 
				{
					//q.push(State(s, head.step+1));
					q[++rear]=new State(s, head->step+1);
				}
				
				
				/*
				if(!st.count(string(s))) //判断扩展的新状态是否已经访问过 
				{
					st.insert(string(s));
					q.push(State(s, head.step+1));
				}
				*/
			}
		}
					
	}
	printf("-1\n");
	
}

int main()
{
	scanf("%s", origin.state);
	
	bfs();
	
	return 0;
}

版本4

双向bfs + map标记

(洛谷)总用时:351ms

大一参加蓝桥杯省赛之前,师兄曾开过一场培训,那时师兄就介绍过双向bfs,当时也讲了哈希表。。但当时听个懵懵懂懂。双向bfs,就是从起点和从终点“同时”bfs,这个同时并不是真的同时,只是两棵bfs树交替向外扩展,相当于你扩展一层后,然后轮到我扩展一层。当两棵bfs树相遇,最短路为相遇的两个状态的步数之和+1。开一个队列也可以实现!

如何判断两棵bfs树相遇呢?这个标记就很巧妙了。。这个标记我是借鉴了其它题解的。

看的出来,综合考虑,在赛场上这是首选!代码简短,效率还高。

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<map>
using namespace std;
const int d[][2]={
   
   {-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左 
string origin; //初始状态
string dest="123804765"; //最终状态 

map<string,int> vis;
map<string,int> step;

void bfs()
{
	//特判
	if(origin==dest)
	{
		printf("0");
		return;
	}
	 
	queue<string> q;
	
	q.push(origin);
	vis[origin]=1;
	step[origin]=0;
	
	q.push(dest);
	vis[dest]=2;
	step[dest]=0;
	
	while(!q.empty())
	{
		string head=q.front();
		q.pop();
		
		int pos;
		for(int i=0; i<head.length(); i++)
			if(head[i]=='0')
			{
				pos=i;
				break;
			}
		
		int x=pos/3;
		int y=pos%3;
		for(int i=0;i<4;i++)
		{
			int x1=x+d[i][0];
			int y1=y+d[i][1];
			
			if(x1>=0 && x1<3 && y1>=0 && y1<3)
			{
				int pos1=x1*3+y1;
				string s=head; //copy一份 
				swap(s[pos], s[pos1]);
				
				if(!vis.count(s))
				{
					q.push(s);
					vis[s]=vis[head];
					step[s]=step[head]+1;
				}
				else if(vis[s]+vis[head]==3)
				{
					printf("%d", step[s]+step[head]+1);
					return;
				}
			}
		}
	}
    printf("-1"); //这个没用,因为题目说一定可达。。但蓝桥杯那题有不可达的情况
}

int main()
{
	
	cin>>origin;
	
	bfs();
	
	return 0;
}

版本5

双向bfs优化 + map判重

(洛谷)总用时:358ms

每次出队,元素少的那个队列的对头元素出队! 所有只能开两个队列了。

详情见大神博客:https://blog.csdn.net/ww32zz/article/details/50755225

好像用时没有减少。。但我在蓝桥杯题库和学校oj上提交,用时少了一点点。原因我盲猜一下,造成两个队列里面元素个数不相等的原因,就是其中一个bfs在扩展状态结点时碰到边界了。所以这个优化是否明显还要却决于两个bfs的起点的位置。蓝桥杯那题的终点状态不是固定的,可能这个优化对于蓝桥杯那题会比较明显吧。。快了十几ms。。如果我没记错。。当然,上述纯是我盲猜。。。也有可能我代码写错了,所以不明显。。

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<map>
using namespace std;
const int d[][2]={
   
   {-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左 
string origin; //初始状态
string dest="123804765"; //最终状态 

map<string,int> vis;
map<string,int> step;

void bfs()
{
	//特判
	if(origin==dest)
	{
		printf("0");
		return;
	}
	 
	queue<string> q1;
	queue<string> q2;
	
	q1.push(origin);
	vis[origin]=1;
	step[origin]=0;
	
	q2.push(dest);
	vis[dest]=2;
	step[dest]=0;
	
	while(!q1.empty() || !q2.empty())
	{
		string head;
		int flag;
		if(q1.size()<q2.size())
		{
			head=q1.front();
			q1.pop();
			flag=1;
		}
		else
		{
			head=q2.front();
			q2.pop();
			flag=2;
		}
		
		int pos;
		for(int i=0; i<head.length(); i++)
			if(head[i]=='0')
			{
				pos=i;
				break;
			}
		
		int x=pos/3;
		int y=pos%3;
		for(int i=0;i<4;i++)
		{
			int x1=x+d[i][0];
			int y1=y+d[i][1];
			
			if(x1>=0 && x1<3 && y1>=0 && y1<3)
			{
				int pos1=x1*3+y1;
				string s=head; //copy一份 
				swap(s[pos], s[pos1]);
				
				if(!vis.count(s))
				{
					if(flag==1)
						q1.push(s);
					else if(flag==2)
						q2.push(s);
					
					vis[s]=vis[head];
					step[s]=step[head]+1;
				}
				else if(vis[s]+vis[head]==3)
				{
					printf("%d", step[s]+step[head]+1);
					return;
				}
			}
		}
	}
}

int main()
{
	
	cin>>origin;
	
	bfs();
	
	return 0;
}

版本6  终极版本

双向bfs优化 +  字典树判重

(洛谷)总用时:178ms

由于在使用双向bfs时要进行特殊标记,所以字典树要进行改动!具体实现我就不赘述了,参见代码。

在学校oj测试,最高用时:16ms

在蓝桥杯题库,最高用时:23ms

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;

const int d[][2]={
   
   {-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左 

string origin; //初始状态
string dest="123804765"; //最终状态 

typedef struct Node
{
	char c;
	int cnt; //以该字符结尾的[前缀]出现的次数。这个没用。。但顺手写上去 
	int flag; //这个flag标记只有在字典树的叶子结点才被标记,非叶子节点这个flag相当于个冗余字段
	int step;
	Node* child; //左孩子有兄弟表示法 
	Node* brother;
	Node()
	{
		cnt=0;
		flag=0;
		step=0;
		child=NULL;
		brother=NULL;
	}
}Node;

class Trie
{
	public:
		Trie(); 
		int insert(string s, int flag, int step);
		Node* find(string s);
	private:
		Node* root;
};

Trie::Trie()
{
	root=new Node;
}

int Trie::insert(string s, int flag, int step)
{
	Node* u=root;
	Node* v=NULL;
	int success=0;
	for(int i=0; i<s.length(); i++)
	{
		int flag=0;
		for(v=u->child; v!=NULL; v=v->brother)
			if(v->c==s[i])	
			{
				v->cnt+=1;
				flag=1;
				break;
			}
		
		if(!flag)
		{
			success=1;
			
			v=new Node;
			v->c=s[i];
			v->child=NULL;
			v->brother=u->child;
			v->cnt=1;
			
			u->child=v;
		}
		u=v;	
	}
	u->flag=flag;
	u->step=step;
	return success;
}

Node* Trie::find(string s)
{
	Node* u=root;
	Node* v=NULL;
	for(int i=0; i<s.length(); i++)
	{
		int flag=0;
		for(v=u->child; v!=NULL; v=v->brother)
			if(v->c==s[i])	
			{
				flag=1;
				break;
			}
				
		if(!flag)
			return NULL;
		u=v;	
	}
	return u;
}

Trie trie;

void bfs()
{
	//特判
	if(origin==dest)
	{
		printf("0");
		return;
	}
	 
	queue<string> q1;
	queue<string> q2;
	
	q1.push(origin);
	trie.insert(origin, 1, 0);
	
	q2.push(dest);
	trie.insert(dest, 2, 0);
	
	while(!q1.empty() || !q2.empty())
	{
		string head;
		int flag;
		if(q1.size()<=q2.size())
		{
			head=q1.front();
			q1.pop();
			flag=1;
		}
		else
		{
			head=q2.front();
			q2.pop();
			flag=2;
		}
		
		int pos;
		for(int i=0; i<head.length(); i++)
			if(head[i]=='0')
			{
				pos=i;
				break;
			}
		
		int x=pos/3;
		int y=pos%3;
		for(int i=0;i<4;i++)
		{
			int x1=x+d[i][0];
			int y1=y+d[i][1];
			
			if(x1>=0 && x1<3 && y1>=0 && y1<3)
			{
				int pos1=x1*3+y1;
				string s=head; //copy一份 
				swap(s[pos], s[pos1]);
				
				Node* h=trie.find(head);
				Node* t=trie.find(s);
				if(t==NULL)
				{
					if(flag==1)
						q1.push(s);
					else if(flag==2)
						q2.push(s);
					
					trie.insert(s, h->flag, h->step+1);
				}
				else if(t->flag + h->flag==3)
				{
					printf("%d", t->step + h->step + 1);
					return;
				}
			}
		}
	}
}


int main()
{
	cin>>origin;
	
	bfs();
	
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_43290318/article/details/102764787