摸鱼日记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
代码:
#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
分析:要求输出对应于 给定的两排列康拓展开之和(溢出时对 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;
}

浙公网安备 33010602011771号