康托展开

引入

康托展开(Cantor expansion)用于将排列转换为字典序的索引(逆展开则相反)

百度百科

维基百科

方法

假设我们要求排列 5 2 4 1 3 的字典序索引

逐位处理:

  • 第一位5 2 4 1 3,如果一个排列的第一位比 \(5\) 小(有 \(4\) 种情况)

    则不管其后 \(4\) 位如何(有 \(4!\) 种情况),其字典序都更小

    所以,至少有 \(4\times 4!\) 个排列字典序更小。
  • 第二位:5 2 4 1 3,如果另一个排列的第一位就是 \(5\) ,但第二位比 \(2\) 小(有 \(1\) 种情况)

    则不管其后 \(3\) 位如何(有 \(3!\) 种情况),其字典序都更小

    所以, 至少有 \(4\times 4!+1\times 3!\) 个排列字典序更小。
  • 第三位:5 2 4 1 3,如果另一个排列的前两位与我们的相同,但第三位比 \(4\) 小(\(2\) 不能选了,从右往左看,有 \(2\) 种情况)

    则不管其后 \(2\) 位如何(有 \(2!\) 种情况),其字典序都更小

    所以, 至少有 \(4\times 4!+1\times 3!+2\times 2!\) 个排列字典序更小。
  • 第四位:5 2 4 1 3,如果另一个排列的前三位与我们的相同,但第四位比 \(1\) 小(不可能, 有 \(0\) 种情况)

    则不管其后 \(1\) 位如何(有 \(1!\) 种情况),其字典序都更小

    所以,至少有 \(4\times 4!+1\times 3!+2\times 2!+0\times1!\) 个排列字典序更小。
  • 第五位:5 2 4 1 3,按照上面的方法操作,很显然, 有 \(4\times 4!+1\times 3!+2\times 2!+0\times1!+0\times0!\) 个排列字典序更小。

因此,若索引从 \(1\) 开始,则 5 2 4 1 3 的索引是 \(4\times 4!+1\times 3!+2\times 2!+0\times1!+0\times0!+1=107\) 。

算法

总结上述方法,可以归纳出以下算法:

  • 枚举排列的每一位,对值为 \(p_k\) 的第 \(k\) 位:

    • 找出后面所有位( \(k+1\) 至 \(n\) )中小于 \(p_k\) 的位数 \(a_k\) (也就是 \(p_k\) 是第 \(k\) 至 \(n\) 位第 \(a_k+1\) 小的)

    若用这些位中的某一位替换第 \(k\) 位,则无论后面 \(n-k\) 位如何排列(总共有 \(a_k(n-k)!\) 种情况),最终的字典序肯定更小

    • 把这些字典序更小的排列数加起来
  • 再加 \(1\) 即为该排列的字典序索引

公式

用公式来表示即为:

\[\mathrm{Index}(p)=1+\sum^n_{k=1}
(n-k)!
|\{p_i\mid p_i<p_k,\ k+1\leq i\leq n\}|
\]

优化

其中 \(a_k\) 的求值过程可以进行优化,设一序列 \(P=\underbrace{[1,1,1,\dots,1]}_{n\text{个}1}\)

我们每处理一位就置 \(P_{p_k}\) 为 \(0\) 。

这样在排列 \(p\) 中,我们没处理过的值就可以被表示为序列 \(P\) 中值为 \(1\) 的索引,即:

\[\{p_i\mid k+1\leq i\leq n\}=\{i\mid P_i=1\}
\]

我们可以用线段树、树状数组这样的数据结构来维护 \(P\) ,要求小于 \(p_k\) 的位数,即是求序列 \(P\) 区间 \([1,p_k-1]\) 中 \(1\) 的个数,这不就是区间求和吗?

算法可以改进为以下这样:

优化算法

  • 初始化长度为 \(n\)(索引从 \(1\) 开始),值全为 \(1\) 的序列 \(P\)
  • 枚举排列的每一位,对值为 \(p_k\) 的第 \(k\) 位:
    • 修改 \(P_{p_k}=0\)
    • 求出 \(a=(n-k)!\sum^{p_k-1}_{i=1}P_i\) ,和式为序列 \(P\) 区间 \([1,p_k-1]\) 的元素和
  • 把所有 \(a\) 相加,最后加 \(1\) 即为索引。

时间复杂度为 \(\mathrm{O}(n\log n)\)

参考代码

题目:P5367 【模板】康托展开 - 洛谷

线段树版本)

#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
//---//
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std; typedef unsigned int u;
typedef long long ll;
typedef unsigned long long llu; #define rep(i, a, b) for (ll i = a; i < b; i++)
#define REP(i, a, b) for (ll i = a; i <= b; i++)
#define per(i, b, a) for (ll i = b; i >= a; i--) const ll N = 1000005;
const ll mod = 998244353; ll p[N], f[N], d[4 * N];
bool lz[4 * N]; #define mid ll m = s + ((t - s) >> 1) void bd(ll s, ll t, ll i) {
if (s == t) {
d[i] = 1;
return;
}
mid;
bd(s, m, i * 2);
bd(m + 1, t, i * 2 + 1);
d[i] = (d[i * 2] + d[i * 2 + 1]) % mod;
} void spr(ll s, ll t, ll i) {
if (lz[i]) {
d[2 * i] = 0;
d[2 * i + 1] = 0;
lz[2 * i] = true;
lz[2 * i + 1] = true;
lz[i] = false;
}
} void upd(ll l, ll r, ll s, ll t, ll i) {
if (l <= s && t <= r) {
d[i] = 0;
lz[i] = true;
return;
}
mid;
spr(s, t, i);
if (l <= m) upd(l, r, s, m, 2 * i);
if (r > m) upd(l, r, m + 1, t, 2 * i + 1);
d[i] = (d[i * 2] + d[i * 2 + 1]) % mod;
} ll get(ll l, ll r, ll s, ll t, ll i) {
if (l > r) return 0;
if (l <= s && t <= r) return d[i];
mid;
spr(s, t, i);
ll sum;
if (l <= m) sum = get(l, r, s, m, 2 * i) % mod;
if (r > m) sum = (sum + get(l, r, m + 1, t, 2 * i + 1)) % mod;
return sum;
} signed main() {
ll n, r = 1;
f[0] = 1;
scanf("%lld", &n);
bd(1, n, 1);
REP(i, 1, n) scanf("%lld", &p[i]);
REP(i, 1, n - 1) f[i] = f[i - 1] * i % mod;
REP(k, 1, n) {
upd(p[k], p[k], 1, n, 1);
r = (r + get(1, p[k] - 1, 1, n, 1) * f[n - k] % mod) % mod;
}
printf("%lld", r);
} // https://www.luogu.com.cn/record/60977896

康托逆展开

引入

康托逆展开用于通过一个已知长度排列的字典序索引反求出该排列

推导

刚刚,我们知道了:

\[\mathrm{Index}(p)=1+\sum^n_{k=1}(n-k)!a_k
\]

其中:

\[a_k=|\{p_i\mid p_i<p_k,\ k+1\leq i\leq n\}|
\]

已知 \(n\) 位排列 \(p\) 的字典序索引为 \(\mathrm{Index}(p)\) ,通过康托逆展开,我们可以求出各个 \(a_i\) ,从而求出排列 \(p\) 。

首先,我们把右侧的 \(1\) 移至左侧,再提出和式中的第一项:

\[\mathrm{Index}(p)-1=(n-1)!a_1+\sum^n_{k=2}(n-k)!a_k
\quad(1)
\]

好像我们能用整除直接求出 \(a_1\) ,但我们得先证明后面那个和式不影响结果。

由 \(a_k\) 的定义,我们有 \(a_k\leq n-k\),所以:

\[\begin{aligned}
&\ \sum^n_{k=2}(n-k)!a_k\\
\leq &\ \sum^n_{k=2}(n-k)!(n-k)\\
= &\ \sum^n_{k=2}(n-k)!(n-k+1-1)\\
= &\ \sum^n_{k=2}(n-k+1)!-(n-k)!\\
= &\ \sum^{n-2}_{k=0}(k+1)!-k!\\
= &\ (n-1)!-1\\
< &\ (n-1)!
\end{aligned}
\]

把式 \((1)\) 右侧的和式看成带余除法的余数 \(R_1\) ,原式写为:

\[\mathrm{Index}(p)-1=(n-1)!a_1+R_1\quad(R_1<(n-1)!)
\]

把 \((n-1)!\) 看作除数,\(a_1\) 看作商,我们就可以表示出 \(a_1\) 了:

\[a_1=\lfloor\frac{\mathrm{Index}(p)-1}{(n-1)!}\rfloor
\]

另外有

\[R_1=(\mathrm{Index}(p)-1)\ \%\ (n-1)!
\]

我们继续拆出余下和式的第一项:

\[R_1=(n-2)!a_2+\sum^n_{k=3}(n-k)!a_k
\]

这个式子和刚刚的是一模一样的,于是我们能同样地求出 \(a_2\) 以及其他所有 \(a\) 值,按照以下递推式:

\[\begin{aligned}
a_i&=\lfloor\frac{R_{i+1}}{(n-i)!}\rfloor\\
R_i&=R_{i-1}\ \%\ (n-i)!
\end{aligned}
\]

这样,我们就求出了所有 \(a\) ,回想一下, \(a_k\) 是指排列的第 \(k\) 位是排列第 \(k\) 位至 \(n\) 位(未处理的所有位)中第 \(a_k+1\) 小的,我们只需从第 \(1\) 位开始,对第 \(i\) 位,从未选中的数中选择第 \(a_i+1\) 小的添加到排列中,最后就能形成对应字典序的排列了。

我们可以用一个初始化为[1,2,3,...,n]vector,每处理一位则erase()一位,每次取索引为 \(a_i\) 的元素即可。

算法

归纳上述推导过程就有了以下的算法:

  • 初始化一个 \(1\) 至 \(n\) 的vector<int>vec
  • 求出 \(1\) 至 \(n-1\) 的阶乘,备用
  • 初始化 \(R=\mathrm{Index}(p)-1\)
  • 对 \(1\) 至 \(n\) 的 \(i\) 执行:
    • 求出\(a=\lfloor\frac{R_{i+1}}{(n-i)!}\rfloor\)
    • 排列的 \(p\) 的第 \(i\) 位即为vec[a]
    • 移除vec[a]
    • 更新:\(R\leftarrow R\ \%\ (n-i)!\)
  • 排列 \(p\) 即为答案

参考代码

题目:P3014 [USACO11FEB]Cow Line S - 洛谷

#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
//---//
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std; typedef unsigned int u;
typedef long long ll;
typedef unsigned long long llu; #define rep(i, a, b) for (ll i = a; i < b; i++)
#define REP(i, a, b) for (ll i = a; i <= b; i++)
#define per(i, b, a) for (ll i = b; i >= a; i--) const ll N = 21; ll p[N], f[N], d[4 * N];
vector<ll> vec;
bool lz[4 * N]; #define mid ll m = s + ((t - s) >> 1) void bd(ll s, ll t, ll i) {
lz[i] = false;
if (s == t) {
d[i] = 1;
return;
}
mid;
bd(s, m, i * 2);
bd(m + 1, t, i * 2 + 1);
d[i] = d[i * 2] + d[i * 2 + 1];
} void spr(ll s, ll t, ll i) {
if (lz[i]) {
d[2 * i] = 0;
d[2 * i + 1] = 0;
lz[2 * i] = true;
lz[2 * i + 1] = true;
lz[i] = false;
}
} void upd(ll l, ll r, ll s, ll t, ll i) {
if (l <= s && t <= r) {
d[i] = 0;
lz[i] = true;
return;
}
mid;
spr(s, t, i);
if (l <= m) upd(l, r, s, m, 2 * i);
if (r > m) upd(l, r, m + 1, t, 2 * i + 1);
d[i] = d[i * 2] + d[i * 2 + 1];
} ll get(ll l, ll r, ll s, ll t, ll i) {
if (l > r) return 0;
if (l <= s && t <= r) return d[i];
mid;
spr(s, t, i);
ll sum;
if (l <= m) sum = get(l, r, s, m, 2 * i);
if (r > m) sum = sum + get(l, r, m + 1, t, 2 * i + 1);
return sum;
} signed main() {
ll n, r, q, I, A;
char c;
f[0] = 1;
scanf("%lld%lld\n", &n, &q);
REP(i, 1, n - 1) f[i] = f[i - 1] * i;
while (q--) {
c = getchar();
while (c != 'P' && c != 'Q') c = getchar();
if (c == 'P') {
scanf("%lld", &I);
I--;
REP(i, 1, n) vec.push_back(i);
REP(i, 1, n) {
A = I / f[n - i];
I %= f[n - i];
printf("%lld ", vec[A]);
vec.erase(vec.begin() + A);
}
printf("\n");
} else {
r = 1;
bd(1, n, 1);
REP(i, 1, n) scanf("%lld", &p[i]);
REP(k, 1, n) {
upd(p[k], p[k], 1, n, 1);
r += get(1, p[k] - 1, 1, n, 1) * f[n - k];
}
printf("%lld\n", r);
}
}
} // https://www.luogu.com.cn/record/60986344

参考

康托展开+逆展开(Cantor expension)详解+优化的更多相关文章

  1. 康托展开&逆展开算法笔记

    康托展开(有关全排列) 康托展开:已知一个排列,求这个排列在全排列中是第几个 康托展开逆运算:已知在全排列中排第几,求这个排列 定义: X=an(n-1)!+an-1(n-2)!+...+ai(i-1 ...

  2. Solr系列六:solr搜索详解优化查询结果(分面搜索、搜索结果高亮、查询建议、折叠展开结果、结果分组、其他搜索特性介绍)

    一.分面搜索 1. 什么是分面搜索? 分面搜索:在搜索结果的基础上进行按指定维度的统计,以展示搜索结果的另一面信息.类似于SQL语句的group by 分面搜索的示例: http://localhos ...

  3. Tomcat 配置详解/优化方案

     转自:http://blog.csdn.net/cicada688/article/details/14451541 Service.xml Server.xml配置文件用于对整个容器进行相关的配置 ...

  4. tomcat配置详解/优化方案

    Service.xml Server.xml配置文件用于对整个容器进行相关的配置. <Server>元素:是整个配置文件的根元素.表示整个Catalina容器. 属性:className: ...

  5. Tomcat 配置详解/优化方案(转)

    转载地址:https://blog.csdn.net/cicada688/article/details/14451541/ Service.xml Server.xml配置文件用于对整个容器进行相关 ...

  6. mysql explain语法详解--优化你的查询

    原文地址:http://blog.csdn.net/zhuxineli/article/details/14455029 explain显示了mysql如何使用索引来处理select语句以及连接表.可 ...

  7. WebView使用详解(二)——WebViewClient与常用事件监听

      登录|注册     关闭 启舰 当乌龟有了梦想……       目录视图 摘要视图 订阅 异步赠书:Kotlin领衔10本好书      免费直播:AI时代,机器学习如何入门?      程序员8 ...

  8. POJ 1077 Eight (BFS+康托展开)详解

    本题知识点和基本代码来自<算法竞赛 入门到进阶>(作者:罗勇军 郭卫斌) 如有问题欢迎巨巨们提出 题意:八数码问题是在一个3*3的棋盘上放置编号为1~8的方块,其中有一块为控制,与空格相邻 ...

  9. HDU 1027 Ignatius and the Princess II(康托逆展开)

    Ignatius and the Princess II Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K ( ...

随机推荐

  1. RabbitMQ之消息模式1

    消息100%的投递 消息如何保障100%的投递成功? 什么是生产端的可靠性投递? 保障消息的成功发出 保障MQ节点的成功接收 发送端收到MQ节点(Broker)确认应答 完善的消息进行补偿机制 BAT ...

  2. NOIP模拟13「工业题·卡常题·玄学题」

    T1:工业题 基本思路   这题有一个重要的小转化: 我们将原来的函数看作一个矩阵,\(f(i,j-1)*a\)相当于从\(j-1\)向右走一步并贡献a,\(f(i-1,j)*b\)相当于从\(i-1 ...

  3. ELK学习之Logstash+Kafka篇

    上一篇介绍了一下Logstash的数据处理过程以及一些基本的配置功能,同时也提到了Logstash作为一个数据采集端,支持对接多种输入数据源,其中就包括Kafka.那么这次的学习不妨研究一下Logst ...

  4. Windows下安装Apollo时的几个常见问题

    今天在本地安装Apollo时遇到几个问题,觉得还是记录下来,希望能给有需要的朋友提供帮助. 安装的过程参考这篇教程,https://www.jianshu.com/p/6cf4b15ba82f.流程基 ...

  5. Python - pip 批量更新

    pip 常用命令 https://www.cnblogs.com/poloyy/p/15170968.html pip list 结合 Linux 命令 pip list 命令可以查询已安装的库,结合 ...

  6. npm 设置同时从多个包源加载包的方法

    随着前后端分离技术的发展成熟,越来越来越多的后台系统甚至前端系统采用前后端分离方式,在大型前后端分离系统中,前端往往包含大量的第三方js 包的引用,各个第三方包又可能依赖另外一个第三方包,因此急需要一 ...

  7. dubbo 2.7应用级服务发现踩坑小记

    本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star. 背景 本文记录最近一位读者反馈的dubbo 2.7.x中应用级服务发现的问题,关于dubbo应 ...

  8. 批量ip段/子网转换

    #coding=utf-8 import re import struct from sys import argv class CIDRHelper(object): def ipFormatChk ...

  9. 【分布式微服务企业快速架构】SpringCloud分布式、微服务、云架构快速开发平台源码

    鸿鹄云架构[系统管理平台]是一个大型 企业.分布式.微服务.云架构的JavaEE体系快速研发平台,基于 模块化.微服务化.原子化.热部署的设计思想,使用成熟领先的无商业限制的主流开源技术 (Sprin ...

  10. Linux的bg和fg和jobs和nohup命令简单介绍

    我们都知道,在 Windows 上面,我们要么让一个程序作为服务在后台一直运行,要么停止这个服务.而不能让程序在前台后台之间切换.而 Linux 提供了 fg 和 bg 命令,让我们轻松调度正在运行的 ...