浅谈康托展开和其逆运算
Posted cdoi-24374
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈康托展开和其逆运算相关的知识,希望对你有一定的参考价值。
康托展开,是一种在(mathcal{O}(n^2))((n)为排列元素个数)时间复杂度求解某一排列在全排列中的次序的算法。
我们以一道例题引入:
排列的序号
题目描述:
给定一个数(n)和一个(n)个数的排列(a),求(a)在(n)的全排列中的序号。
输入描述:
第一行一个整数(n),第二行一个排列(a)。
输出描述:
求(a)在(n)的全排列中的序号。
输入输出样例:
输入
3 123
输出
1
数据范围
(nle 15)
根据排列组合、加法原理等等,得出一个式子:
[ans=sum_{i=1}^n(a_{n+1-i}(n-i)!)]
((a_i)表示原数的第(i)位在当前未出现的元素中是排在第几个)
此为康托展开,代码如下:
ull Cantor(int n,int a[15]) //对于n的一个排列a进行康托展开
{
ull ans=0; //因答案可能很大所以用ull
for (int i=0;i<n;i++)
{
int x=0; //x代指公式中a[i],节省空间
for(int j=i+1;j<n;j++) //计算公式中a[i]
if (a[j]<a[i]) ++x;
ans+=x*fact[n-i-1];
}
return ans+1; //答案要+1
}
逆康托展开倒着回去就行:
void CantorReverse(long long r,int len,int a[]) //康托展开逆运算,结果在a中
{
r--; //初始r要减1
bool vis[20]={0}; //vis[i]用来标记是否排列中有数字i
for(int i=1;i<=len;i++)
{
long long tp=r/fact[len-i]; //得出商,确定初始值
r-=tp*fact[len-i]; //用减法代替取模加快运算
int j;
for(j=0;j<len;j++) //求出i位上数字
if(!vis[j]){if(!tp) break;--tp;} //依次检查vis并标记tp
vis[j]=1;
a[i-1]=j;
}
}
++++++++++++++++++++++++++++++++++++++++++分割线+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
前面是不是很草率
好,再来一道例题:
那道题就是八数码,输入开始序列,求出它到
1 2 3
8 0 4
7 6 5
最少移动几步,如果不能在(5000)步以内求解,输出(-1)
这道题BFS
大家都会吧,把状态还能状压,我们讲的是把状压后状态康托进一步节省空间。
Code(RE30%):
//#define DEBUG
#ifdef DEBUG
#include<windows.h>
#include<conio.h>
#include<ctime>
#include<cstdio>
#define JG puts("----------------------------------");
#define wait(time) Sleep(time*1000);
#define Get getch();
#define cls system("cls");
#endif // DEBUG
#include<iostream>
#include<cstring>
#define max(x,y) {(x)>(y)?(x):(y)} //优化
#define min(x,y) {(x)<(y)?(x):(y)}
//#define DEFINE_LIQUEUE
//#define USE_QuickIO
template<typename T> //交换
inline void Swap(T& x,T& y){T tmp=x;x=y;y=tmp;}
using namespace std;
int a[9]; //定义目标布局数组
typedef unsigned long long ull;
#ifdef DEFINE_LIQUEUE
namespace LiQueue //使用namespace防止CE
{
template<typename T>
class LiQueue //定义queue
{
typedef T *TPoint;
T Rear,Front;
//使用单链表可以避免数组开太大
//并且queue中只需要插入尾部和删除头部,很适合链表。
class slist
{
struct node //节点结构体
{
T data;
node* next;
}*head;
T rear; //为了方便使用队列加了个rear
public:
slist(){head=NULL;} //默认头指针指向NULL
bool empty(){return !head;} //链表是否为空
void Insert_head(T data) //在头部插入
{
node* NewNode=new node; //申请新节点
NewNode->data=data; //设定data
if (this->empty()) //如果链表为空
NewNode->next=NULL; //则next为NULL
else NewNode->next=head->next->next; //否则指向下一个节点
head->next=NewNode; //将头节点指向它
rear=data; //标记rear
}
void Delete_tail()
{
if (empty()) return ;
if (head->next->next==NULL)
{
head->next=head=NULL;
return ;
}
node* For=head; //遍历用的节点
while (For->next->next) //遍历到目标结点向前一个节点。
For=For->next; //下一个节点
For->next=NULL; //直接置NULL,就不垃圾回收了。
rear=For->data; //标记rear
}
T GetRear(){return rear;}
}Queue;
public:
LiQueue(){}
bool empty(){return Queue.empty();} //队列是否为空
void push(T data){Queue.Insert_head(data);} //入队时插入头部
T front(){return Queue.GetRear();} //取尾部
void pop(){Queue.Delete_tail();} //出队时删除尾部
};
}
#endif
#ifdef USE_QuickIO
typedef unsigned long long ull;
struct READ
{
template<typename type>
inline READ& operator>> (type& num)
{
register char c=getchar(),w=1;
while('0'>c||c>'9'){if(c==EOF) return *this;w=c=='-'?-1:1;c=getchar();}
num=0;
while ('0'<=c&&c<='9'){num=(num<<1)+(num<<3)+(c-'0');c=getchar();}
num*=w;
return *this;
}
}cin;
class WRITE
{
private:
char out[1<<10],*top;
public:
inline WRITE(){top=out;}
inline ~WRITE(){fwrite(out,1,top-out,stdout);}
inline WRITE& operator<< (char c){
*(top++)=c;
if (top==out+(1<<20)) fwrite(top=out,1,1<<20,stdout);
return *this;
}
inline WRITE& operator <<(ull num){
if(num==0) return *this;
return *this<<num/10<<(char)(num%10+'0');
}
template<typename type>
inline WRITE& operator <<(type & num){
if(num==0) return *this<<'0';
if(num>0) return *this<<(ull)(num);
return *this<<'-'<<(ull)(-num);
}
}cout;
#endif
const ull fact[20]={1,1,2,6,24,120,720,5040,40320,362880,3628800,39916800,479001600,6227020800,87178291200,1307674368000}; //1~15阶乘表
const int UpperBound=5000; //上界
const int EndCantor=46686,EndValue=123804765; //初始布局的康托展开值和初始布局状压后的值。
int StartCantor,StartValue; //结束布局的康托展开值和初始布局状压后的值。
const int N=370000; //状态最大9!=362880
const short dx[4]={0,0,-1,1},dy[4]={1,-1,0,0}; //方向数组
bool vis[N]; //标记数组
int step[N];
inline void Input(){cin>>a[0]>>a[1]>>a[2]>>a[3]>>a[4]>>a[5]>>a[6]>>a[7]>>a[8];}//输入
ull Cantor(int n,int a[15]) //对于n的一个排列a进行康托展开
{
ull ans=0; //因答案可能很大所以用ull
for (int i=0;i<n;i++)
{
int x=0; //x代指公式中a[i],节省空间
for(int j=i+1;j<n;j++) //计算公式中a[i]
if (a[j]<a[i]) ++x;
ans+=x*fact[n-i-1];
}
return ans+1; //答案要+1
}
void CantorReverse(long long r,int len,int a[]) //康托展开逆运算,结果在a中
{
r--; //初始r要减1
bool vis[20]={0}; //vis[i]用来标记是否排列中有数字i
for(int i=1;i<=len;i++)
{
long long tp=r/fact[len-i]; //得出商,确定初始值
r-=tp*fact[len-i]; //用减法代替取模加快运算
int j;
for(j=0;j<len;j++) //求出i位上数字
if(!vis[j]){if(!tp) break;--tp;} //依次检查vis并标记tp
vis[j]=1;
a[i-1]=j;
}
}
void bfs()
{
memset(step,-1,sizeof step); //step数组初始化为-1
int Q[N],rear,front; //Q队列用来存储已探索状态状压后康托展开的值。
rear=front=0; //初始化
vis[StartCantor]=true; //已访问本身
step[StartCantor]=0; //到本身的步数为0
Q[rear]=StartCantor,++front; //放入初始状态
while (rear!=front)
{
int t=Q[rear]; //取出队头状态
++rear;
if (t==EndCantor) {cout<<step[t];return ;} //如果是结束状态
vis[t]=true;
int cp[9],p[3][3]; //cp:还原后一维数组,p:还原后二维数组
CantorReverse(t,9,cp); //还原
p[0][0]=cp[0];p[0][1]=cp[1];p[0][2]=cp[2];//一维数组转二维
p[1][0]=cp[3];p[1][1]=cp[4];p[1][2]=cp[5];
p[2][0]=cp[6];p[2][1]=cp[7];p[2][2]=cp[8];
#ifdef DEBUG
JG;
cout<<"现在的状态:
";
for (int i=0;i<3;i++)
{
for (int j=0;j<3;j++)
cout<<p[i][j]<<' ';
cout<<'
';
}
JG;
cout<<"现在的CP:";
for (int i=0;i<9;i++) cout<<cp[i]<<' ';
cout<<'
';
#endif // DEBUG
int x,y;
for (int i=0;i<3;i++) //找到0的坐标
for (int j=0;j<3;j++)
if (!p[i][j]) {x=i,y=j;break;}
#ifdef DEBUG
cout<<"找到的0的坐标:("<<x<<','<<y<<")
";
Get;
cls;
#endif // DEBUG
for (int i=0;i<4;i++) //扩展新状态
{
int tx=x+dx[i],ty=y+dy[i]; //新坐标
if (tx>=0&&tx<3&&ty>=0&&ty<3) //不越界
{
#ifdef DEBUG
JG;
cout<<x<<"->"<<tx<<'
';
cout<<y<<"->"<<ty<<'
';
#endif // DEBUG
Swap(p[tx][ty],p[x][y]); //新状态
for (int ii=0;ii<9;ii++) //转一维
cp[ii]=p[ii/3][ii%3];
#ifdef DEBUG
cout<<"更新状态:
";
for (int i=0;i<3;i++)
{
for (int j=0;j<3;j++)
cout<<p[i][j]<<' ';
cout<<'
';
}
cout<<"更新的CP:";
for (int i=0;i<9;i++) cout<<cp[i]<<' ';
cout<<'
';
#endif // DEBUG
int NowCantor=Cantor(9,cp); //Cantor处理
if (vis[NowCantor]) //重复
{
Swap(p[tx][ty],p[x][y]); //恢复原状态
continue;
}
Q[front]=NowCantor; //入队
++front;
step[NowCantor]=step[t]+1;
if (step[NowCantor]>UpperBound){cout<<-1;return ;}
Swap(p[tx][ty],p[x][y]); //恢复原状态
}
}
#ifdef DEBUG
cls;
#endif // DEBUG
}
}
int main()
{
#ifdef FILE
freopen("Eight-figure Puzzles.in","r",stdin);
freopen("Eight-figure Puzzles.in","r",stdout);
#endif // OPEN FILE
Input(); //输入
#ifdef DEBUG
cls; //清屏
#endif // DEBUG
int z=0;
for (int i=0;i<9;i++) z=z*10+a[i]; //获取数字
StartCantor=Cantor(9,a); //设置结束Cantor与Value
StartValue=z;
bfs(); //Breadth First Search
#ifdef FILE
fclose(stdin);
fclose(stdout);
#endif // CLOSE FILE
return 0;
}
以上是关于浅谈康托展开和其逆运算的主要内容,如果未能解决你的问题,请参考以下文章