【算法速成课 3】康托展开(Cantor Expansion)/ 题解 P3014 [USACO11FEB] Cow Line S

专栏:算法速成课_proMatheus的博客-CSDN博客

前置知识:树状数组


前导

康托展开(Cantor Expansion)是一种将一个排列 ,映射为一个唯一整数的编码方法。

常用于排列的哈希、状态压缩或字典序编号等场景。

题意

任务一:求一个全排列是第几个全排列,按字典序(即从小到大)。

任务二:求第 个全排列。

1.康托展开(任务 1)

分析

假如我问你,求 是第几个 的全排列,你会怎么做?

先一个个列出来?

发现是第 个。

那有没有更快的方法?

我们发现 的第二个位是 ,这代表 都在它的前面。

直接加上所有 的数量

接着发现第三个位是 ,照理来说应该是它后面、比它小的 在它这个位置,

但现在这里是 ,代表 都在 的前面,加上数量

,这是所有在 前面的数,即 是第 个全排列。

再回顾总结下,假设要求 的全排列。

如果有比当前第 个位上的数 小,且还没出现过的数

那么这个 肯定能顶替 得到更小的字典序 ,排在题目给出排列的前面( 位不动)。

累计答案加上 ,这表示所有 顶替 构成的排列都排在题目给出排列的前面

很明显,有几个这样的 就应该加几个

实现

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

想要求出比当前 小且还没出现过数的个数,可以考虑使用树状数组 / 线段树 / 平衡树。

后两个都稍有麻烦,我们用树状数组

时间复杂度

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 1e6 + 10;
const LL P = 998244353;

int n, a[N];
LL c[N], fac[N];

void add(int x, LL d) {
	for (; x <= n; x += x & -x) {
		c[x] = (c[x] + d) % P;
	}
}

LL get_sum(int x) {
	LL res = 0;
	for (; x >= 1; x -= x & -x) {
		res = (res + c[x]) % P;
	}
	return res;
}

int main () {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	cin >> n;
	for (int i = 1; i <= n; i ++) {
		cin >> a[i];
	}
	
	memset(c, 0, sizeof(c));
	fac[0] = 1;
	for (int i = 1; i <= n; i ++) {  // 初始化阶乘数组 
		fac[i] = fac[i - 1] * i % P;   // 一定要取模!! 
	}
	
	LL ans = 0;
	for (int i = 1; i <= n; i ++) {
		int x = a[i];
		LL t = ( (x - 1) - get_sum(x - 1) + P) % P;   
		// get_sum 求出来的是出现过比 x 小的数,要求没出现过的 
		ans = (ans + t * fac[n - i] % P) % P;
		add(x, 1);   // 把 x 放进去 
	}
	cout << (ans + 1) << "\n";   // 别忘了加 1,ans 是在题目给出排列前面的排列数 
	
	return 0;
}

2.逆康托展开(任务 2)

分析

聪明的你肯定想到了应该怎么做:

假设给出的排列序号是 ,循环 先减减。

这样 就代表着在答案排列前面的排列个数 ,接下来每一个位都根据前面的排列定值

当前循环到

如果 里面有 ,取 未出现 的第 小数为

那么当前位就等于

因为按理说当前位就该是 未出现 的最小数了,但 里又有

这代表在第 位,还有** 个比它小的数构成的排列** 排在它前面( 位不动),

又由于不能重复,所以取 未出现 的第 小数。

别忘了

还是拿 的例子,现在我们只知道

时, 里面有 未出现 的第 小数为

时, 里面有 未出现 的第 小数为

时, 里面有 未出现 的第 小数为

时, 里面有 未出现 的第 小数为

时, 里面有 未出现 的第 小数为

得出

实现

难点在求 未出现 的最小数,这玩意 ,想优化上线段树 or 树状数组上倍增。

(不过例题 很小不需要,无所谓我会给出两份代码)

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

P 操作就是逆展开。

,long long 的范围是

即差不多 ,可以放心用。

逆展开 代码(我用了 set,总时间复杂度 可 AC):

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 22;

int n, a[N];
LL c[N], fac[N];

void add(int x, LL d) {
	for (; x <= n; x += x & -x) {
		c[x] += d;
	}
}

LL get_sum(int x) {
	LL res = 0;
	for (; x >= 1; x -= x & -x) {
		res += c[x];
	}
	return res;
}

int main () {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int K;
	cin >> n >> K;
	
	fac[0] = 1;
	for (int i = 1; i <= n; i ++) {
		fac[i] = fac[i - 1] * i;   // 阶乘这一块 /. 
	}
	
	while (K --) {
		char s[5];
		cin >> s;
		
		if (s[0] == 'Q') {   // 正展开 
			memset(c, 0, sizeof(c));   // 每个询问都要清空一次 
			LL ans = 0;
			
			for (int i = 1; i <= n; i ++) {
				cin >> a[i];
				LL t = a[i] - 1 - get_sum(a[i] - 1);
				ans += t * fac[n - i]; 
				add(a[i], 1);
			}
			
			cout << (ans + 1) << "\n";
		}
		
		else {   		   // 逆展开 
			LL k;     // 这玩意可有 20! 那么大 
			cin >> k; k --;   // 减减 
			set<int> set_;  // 未使用数字集合 
			for (int i = 1; i <= n; i ++) {
				set_.insert(i);    // 全都放进去 
			}
			
			for (int i = 1; i <= n; i ++) {
				int aa = k / fac[n - i];  // 重名了用 aa
				auto it = set_.begin(); 
				advance(it, aa);   // 移到第 aa + 1 个元素 
				a[i] = *it;
				set_.erase(*it);     // 删了 
				k -= aa * fac[n - i];  // 别忘了减 
			}
			
			for (int i = 1; i <= n; i ++) {
				cout << a[i] << " ";
			}
			cout << "\n";
 		}
	}
	
	return 0;
} 

逆展开树状数组倍增 做法,总时间复杂度

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 22;

int n, a[N];
LL c[N], fac[N];

void add(int x, LL d) {
	for (; x <= n; x += x & -x) {
		c[x] += d;
	}
}

LL get_sum(int x) {
	LL res = 0;
	for (; x >= 1; x -= x & -x) {
		res += c[x];
	}
	return res;
}

// 在树状数组 c 中找第 k 小的可用数(k 从 1 开始)
// 这里利用了树状数组的特性,即查询 1 到 n 最多遍历 log n 个值 
int kth(int k) {
    int p = 0, s = 0;    
	// p 是当前树状数组上刚刚遍历到的节点(当前遍历范围可使用数字最大值) 
	// s 是当前已遍历范围里可使用数字的总个数 
	// 整个过程就是不断调整 p 的大小(遍历范围的最大值)
	// 来看看 s 什么时候刚好等于 k - 1
	// 由于最后 s 肯定停在 k - 1 的临界值(再大一点就不是了)
	// 所以 p + 1 是第 k 个数 
	 
    // n <= 20,所以 1 << 5 = 32 足够
    for (int i = 5; i >= 0; i --) {
        int t = p + (1 << i);
        if (t <= n && s + c[t] < k) {  // 节点还小于 n,总个数要小于 k 
            s += c[t];
            p = t;
        }
    }
    
    // 现在 p 是最大的满足 get_sum(p) < k 的下标 
    return p + 1;
}

int main () {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int K;
	cin >> n >> K;
	
	fac[0] = 1;
	for (int i = 1; i <= n; i ++) {
		fac[i] = fac[i - 1] * i;   // 阶乘这一块 /. 
	}
	
	while (K --) {
		char s[5];
		cin >> s;
		
		if (s[0] == 'Q') {   // 正展开 
			memset(c, 0, sizeof(c));   // 每个询问都要清空一次 
			LL ans = 0;
			
			for (int i = 1; i <= n; i ++) {
				cin >> a[i];
				LL t = a[i] - 1 - get_sum(a[i] - 1);
				ans += t * fac[n - i]; 
				add(a[i], 1);
			}
			
			cout << (ans + 1) << "\n";
		}
		
		else {   		   // 逆展开 
			LL k;     // 这玩意可有 20! 那么大 
			cin >> k; k --;   // 减减 
			
			memset(c, 0, sizeof(c));   // 现在这个树状数组是存未使用的数字 
			for (int i = 1; i <= n; i++) {
				add(i, 1);     // 全都放进去 
			}
			
			for (int i = 1; i <= n; i ++) {
				int aa = k / fac[n - i];   // 重名了用 aa
				int t = kth(aa + 1);     // 第 (aa + 1) 小,kth 是 1-indexed
				a[i] = t;
				add(t, -1);              // 删掉这个数
				k -= aa * fac[n - i];      // 别忘了减 
			}
			
			for (int i = 1; i <= n; i ++) {
				cout << a[i] << " ";
			}
			cout << "\n";
 		}
	}
	
	return 0;
}
相关推荐
m0_748233643 小时前
【类与对象(中)】C++类默认成员函数全解析
开发语言·c++·算法
scilwb3 小时前
STM32 实战:驯服失控的 M3508 电机 - PID 位置环频率的“坑”与“药”
算法·代码规范
chonbw3 小时前
226.翻转二叉树
算法·leetcode
源代码•宸5 小时前
Qt6 学习——一个Qt桌面应用程序
开发语言·c++·经验分享·qt·学习·软件构建·windeployqt
苏纪云5 小时前
数据结构<C++>——数组
java·数据结构·c++·数组·动态数组
周杰伦_Jay5 小时前
【 RocketMQ 全解析】分布式消息队列的架构、消息转发与快速实践、事务消息
分布式·算法·架构·rocketmq·1024程序员节
郝学胜-神的一滴5 小时前
使用现代C++构建高效日志系统的分步指南
服务器·开发语言·c++·程序人生·个人开发
sprintzer5 小时前
10.16-10.25力扣计数刷题
算法·leetcode
王哈哈^_^5 小时前
【数据集】【YOLO】【目标检测】建筑垃圾数据集 4256 张,YOLO建筑垃圾识别算法实战训推教程。
人工智能·深度学习·算法·yolo·目标检测·计算机视觉·数据集