康托展开 全排列与其字典序的双映射转换

对于给定的序列1 2 3,其全排列有6种,按照字典序从小到大即为

0 1   2 3 4 5
1,2,3 1,3,2 2,1,3 2,3,1 3,1,2 3,2,1

可以看出,每个全排列序列都唯一对应一个字典序数(从0开始),那么,有什么方法可以根据序列求出其对应的字典序或者根据字典序来推断其对应序列呢

一个朴素的思想,我们使用深搜或者next_permutation函数来对序列按照字典序来遍历,然后记录其出现的位置,最坏时间复杂度为N!

可以看出,仅仅是求出其字典序已经是N!的时间复杂度了,如果我们有别的查找询问的操作,会在N!基础上进行,这是十分大的时间复杂度

我们不妨这样想,如果能求出一个序列之前序列的个数,根据字典序,也就是所有小于当前序列的序列的个数,那所求序列的字典序不就是这个个数吗

对于序列3,4,5,2,1,我们来分析一下

首位为3,要找比整个序列小的序列,只需要找首位比3小的数所构成的序列,可以知道有1,2两个数比3小,则以1,2开头的序列数就是 2*(5-1)!(这里的阶乘即为后四位用剩下四个数的全排列)

第二位为4,我们去寻找比4小的而且并未作为前缀的数,有1,2(3在序列中已经使用作为第一位),因此一共有2*(4-1)!

第三位为5,寻找比5小且未使用的数,1,2,共有2*(3-1)!;

第四位为2,寻找比2小且未使用的数,1,共有1*(2-1)!;

最后位为1,寻找比1小且未使用的数,没有,即为0个,共有0*(1-1)!

综上,所有比34521小的序列就被我们都找出来了,因此加起来就有

2*(4-1)!+2*(3-1)!+1*(2-1)!+0*(1-1)个,得数就是序列34521的字典序

综上我们可以得出这样的公式,对于一个长度为n的序列,其某个全排列序列对应的字典序为

an(n-1)!+an((n-1)-1)!+........+a2(2-1)!+a1(1-1)!

其中an是小于A[n]且未被当作前缀的元素的个数

这样,我们就可以在O(N 2)的时间复杂度内求出其对应字典序了

给出实现代码

#include<algorithm>
#include<cstdio>
#include<vector>
#include<iostream>
using namespace std;
long long Fact[50];//存储阶乘的数组
void fact(int N)//阶乘初始化函数
{
    if (N == 0)
    {
        Fact[N] = 1; return;
    }
    fact(N - 1);
    Fact[N] = Fact[N - 1] * N;
}
int Coutor(vector<int>&A)//康托展开实现函数
{
    int ans = 0;//答案
    for (int i = 0; i < A.size(); i++)//从最高位开始扫描每一位
    {
        int K = 0;//比这一位小的数的个数
            for (int j = i+1/*这里加一即为避免前缀元素的重复计入*/; j < A.size(); j++)
        {
                if (A[i] > A[j])K++;
        }
            ans += K * Fact[A.size() - i - 1];//按照公式计算
    }
    return ans;
}
int main()
{
    fact(20);//初始化阶乘数组
    vector<int>A={1,2,3};//初始化排列数组
    do {
        cout << Coutor(A)<<endl;//输出字典序
    } while (next_permutation(A.begin(), A.end()));//求其全排列
    return 0;
}

 

这样就可以求出从全排列到期字典序了,其实康托展开也可以看作从全排列到字典序转换的哈希函数

但是,还么有完,既然开头我们就说了,康托展开是构造全排列序列与其字典序的双重映射,那么我们如何逆过来,也就是

已知一个全排列的字典序,如何将其展开为位于此字典序上的全排列?其实很简单

我们只需要逐个位来除以刚才乘上的阶乘,得到的整数就是该位上的数比多少数大,以此类推就可以推出此字典序对应的全排列序列

这里给出一个例子,对于五位全排列5,4,3,2,1,其字典序为119(注意从0开始计算),最高位到最低位的阶乘值依次是4!=24,3!=6,2!=2,1!=1,0!=1

因此

119/24=4......23,比四个数大的数为5

23/6=3.......5,比三个数大的数为4

5/2=2.......1,比两个数大的数为3

1/1=1......0,比一个数大的数为2

0/1=0 ,比零个数大的数为1

这样我们就找出了字典序119所对应的序列54321

给出代码实现

#include<algorithm>
#include<cstdio>
#include<vector>
#include<iostream>
using namespace std;
long long Fact[50];
void fact(int N)
{
    if (N == 0)
    {
        Fact[N] = 1; return;
    }
    fact(N - 1);
    Fact[N] = Fact[N - 1] * N;
}
vector<int> incoutor(int Dict, int len)
{
    bool Visited[10] = { false };//使用标记数组
    int cnt = 0;//存储an
    vector<int>C;//结果存储
    for (int i = 0; i < len; i++)
    {
        cnt = Dict / Fact[len - i - 1];
        Dict %= Fact[len - i - 1];//更新Dict和cnt
        for (int j = 1; j <= len; j++)
        {
            if (Visited[j])continue;//如果j已经收入了,掠过本此操作
            if (!cnt)//当cnt为0的时候说明找到了相应的j,进行存入与收入标记
            {
                Visited[j] = true;
                C.push_back(j);
                break;
            }
            cnt--;
        }
    }
    return C;
}
int main()
{
    fact(20);
    auto C = incoutor(119, 5);
    for (auto X : C)
    {
        cout << X;
    }
    return 0;
}
 

 

posted @ 2023-04-26 21:23  凪风sama  阅读(9)  评论(0编辑  收藏  举报