[算法专项] 托康展开+约瑟夫环

3038 字
15 分钟
[算法专项] 托康展开+约瑟夫环

托康展开+约瑟夫环#

算法讲解056【必备】并查集-上_哔哩哔哩_bilibili

更好的本文阅读体验

[算法思想]#

康托展开的核心思想是利用阶乘数系将排列的字典序排名与排列本身建立一一对应的双射关系。其原理是按位统计“当前位之后尚未使用的元素中,比当前元素小的个数”,并将该计数乘以剩余元素个数的阶乘,最后将各结果累加,从而得到该排列在所有全排列中的唯一字典序序位(通常加 1 转为 1-based 排名)。这一思想同样支持逆向操作,能够根据排名反推出原始排列,常用于状态压缩、哈希去重及解决排列的计数与定位问题。

托康展开原理#

康托展开是一个双射,它将一个排列映射为一个整数,该整数表示该排列在所有排列中的字典序排名(从 0 开始)。

rank[0,N!1]\text{rank} \in [0, N! - 1]

对于一个排列 X=(X1,X2,,XN)X = (X_1, X_2, \dots, X_N)

rank(X)=i=1Nai×(Ni)!\text{rank}(X) = \sum_{i=1}^{N} a_i \times (N - i)!

其中 aia_i = 在 X[i] 后面出现的、比 X[i] 小的数的个数

等价于:在当前未使用的数字中,有多少个比 X[i] 小的数

这个查询可以通过树状数组进行查询


康托展开
def cantor_rank(X):
N = len(X)
fact = [1] * (N + 1)
for i in range(1, N + 1):
fact[i] = fact[i-1] * i
rank = 0
used = [False] * (N + 1)
for i in range(N):
# 统计未使用且小于 X[i] 的数字个数
cnt = 0
for num in range(1, X[i]):
if not used[num]:
cnt += 1
rank += cnt * fact[N - i - 1]
used[X[i]] = True
return rank

把排列看作一个 N 位数字,每位可以选剩余未用的数字:

第1位:有 N 种选择 → rank 变化量 = 选第 k 小的数,跳过 (k-1) × (N-1)! 个排列
第2位:有 N-1 种选择 → rank 变化量 = 选第 k 小的数,跳过 (k-1) × (N-2)! 个排列
...

本质:康托展开就是计算”有多少个排列排在它前面”。


逆康托展开#

给定 rank,还原排列:

逆康托展开
def cantor_inverse(N, rank):
fact = [1] * (N + 1)
for i in range(1, N + 1):
fact[i] = fact[i-1] * i
available = list(range(1, N + 1))
res = []
for i in range(N):
idx = rank // fact[N - i - 1]
rank %= fact[N - i - 1]
res.append(available.pop(idx))
return res

[托康展开]#

洛谷P5367#

题目#

1N1\sim N 的一个给定全排列在所有 1N1\sim N 全排列中的排名。结果对 998244353998244353 取模。

树状数组+托康展开#

c++
#include <bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
class Fenwick {
private:
int n;
vector<int> bit;
public:
Fenwick(int n) : n(n), bit(n + 1, 0) {}
void add(int idx, int val) {
while (idx <= n) {
bit[idx] = (bit[idx] + val) % MOD;
idx += idx & -idx;
}
}
int sum(int idx) {
int res = 0;
while (idx > 0) {
res = (res + bit[idx]) % MOD;
idx -= idx & -idx;
}
return res;
}
};
int tuo_kang(vector<int>& a) {
int n = a.size();
vector<long long> fact(n + 1, 1);
for (int i = 1; i <= n; i++) {
fact[i] = fact[i - 1] * i % MOD;
}
Fenwick bit(n);
for (int i = 1; i <= n; i++) {
bit.add(i, 1);
}
long long rank = 0;
for (int i = 0; i < n; i++) {
int cnt = bit.sum(a[i] - 1);
rank = (rank + cnt * fact[n - i - 1]) % MOD;
bit.add(a[i], -1);
}
return (rank + 1) % MOD;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
cout << tuo_kang(a) << '\n';
return 0;
}
py
MOD = 998244353
class Fenwick:
def __init__(self, n):
self.n = n
self.bit = [0] * (n + 1)
def add(self, idx, val):
while idx <= self.n:
self.bit[idx] += val
idx += idx & -idx
def sum(self, idx):
res = 0
while idx > 0:
res += self.bit[idx]
idx -= idx & -idx
return res
def tuo_kang(a):
n = len(a)
fact = [1] * (n + 1)
for i in range(1, n + 1):
fact[i] = fact[i - 1] * i % MOD
bit = Fenwick(n)
for i in range(1, n + 1):
bit.add(i, 1)
rank = 0
for i in range(n):
cnt = bit.sum(a[i] - 1)
rank = (rank + cnt * fact[n - i - 1]) % MOD
bit.add(a[i], -1)
return (rank + 1) % MOD
def main():
n = int(input())
a = list(map(int,input().split()))
print(tuo_kang(a))
if __name__ == "__main__":
main()

ABC468C-DFS/康托展开#

题目#

给你一个整数 NN 和整数序列 P=(P1,P2,,PN)P=(P_1,P_2,\ldots, P_N)Q=(Q1,Q2,,QN)Q=(Q_1,Q_2,\ldots,Q_N) ,每个序列都是 (1,2,,N)(1,2,\ldots,N) 的排列。

请问有多少个整数序列是 (1,2,,N)(1,2,\ldots,N) 的置换序列,且其序数大于 PP ,小于 QQ

什么是整数序列的词序?

对于整数序列 S=(S1,S2,,SS)S = (S_1,S_2,\ldots,S_{|S|})T=(T1,T2,,TT)T = (T_1,T_2,\ldots,T_{|T|}) ,如果下面的 1.1.2.2. 成立,我们就说 SS 在词典上小于 TT 。这里, S,T|S|, |T| 分别表示 S,TS, T 的长度。

  1. S<T|S| \lt |T|(S1,S2,,SS)=(T1,T2,,TS)(S_1,S_2,\ldots,S_{|S|}) = (T_1,T_2,\ldots,T_{|S|})
  2. 存在一个整数 1imin{S,T}1 \leq i \leq \min\lbrace |S|, |T| \rbrace ,使得以下两个条件都成立。
    • (S1,S2,,Si1)=(T1,T2,,Ti1)(S_1,S_2,\ldots,S_{i-1}) = (T_1,T_2,\ldots,T_{i-1})
    • SiS_i 在数值上小于 TiT_i

DFS#

排列型DFS

DFS-py
def dfs(n, p, q):
used = [False] * (n + 1)
ans = 0
def dfs(pos, cur):
nonlocal ans
if pos == n:
if p < cur < q:
ans += 1
return
for num in range(1, n + 1):
if not used[num]:
if cur and cur < p[:pos] and cur > q[:pos]:
continue
used[num] = True
cur.append(num)
dfs(pos + 1, cur)
cur.pop()
used[num] = False
dfs(0, [])
return ans
n = int(input())
p = list(map(int,input().split()))
q = list(map(int,input().split()))
print(dfs(n,p,q))
DFS-c++
#include <bits/stdc++.h>
using namespace std;
int dfs(int n, vector<int>& p, vector<int>& q) {
vector<bool> used(n + 1, false);
int ans = 0;
function<void(int, vector<int>&)> dfs_impl = [&](int pos, vector<int>& cur) {
if (pos == n) {
if (cur > p && cur < q) {
ans++;
}
return;
}
for (int num = 1; num <= n; num++) {
if (!used[num]) {
// 剪枝:如果前缀已经不可能在区间内
bool prefix_ok = true;
if (pos > 0) {
bool less_than_p = true, greater_than_q = true;
for (int k = 0; k < pos; k++) {
if (cur[k] >= p[k]) less_than_p = false;
if (cur[k] <= q[k]) greater_than_q = false;
}
if (less_than_p && greater_than_q) {
prefix_ok = false;
}
}
if (!prefix_ok) continue;
used[num] = true;
cur.push_back(num);
dfs_impl(pos + 1, cur);
cur.pop_back();
used[num] = false;
}
}
};
vector<int> cur;
dfs_impl(0, cur);
return ans;
}
int main() {
int n;
cin >> n;
vector<int> p(n), q(n);
for (int i = 0; i < n; i++) cin >> p[i];
for (int i = 0; i < n; i++) cin >> q[i];
cout << dfs(n, p, q) << endl;
return 0;
}

库函数#

使用permutations直接生成全排列

permutations
from itertools import permutations
n = int(input())
p = list(map(int, input().split()))
q = list(map(int, input().split()))
ans = 0
for a in permutations([i + 1 for i in range(n)]):
ans += p < list(a) < q
print(ans)

康托展开#

托康排序+树状数组优化
MOD = 998244353
class Fenwick:
def __init__(self, n):
self.n = n
self.bit = [0] * (n + 1)
def add(self, idx, val):
while idx <= self.n:
self.bit[idx] += val
idx += idx & -idx
def sum(self, idx):
res = 0
while idx > 0:
res += self.bit[idx]
idx -= idx & -idx
return res
def tuo_kang(a):
n = len(a)
fact = [1] * (n + 1)
for i in range(1, n + 1):
fact[i] = fact[i - 1] * i % MOD
bit = Fenwick(n)
for i in range(1, n + 1):
bit.add(i, 1)
rank = 0
for i in range(n):
cnt = bit.sum(a[i] - 1) # 未使用且小于 a[i] 的个数
rank = (rank + cnt * fact[n - i - 1]) % MOD
bit.add(a[i], -1) # 标记 a[i] 已使用
return (rank + 1) % MOD
def main():
n = int(input())
a = list(map(int,input().split()))
b = list(map(int,input().split()))
print(tuo_kang(b) - tuo_kang(a) - 1 if tuo_kang(b) - tuo_kang(a) - 1 > 0 else 0)
if __name__ == "__main__":
main()

[逆托康展开]#

逆康托展开:已知 rank,从高位到低位依次还原 cnt_i,然后从未使用的数字中选出第 cnt_i + 1 小的数字。

核心要点在于用阶乘进制表示排列

对于给定的一个排名rank

从高位到低位逐位确定

对于位置 i(0-based):

  • fact = fact[n - 1 - i](剩余位置数的阶乘)
  • idx = rank // fact(当前位选择的数字在 nums 中的下标)
  • ans[i] = nums[idx]
  • 从 nums 中删除 nums[idx]
  • rank %= fact
列子
n = 5, rank = 3 (0-based)
fact = [1, 1, 2, 6, 24, 120]
nums = [1, 2, 3, 4, 5]
i=0: fact = 4! = 24, idx = 3//24 = 0, ans[0]=nums[0]=1, nums=[2,3,4,5], rank=3
i=1: fact = 3! = 6, idx = 3//6 = 0, ans[1]=nums[0]=2, nums=[3,4,5], rank=3
i=2: fact = 2! = 2, idx = 3//2 = 1, ans[2]=nums[1]=4, nums=[3,5], rank=1
i=3: fact = 1! = 1, idx = 1//1 = 1, ans[3]=nums[1]=5, nums=[3], rank=0
i=4: fact = 0! = 1, idx = 0//1 = 0, ans[4]=nums[0]=3, nums=[]
结果: [1, 2, 4, 5, 3]

如何快速找到第idx+1小的数字?

树状数组+二分
# 维护一个树状数组,初始所有位置为 1
def find_kth(k): # 找第 k 小的未使用数字(1-based)
l, r = 1, n
while l < r:
mid = (l + r) // 2
if bit.sum(mid) >= k:
r = mid
else:
l = mid + 1
return l

洛谷U72177#

逆托康展开#

逆托康展开py
import sys
class Fenwick:
def __init__(self, n):
self.n = n
self.bit = [0] * (n + 1)
def add(self, idx, val):
while idx <= self.n:
self.bit[idx] += val
idx += idx & -idx
def sum(self, idx):
res = 0
while idx > 0:
res += self.bit[idx]
idx -= idx & -idx
return res
def get_fact(n):
"""预处理阶乘数组"""
fact = [1] * (n + 1)
for i in range(1, n + 1):
fact[i] = fact[i - 1] * i
return fact
def cantor(a, fact):
"""
康托展开:求排列的排名(0-based)
a: 1..n 的排列
fact: 阶乘数组
返回: 0-based 排名
"""
n = len(a)
bit = Fenwick(n)
for i in range(1, n + 1):
bit.add(i, 1)
rank = 0
for i, x in enumerate(a):
cnt = bit.sum(x - 1) # 比 x 小的未使用元素个数
rank += cnt * fact[n - i - 1]
bit.add(x, -1)
return rank
def inv_cantor(n, rank, fact):
"""
逆康托展开:根据排名还原排列
n: 排列长度
rank: 0-based 排名
fact: 阶乘数组
返回: 排列列表
"""
bit = Fenwick(n)
for i in range(1, n + 1):
bit.add(i, 1)
ans = []
for i in range(n):
k = rank // fact[n - i - 1] + 1 # 第 k 小(1-based)
rank %= fact[n - i - 1]
# 二分查找第 k 小的未使用数字
l, r = 0, n + 1
while l + 1 < r:
mid = (l + r) // 2
if bit.sum(mid) >= k:
r = mid
else:
l = mid
ans.append(r)
bit.add(r, -1)
return ans
def solve():
input = sys.stdin.readline
n = int(input())
m = int(input())
a = list(map(int, input().split()))
# 预处理阶乘
fact = get_fact(n)
# 康托展开求当前排名
rank = cantor(a, fact)
# 加上 M
new_rank = rank + m
# 逆康托展开还原排列
ans = inv_cantor(n, new_rank, fact)
print(' '.join(map(str, ans)))
if __name__ == "__main__":
solve()
逆托康展开c++

CF501D#

题目#

人话:

给定两个排列p和q,求出这两个排列的排名和,之后求出这个排名的排列

逆托康展开#

逆托康展开
import sys
class Fenwick:
def __init__(self, n):
self.n = n
self.bit = [0] * (n + 1)
def add(self, idx, val):
while idx <= self.n:
self.bit[idx] += val
idx += idx & -idx
def sum(self, idx):
res = 0
while idx > 0:
res += self.bit[idx]
idx -= idx & -idx
return res
def kth(self, k):
"""返回第 k 小的未使用数字(1-based)"""
l, r = 0, self.n
while l + 1 < r:
mid = (l + r) // 2
if self.sum(mid) >= k:
r = mid
else:
l = mid
return r
def get_coeff(perm):
"""
将排列转换为阶乘数制系数数组
coeff[i] = 在第 i 位,比 perm[i] 小且未使用的数字个数
"""
n = len(perm)
bit = Fenwick(n)
for i in range(1, n + 1):
bit.add(i, 1)
coeff = [0] * n
for i, x in enumerate(perm):
coeff[i] = bit.sum(x) # 注意:x 是 0-based,所以比 x 小的个数 = sum(x)
bit.add(x + 1, -1) # 删除 x
return coeff
def recover_perm(coeff):
"""
从阶乘数制系数数组还原排列
"""
n = len(coeff)
bit = Fenwick(n)
for i in range(1, n + 1):
bit.add(i, 1)
ans = [0] * n
for i, c in enumerate(coeff):
k = c + 1 # 第 k 小(1-based)
num = bit.kth(k)
ans[i] = num - 1
bit.add(num, -1)
return ans
def solve():
input = sys.stdin.readline
n = int(input())
p = list(map(int, input().split()))
q = list(map(int, input().split()))
cp = get_coeff(p)
cq = get_coeff(q)
# 阶乘数制相加(从低位到高位)
res = [0] * n
carry = 0
for i in range(n - 1, -1, -1):
base = n - i # 当前位的进制:从 1 到 n
s = cp[i] + cq[i] + carry
res[i] = s % base
carry = s // base
# carry 忽略(模 n!)
ans = recover_perm(res)
print(' '.join(map(str, ans)))
if __name__ == "__main__":
solve()

[约瑟夫环]#

n 个人的编号是 1∼n,如果他们依编号按顺时针排成一个圆圈,从编号是 1 的人开始顺时针报数。

(报数是从 1 报起)当报到 k 的时候,这个人就退出游戏圈。下一个人重新从 1 开始报数。

求最后剩下的人的编号。这就是著名的约瑟夫环问题。

本题目就是已知 n,k 的情况下,求最后剩下的人的编号。

每次出队一人,在约瑟夫环上的每个人都向前移动了 kk 位。 那么如果要反着来,就是每次加上一个人,在约瑟夫环上的每个人都向后移动了 kk 位。 易得剩下一个人的时候,最终答案对应的下标为 F(1)=0F(1)=0。 那么加上一个人,最终答案对应的下标就是 F(2)=(F(1)+k)mod2F(2)=(F(1)+k)mod2。 再加上一个人,最终答案对应的下标就是 F(3)=(F(2)+k)mod3F(3)=(F(2)+k)mod3。 以此类推, 那么可以得出递推公式:

  • F(1)=0F(1)=0
  • F(n)=(F(n1)+k)modn F(n)=(F(n−1)+k)modn 于是就可以求答案了。
约瑟夫环
def josephus(n, k):
ans = 0 # f(1, k) = 0
for i in range(2, n + 1):
ans = (ans + k) % i
return ans
def main():
n,k = map(int,input().split())
print(josephus(n,k) + 1)
if __name__ == "__main__":
main()

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

[算法专项] 托康展开+约瑟夫环
http://blog.7a7a68.xyz/posts/铜牌算法托康展开约瑟夫环/
作者
JulY
发布于
2026-08-18
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
JulY
愿你明日如绚丽之花.
公告
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
76
分类
4
标签
20
总字数
160,526
运行时长
0
最后活动
0 天前

目录