浅谈康托展开和其逆运算

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;
}

以上是关于浅谈康托展开和其逆运算的主要内容,如果未能解决你的问题,请参考以下文章

康托展开及其逆运算

关于数论康托展开及其逆运算

bzoj3301: [USACO2011 Feb] Cow Line

康托展开和逆康托展开

宽搜经典题之二——8数码难题+康托展开

全排列的编码与解码——康托展开 (附完整代码)