【算法速成课 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;
}
相关推荐
weixin_4241836112 小时前
开放—封闭原则实战(下):基于策略的流程编排与编译期多态
c++·分布式·系统架构
wuminyu13 小时前
C++协程实现接收端的零拷贝Buffer管理原理剖析
java·linux·c语言·jvm·c++
小蒋学算法13 小时前
算法-交替删除操作后最后剩下的整数-跟着灵茶山艾府学算法
数据结构·算法
m0_3807438713 小时前
C++中的freopen的用法实例详解
开发语言·c++
荆棘鸟智能13 小时前
林业遥感长势评估怎么做?从NDVI时序分析到LiDAR蓄积量回归
人工智能·算法·智慧林业
jimy114 小时前
polymorph.cpp里面的“析构函数、智能指针”问题
开发语言·c++
hhb_61814 小时前
AIRAGDebug:一键定位RAG链路异常
人工智能·python·算法
朝朝辞暮i15 小时前
C++ 第 6 课:switch —— 多状态选择
开发语言·c++·算法
朝朝辞暮i15 小时前
C++ 第 4 课:if / else —— 让程序自己做决定
开发语言·c++
苏打豆15 小时前
Pinocchio源码阅读——刚体运动物理量空间变换及求导
c++·线性代数·算法·矩阵·机器人