【算法速成课 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;
}
相关推荐
MM_MS14 小时前
Halcon变量控制类型、数据类型转换、字符串格式化、元组操作
开发语言·人工智能·深度学习·算法·目标检测·计算机视觉·视觉检测
独自破碎E14 小时前
【二分法】寻找峰值
算法
mit6.82414 小时前
位运算|拆分贪心
算法
ghie909015 小时前
基于MATLAB的TLBO算法优化实现与改进
开发语言·算法·matlab
恋爱绝缘体115 小时前
2020重学C++重构你的C++知识体系
java·开发语言·c++·算法·junit
wuk99815 小时前
VSC优化算法MATLAB实现
开发语言·算法·matlab
Z1Jxxx15 小时前
加密算法加密算法
开发语言·c++·算法
乌萨奇也要立志学C++15 小时前
【洛谷】递归初阶 三道经典递归算法题(汉诺塔 / 占卜 DIY/FBI 树)详解
数据结构·c++·算法
vyuvyucd16 小时前
C++引用:高效编程的别名利器
算法
鱼跃鹰飞16 小时前
Leetcode1891:割绳子
数据结构·算法