【算法速成课 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;
}
相关推荐
2301_803554521 分钟前
socket编程
c++
热爱编程的OP7 分钟前
Linux进程池与管道通信详解:从原理到实现
linux·开发语言·c++
Blossom.1185 小时前
移动端部署噩梦终结者:动态稀疏视觉Transformer的量化实战
java·人工智能·python·深度学习·算法·机器学习·transformer
轻微的风格艾丝凡5 小时前
卷积的直观理解
人工智能·深度学习·神经网络·算法·计算机视觉·matlab·cnn
田梓燊7 小时前
红黑树分析 1
算法
晚风吹长发8 小时前
二分查找算法+题目详解
c++·算法·二分查找
悠悠~飘8 小时前
18.PHP基础-递归递推算法
算法·php
pilgrim538 小时前
结合 Leetcode 题探究KMP算法
算法·leetcode
罗义凯8 小时前
其中包含了三种排序算法的注释版本(冒泡排序、选择排序、插入排序),但当前只实现了数组的输入和输出功能。
数据结构·c++·算法
kevien_G19 小时前
JAVA之二叉树
数据结构·算法