把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【题解】Luogu P2730 魔板

蒟蒻的第一道蓝题……好像也没有蓝的程度

一篇无STL的超弱题解(入门写法无误了QAQ

传送门

很经典的一道BFS

这是初始状态。

这是初始状态

操作A

qwq侵删

操作B

qwq侵删

操作C

qwq侵删

思路1 不使用cantor展开的情况

1. 对于存储这个操作序列

  • 一个没有什么用的空间小优化

(然后时间就炸了)

  • 存储一个字符,我们都知道需要1个Byte。那么我们存储一个魔板序列时,就需要8个Byte。

  • 魔板的状态有8!=40320种,那我们在不断的存储许多新的状态时,需要预先开至少8*40320个字节的空间。

  • 如果我们使用int类型进行存储,每个int类型只需要4Byte,那么就只需要4*40320个字节。(也没优化多少啊)

2.进行扩展

  • 建立一个队列,初始的状态为队列[1][1]=12345678

  • 接着我们判断,如果初始状态已经等于目标状态,直接输出0,即不需要操作

  • 这个队列中的每个状态,我们需要三个空间,分别存储魔板的状态,魔板的父亲位置(就是现在魔板的状态是由队列中哪个下标的状态进行一次变化而来的),以及魔板是由魔板父亲状态经过什么操作来达到现在状态的。我们可以将三种操作简写为1,2,3。

  • 对魔板状态进行扩展。即将魔板分别进行三种操作。

  • PS:由于我们存储了int类型,可以手推一下三种操作的公式。推荐参考上面三幅图片。

  • 每一种操作结束后,判断现在魔板状态是否在之前有出现过。(判重)

  • PS:判重不使用康托展开,开87654322大的数组即可。亲测可过。

  • 一旦判断已经达到目标状态,输出。

3.输出

  • 我们在队列中已经存储过魔板的父亲位置。现在要利用这个位置来逆推我们的步骤。

  • 建立一个way数组。不断的存储父亲位置。

  • 逆向输出。输出对应位置上的操作。

  • 详情查看代码。

请从变量定义及主函数开始阅读

#include <cstdio>
#include <cmath>//pow函数的头文件

int tl[100000][3],maxn=100000;//tl是我们存储状态的队列。tl[i][1]是指第i个情况的魔板状态,tl[i][0]是第i个情况的父亲位置,t[i][2]是指第i个情况的父亲的操作(能得到i情况)

int now=12345678,finish=0;//初始状态&目标状态

int way[100000];//用于最后逆推过程的时候存储父亲位置

bool book[87654322]={};//判重(真的不会M可能是因为最开始存int

//下面三个操作函数都很乱,建议手算公式<qwq>

void A(int j)//A操作!
{
	int qwq=0;
	for (int i=8;i>=1;i--)
	{
		qwq+=((tl[j][1]%10)*pow(10,i-1));
		tl[j][1]/=10;
	}
	tl[j][1]=qwq;
	return;
}


void B(int j)//B操作!
{
	tl[j][1]=(tl[j][1]%100000/10000)*10000000+(tl[j][1]/100000)*10000+tl[j][1]%1000*10+tl[j][1]%10000/1000;
}


void C(int j)//C操作!
{
	tl[j][1]=tl[j][1]-((tl[j][1]/100000)%100)*100000-(tl[j][1]%1000)/10*10+tl[j][1]/1000000%10*100000+tl[j][1]/100000%10*100+tl[j][1]/100%10*10+tl[j][1]/10%10*1000000;//2367->7236
}

///////////////////////////////////分割线正常阅读


bool look_for(int n)//判重,即新情况的模版状态有没有在队列中出现过
{
	return book[ tl[n][1] ];
}



bool found(int w)//判断是否达到目标状态
{
	if ( tl[w][1] == finish )
	{
		return true;
	}
	return false;
}


void print(int zt)//打印函数。zt是指队列第zt种情况
{
	if ( zt == 1 )//请联系start函数的第一句“if……”理解
	{
		printf("0");
		return;
	}
	int k=0,z=zt;
	while (z>0)//逆推!比较难理解可以试着手画一下示意
	{
		k++;
        
		way[k] = z;
		z = tl[z][0];
	}
	printf("%d\n",k-1);
	for (int i=k-1;i>0;i--)
	{
		z = way[i];
        
		printf("%c",tl[z][2]+'A'-1);//转化为字符输出!原本是用123表示ABC操作
	}
	return;
}
void start()//开始啦!
{
	if ( found(1) )//如果初始状态==目标状态
	{
		print(1);
        
		return;
	}
	int i=1,j=2;
	while ( i<j && j<maxn )//仍有状态可以扩展&未溢出上限
	{
		for (int k=1;k<=3;k++)
		{
        
			tl[j][1]=tl[i][1];//预入队!
            
            
			if (k==1){  A(j);  }//A操作
            
			if (k==2){  B(j);  }//B操作
            
			if (k==3){  C(j);  }//C操作
            
            
			if ( ! look_for(j) )//如果这种状态在之前没有出现过
			{
            
				tl[j][0]=i;//tl[j][0]:我父亲叫i!
                
				tl[j][2]=k;//tl[j][2]:我是i和k的孩子!
                
                
                if (found(j))//如果已经达到了目标状态
				{
					print(j);//输出!
					return;
				}
				else {  book[ tl[j][1] ]=1;  j++;  }//标记这种状态已经出现过&队尾+1
			}
		}
		i++;//一种情况的扩展完毕,队首+1
	}
}
int main()
{
	for (int i=8;i>=1;i--)//输入,转换成int
	{
		int x;
		scanf("%d",&x);
		finish+=(x*pow(10,i-1));
	}
    
	tl[1][1]=now; tl[1][0]=0;//队列的第一个状态就是初始状态now,ta的父亲没有了……
    
	start();//开始吧!
    
	return 0;//圆满的结束……
}

思路2 cantor展开

什么是康托展开?

  • 对于n的一个排列,我们可以通过康托展开,映射出这个排列在n的全排列中占第几小。

  • 那么我们本来使用87654322大的数组来标记出现过的状态,这时就只需要开8!=40320大的数组进行标记。

  • 注意!使用康托展开需要将数位一位位分离。请慎重考虑要使用什么类型存储。推荐使用字符存储。

这里提供一个正向康托展开的代码。 具体可以戳这里

int factorial[11]={1,1,2,6,24,120,720,5040,40320,362880};//0-10的阶乘!


int cantor(int a[]){//正向康托展开,求排列a[]在1~8的全排列中占第几小

    int ans=0,sum=0;
    for(int i=1;i<=7;i++)//最后一位不用计算(0*0!) 
	{
        for(int j=i+1;j<=8;j++)//寻找这个排列的第i位的后面有多少个数小于ta 
        {
        
        	if(a[j]<a[i]) sum++;
            
        }
        
        ans+=sum*factorial[8-i];//ans+=sum*(8-1)! 
        
        sum=0;//计数归零 
    }
    
    
    return ans+1;//这里计算的ans是比a[]排列小的排列的个数,所以a[]排列是第ans+1小的! 
}

其余的思路基本是相同的。不过对于ABC三种操作请使用swap来实现。

蒟蒻的第一篇题解qwq
准备被残忍拒绝

posted @ 2019-03-30 10:35  Kan_kiz  阅读(235)  评论(1编辑  收藏  举报
浏览器标题切换
浏览器标题切换end