摸鱼日记1:康托展开/逆康托展开

康托展开也是很久以前就有所了解的算法了,今天才真正认真的码了两遍,还是写篇博客强化一下8

o( ̄┰ ̄*)ゞ 题目会补的,一定会的(悲)

定义

(逆)康托展开构建了一个在 正整数排列正整数 之间的双射关系,即用一个正整数表示该排列按照字典序在所有排列中的排名(康托展开),根据排名也可以反推出该排列(逆康托展开)。

康托展开

举一个实例来说明:

排列P:2 3 5 4 1

排名:36

我们来尝试构造出所有字典序小于P的排列。

首先,在第 1 位如果放置 1 ,得到的 1*4! 个排列按照字典序一定小于P。

否则,在第 1 位放置 2 (如果放置 3/4/5 显然不成立),考虑在第二位放置 1( 2 已经在第一位,而 3/4/5 显然不成立),所得到的 1*3! 个排列一定小于P。

否则,在第 2 位放置 3 ,考虑在第 3 位放置 1/4 ,所得到的 2*2!个排列一定小于P。

否则,在第 3 位放置 5 ,考虑在第 4 位放置 1 ,所得到的 1*1!个排列一定小于P。

否则,得到的是原排列,对小于P的排列无贡献。

得到小于P的排列个数 4! + 3! + 2*2! + 1! = 35

则P的排名为 35 + 1 = 36

关于代码实现,该位置的可选择的数的个数取决于 该位置之后 小于 原排列中该数的数的个数(前面的不动,也就是用掉了)

也就是说我们从当前位置的下一个开始遍历,遇到比当前位置更小的就x++,最后加上x*(n-i)!

fac[]为预处理的阶乘数组

上代码:

int cantor(int permutation[], int len)
{
    int rk = 0;
    for (int i = 0; i < len; i++)
    {
        int x = 0;
        for (int j = i + 1; j < len; j++)
            if (permutation[i] > permutation[j])
                x++;
        rk += x * fac[len - i - 1];
    }
    return rk + 1;
}

逆康托展开

同康托展开的想法,每次从rank中去掉尽量多的 (n-i)! ,若rank中有 x 个 (n-i)! ,该位选择剩下可选数中第 x+1 大的数

关于代码实现,直接进行模拟,vector<int> vec 维护剩下的数

上代码:

vector<int> incantor(int rk, int len)
{
    rk--;
    int x;
    vector<int> vec, ans;
    for (int i = 1; i <= len; i++)
        vec.push_back(i);
    for (int i = 1; i <= len; i++)
    {
        ans.push_back(vec[x = rk / fac[len - i]]);
        vec.erase(vec.begin() + x);
        rk %= fac[len - i];
    }
    return ans;
}

小结

下面是总代码:

#include <bits/stdc++.h>
using namespace std;
int fac[20]{1, 1}, num[20];
int cantor(int permutation[], int len)
{
    int rk = 0;
    for (int i = 0; i < len; i++)
    {
        int x = 0;
        for (int j = i + 1; j < len; j++)
            if (permutation[i] > permutation[j])
                x++;
        rk += x * fac[len - i - 1];
    }
    return rk + 1;
}
vector<int> incantor(int rk, int len)
{
    rk--;
    int x;
    vector<int> vec, ans;
    for (int i = 1; i <= len; i++)
        vec.push_back(i);
    for (int i = 1; i <= len; i++)
    {
        ans.push_back(vec[x = rk / fac[len - i]]);
        vec.erase(vec.begin() + x);
        rk %= fac[len - i];
    }
    return ans;
}
int main()
{
    freopen("in.txt", "r", stdin);
    int n;
    cin >> n;
    for (int i = 0; i < n; i++)
        cin >> num[i];
    for (int i = 2; i <= n; i++)
        fac[i] = fac[i - 1] * i;
    int rk = cantor(num, n);
    cout << "rank:" << rk << endl;
    if (rk != 1)
    {
        vector<int> v = incantor(rk, n);
        cout << "permutation:";
        for (int i = 0; i < n; i++)
            cout << v[i] << (i == n - 1 ? "" : " ");
        cout << endl;
    }
    return 0;
}

康拓展开优化

前面的算法复杂度都在 O(n2),显然满足不了 n 稍大时的需求,注意到主要复杂度关卡在查找当前剩余比该位更小的数上面,相当于维护一个需要单点修改区间求和的数组,自然想到使用树状数组/线段树(但我不会写线段树(悲))。

洛谷P5367 【模板】康托展开

【模板】康托展开洛谷链接

代码:

//O(nlogn)用了树状数组优化,这个复杂度比较合适
#include <bits/stdc++.h>
#define ll long long
#define MOD 998244353
using namespace std;
const int MAXN = 1e6 + 5;
ll n, fac[MAXN]{1}, tr[MAXN];
inline int lowbit(int x) { return x & -x; }
void add(int x, int data)
{
    while (x <= n)
    {
        tr[x] += data;
        x += lowbit(x);
    }
}
int sum(int x)
{
    int ans = 0;
    while (x > 0)
    {
        ans += tr[x];
        x -= lowbit(x);
    }
    return ans;
}
ll cantor()
{
    ll rk = 0;
    int t;
    for (int i = 1; i <= n; i++)
    {
        cin >> t; //QAQ甚至不需要把排列存下来
        rk += (sum(t) - 1) * fac[n - i];
        rk %= MOD;
        add(t, -1); //相当于标记这个数用掉了
    }
    return rk + 1;
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        fac[i] = fac[i - 1] * i % MOD;
    for (int i = 1; i <= n; i++)
        add(i, 1);
    ll ans = cantor();
    cout << ans;
    return 0;
}

—————————8.31更新——————————

逆康托展开优化

既然康托展开可以优化到O(nlogn)那么逆康托展开能不能优化呢?

答案当然是可以的,使用权值线段树(暂时还不会QWQ)或者树状数组+二分即可优化,其中后者复杂度O(nlognlogn)还是可以接受的。

这里主要说一下树状数组+二分的方法,即和康托展开中一样,维护还没使用的数的个数的前缀和,二分地查找满足条件的最大整数。

UVA11525 Permutation

UVA11525洛谷链接

代码:

#include <bits/stdc++.h>
#define F freopen("in.txt", "r", stdin)
#define MAXN 500005
using namespace std;
int BIT[MAXN], n;
inline int lowbit(int x)
{
    return x & -x;
}
void upd(int x, int data)
{
    while (x <= n)
    {
        BIT[x] += data;
        x += lowbit(x);
    }
}
int query(int x)
{
    int ans = 0;
    while (x > 0)
    {
        ans += BIT[x];
        x -= lowbit(x);
    }
    return ans;
}
int main()
{
    //F;
    int t;
    cin >> t;
    while (t--)
    {
        memset(BIT, 0, sizeof(BIT));
        cin >> n;
        for (int i = 1; i <= n; i++)
            upd(i, 1);
        for (int i = 0; i < n; i++)
        {
            int p;
            cin >> p;
            int l = 1, r = n, mid, ans;
            while (l <= r)
            {
                mid = (l + r) >> 1;
                if (query(mid - 1) <= p)
                {
                    l = mid + 1;
                    ans = mid;
                }
                else
                    r = mid - 1;
            }
            cout << ans << (i == n - 1 ? "" : " ");
            upd(ans, -1);
        }
        cout << endl;
    }
    return 0;
}

综合题

CF501D Misha and Permutations Summation

CF501D洛谷链接

分析:要求输出对应于 给定的两排列康拓展开之和(溢出时对 n! 取模)的排列

这题数据范围太大,不可能算出康拓展开之和再分解。考虑只采用每位数在当前的排名( cantor()中每次的 x )序列作为康托展开的结果(中间产物)。

求康托展开之和可以等价于求该序列之和(相当于是变进制数,且倒数第 i 位为 i 进制)。

举例说明8:

P1:2 3 5 4 1   rk1:36 = 1 * 4! + 1 * 3! + 2 * 2! + 1 * 1! + 0 * 0!

P2:2 4 5 1 3   rk2:41 = 1 * 4! + 2 * 3! + 2 * 2! + 0 * 1! + 0 * 0!

P3=P1 + P2     rk3:77 = 3 * 4! + 0 * 3! + 2 * 2! + 1 * 1! + 0 * 0! = (1+1) * 4! + (1+2) * 3! + (2+2) * 2! + (1+0) * 1! + (0+0) * 0!

上式中的黑色数列就是我们需要的部分,构成了一个变进制数,变进制数的加法和一般的加法一样,不过是进位的时候需要mod的数是和位数有关的,上图标红的部分是需要进位的,理解之后代码就很好写啦。

代码:

#include <bits/stdc++.h>
#define F freopen("in.txt", "r", stdin)
#define MAXN 200005
using namespace std;
int n, BIT[MAXN], can[MAXN], cBIT[MAXN];
inline int lowbit(int x)
{
    return x & -x;
}
void upd(int arr[], int x, int data)
{
    while (x <= n)
    {
        arr[x] += data;
        x += lowbit(x);
    }
}
int query(int arr[], int x)
{
    int ans = 0;
    while (x > 0)
    {
        ans += arr[x];
        x -= lowbit(x);
    }
    return ans;
}
void cantor()
{
    memset(cBIT, 0, sizeof(cBIT));
    for (int i = 1; i <= n; i++)
        upd(cBIT, i, 1);
    for (int i = 0; i < n; i++)
    {
        int t;
        cin >> t;
        t++;
        can[i] += query(cBIT, t - 1);
        upd(cBIT, t, -1);
    }
}
int main()
{
    //F;
    cin >> n;
    for (int i = 1; i <= n; i++)
        upd(BIT, i, 1);
    cantor();
    cantor();
    reverse(can, can + n);
    for (int i = 0; i < n; i++)
    {
        can[i + 1] += can[i] / (i + 1);
        can[i] %= i + 1;
    }
    reverse(can, can + n);
    for (int i = 0; i < n; i++)
    {
        int l = 1, r = n, mid, ans;
        while (l <= r)
        {
            mid = (l + r) >> 1;
            if (query(BIT, mid - 1) <= can[i])
            {
                l = mid + 1;
                ans = mid;
            }
            else
                r = mid - 1;
        }
        cout << ans - 1 << (i == n - 1 ? "" : " ");
        upd(BIT, ans, -1);
    }
    return 0;
}
posted @ 2021-08-30 16:49  Slithery  阅读(273)  评论(0)    收藏  举报