扑克置换问题
参考
问题
在玩扑克牌时定义出一种扑克置换$f_{n,v}$ (共n张牌,v张置底取顶这种操作形成的置换)
在无意的尝试中发现了 $f_{5,2}=f^{-1}{5,1}$的奇特现象
计算机验证了n在3000以内, 找不到$f=f_{n,t}^{-1} (k \ne t)$ 的其他解,
因此有极大概率 n,k,t= 5,2,1 以外的特解不存在。
扑克置换定理
$f$ 表示对单张牌状态的变换,
$\tilde f$表示对整个牌状态的变换
$f$ 是一种操作,牌的初始状态为 $1, 2, 3, \dots, n$,经过 $f$ 作用后,牌的顺序变成了 $\tilde f(1, 2, \dots, n)$=$f(1), f(2), f(3), \dots, f(n)$
这个操作实际上定义出了一个函数,该函数是 ${1, 2, 3, \dots, n}$ 上的一个置换。
$$
\begin{pmatrix}
1 & 2 & \cdots & n \
f(1) & f(2) & \cdots & f(n)
\end{pmatrix} := \tilde f(1, 2, \dots, n) = f(1), f(2), \dots, f(n)
$$
目标:求一个初始状态 $s$,使得 $\tilde f(s) = 1, 2, \dots, n$。
很容易猜出 $s = f^{-1}(1), f^{-1}(2), \dots, f^{-1}(n)$,这里 $f^{-1}$ 是指 $f$ 的反函数。所以扑克置换问题最终归结为求函数 $f^{-1}$。
证明
- P1: 将 $s = f^{-1}(1), f^{-1}(2), \dots, f^{-1}(n)$ 代入 $\tilde f(x)$
- P2:
$$
\begin{aligned}
\tilde f(s) &= \tilde f(f^{-1}(1), f^{-1}(2), \dots, f^{-1}(n)) \
&= f(f^{-1}(1)), f(f^{-1}(2)), \dots, f(f^{-1}(n)) \
&= 1, 2, 3, \dots, n
\end{aligned}
$$
注:具体的操作 $f$ 可能很复杂,因此函数 $f$ 通常用列表法表示。
$f^{-1}$ 的求法
具体求 $f^{-1}$,总结了如下三种方法:
方法 1: 列表法
对 $1, 2, \dots, n$ 施加 $f$ 得到状态 $f(1), f(2), \dots, f(n)$,根据 $f$ 再列出 $f^{-1}(1), f^{-1}(2), \dots, f^{-1}(n)$。
例子
$f$ 为 1 张置底取顶,使用观察法得到 $f^{-1}$:
| $x$ | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| $f(x)$ | 2 | 4 | 1 | 5 | 3 |
| $f^{-1}(x)$ | 3 | 1 | 5 | 2 | 4 |
方法 2: 循环置换
这个方法无需动脑,但很费手。$f$是个 n-循环置换 ,$f$阶为 $n$,所以 $e = f^0 = f^n$。
两侧乘以 $f^{-1}$ 得到 $f^{-1} = f^{n-1}$。
具体做法:对 $1, 2, \dots, n$ 连续施加 $n-1$ 次 $f$,即可得到 $f^{-1}$。
例子
用循环置换将 $f^1, f^2, f^3, f^4, f^5$ 列出如下(其中 $f^4 = f^{-1}$):
| $x$ | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| $f^1(x)$ | 2 | 4 | 1 | 5 | 3 |
| $f^2(x)$ | 4 | 5 | 2 | 3 | 1 |
| $f^3(x)$ | 5 | 3 | 4 | 1 | 2 |
| $f^4(x)$ | 3 | 1 | 5 | 2 | 4 |
| $f^5(x)$ | 1 | 2 | 3 | 4 | 5 |
方法 3: 对偶原理
这个方法最野,也最快。
这不是严谨数学, 这里只是一个能解决问题的技巧
牌区分 手上,桌上,不是简单的一个牌序
这要靠想象力,将电影倒放
这里分析了词项,猜测尝试得到了有效的对偶操作
两类对偶的结果 对 词项的解释不同
对偶原理.csdn 博大精深,但找到恰当的对偶对是有点困难的。
$F$ 是一个操作,$F$ 的对偶是 $F^{-1}$。 $F^{-1} = \text{逆操作}F$。
F是一个操作单元,重复施加5次F 形成了最终的置换f
操作约定
桌底的牌比手上的牌更靠下方
手上的牌经过一次F操作,会转移到桌子最上方
- 置底: 把手上最顶的牌放到手上最底
- 取顶: 把手上最顶的牌放到桌子上
- 置顶: 把手上最底的牌放到手上最顶
- 取底: 把手上最底的牌放到桌子上
- 个体词:底,顶
- 谓词:置,取
逆变换操作
这是不严格的,是复合运算求逆的尝试和拼凑
复合的逆公式为: $(A \circ B)^{-1} = B^{-1} \circ A^{-1}$
$(置_底∘取_顶)^{-1} =取_顶^{-1} ∘置_底^{-1} =置_顶∘取_底$
$F$:$置底∘取顶$
$F^{-1}$:$置顶∘取底$
$F_i$ :指$F$操作了$i$次
$F_i^{-1}$: 指$F^{-1}$操作了$i$次
初始状态手上牌的状态
生成状态S的过程
| $初始状态F_0^{-1}$ | $F_1^{-1}$ | $F_2^{-1}$ | $F_3^{-1}$ | $F_4^{-1}$ | $F_5^{-1}$ |
|---|---|---|---|---|---|
| 手1 | 手5 | 手3 | 手1 | 手 3 | 卓3 |
| 手2 | 手1 | 手5 | 手3 | 卓1 | 卓1 |
| 手3 | 手2 | 手1 | 卓5 | 卓5 | 卓5 |
| 手4 | 手3 | 卓2 | 卓2 | 卓2 | 卓2 |
| 手5 | 卓4 | 卓4 | 卓4 | 卓4 | 卓4 |
验证状态S的过程
先放桌子上的牌是先亮出来的
| $初始状态F_0$ | $F_1$ | $F_2$ | $F_3$ | $F_4$ | $F_5$ |
|---|---|---|---|---|---|
| 手3 | 手5 | 手4 | 手5 | 手 5 | 卓5 |
| 手1 | 手2 | 手3 | 手4 | 卓4 | 卓4 |
| 手5 | 手4 | 手5 | 卓3 | 卓3 | 卓3 |
| 手2 | 手3 | 卓2 | 卓2 | 卓2 | 卓2 |
| 手4 | 卓1 | 卓1 | 卓1 | 卓1 | 卓1 |
逆时间下的对偶操作
这是严格的,有逻辑的,就是时间反演下的操作的直接翻译
手上牌 和 桌上牌 都是牌面朝下
左侧的$置_底$是手上的顶置手上的底
左侧的$取_顶$是取手上的顶到桌上的底
右侧的$取_底$是取桌上牌的底到手上的顶
右侧的$置_顶$是 手上牌的底到手上的顶
$(置_底∘取_顶)' =取_顶' ∘置_底' =取_底∘置_顶$
操作的精细化表达
这样表达才是清晰精确的
$A$ :手
$B$: 卓
$A_0$ : 手下
$A_1$ : 手上
$B_0$ : 卓下
$B_1$ : 卓上
$A_1A_0$ : 手上 转移 到 手下
$A_0B_0$ : 手下 转移 到 卓下
$A_1B_1$ : 手上 转移 到 卓上
$(A_1A_0)'=A_0A_1$: 手下 转移 到 手上
$(置_底∘取_顶)' =取_顶' ∘置_底' =取_底∘置_顶$
左侧=$A_1A_0$∘$A_1B_0$
$(A_1A_0$∘$A_1B_0)'=B_0A_1∘A_0A_1$
f的另一种神奇的逆g
$g$ 为 2 张置底取顶:
2张置底取顶操作g 和 1张置底取顶操作f 互为逆操作
| $x$ | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| $g(x)$ | 3 | 1 | 5 | 2 | 4 |
| $g^{-1}(x)$ | 2 | 4 | 1 | 5 | 3 |
️ 注意
在有 5 张牌时,1 张置底取顶 与 2 张置底取顶 互为反函数,但这不具有一般性。当牌多的时候,两者并无明显的关系。
$g=f^{-1}$的一般化和构造问题
这个和约瑟夫问题很像,但并不一样, 我还没有找到解决办法
约定
在操作过程中,手上牌面朝下, 桌上牌面朝上
最终的牌序是 将牌朝下, 顶上的牌为第一张
$$ F(n,k,t) := f_{n,k}=f_{n,t}^{-1}$$
$$ σ_v := f_{n,v} (n固定) $$
$因为f_{n,v}$ 的解析式很难构造出,让人感觉无从下手
$扑克置换f_{n,v}$ : 共n张牌,手上顶部的v张牌,置手底.再取手顶上的牌,倒扣到桌上的操作(当手上牌数$\le v$,则手上的v张牌全倒扣在桌子上)
是否存在 n,k,t使得$f_{n,k}=f_{n,t}^{-1}$
首先已经验证 $n,k,t=5,2,1$ 是成立的,
问题是 当 n,k,t满足什么关系时 $f_{n,k}=f_{n,t}^{-1}$ 才成立
或者说
$$\mathcal{F}n={,f\mid 0\le v<n,}$$
$$
\mathcal{F}
=\bigcup_{n\ge1}\mathcal{F}n
={,f\mid n\ge1,\ 0\le v<n,}.
$$
中 $n,k,t=5,2,1$ 是 $f_{n,k}=f_{n,t}^{-1}$ 成立的一个组合,是否有高效的算法找到更多的其他组合?
分析 $F(n,k,t)$
- 首先$f_{n,v}$和$f^{-1}{n,v}$的阶一样,即$ord(f) =ord(f_{n,t})$这能减少一些计算量
- $f_{n,v}=f^{-1}_{n,v}$ 是满足自逆的大类
- 可以通过对$1,2...n$连续施加两次能回到$1,2...n$验证
- $\mathcal{F}n={,f\mid 0\le v<n,}$ 只是 $S_n$的一个很小子集,一般不构成群
如果$\mathcal{F}_n$能够构成一个群,则有概率存在非平凡特解 - $f_{n,0}=σ_0是S_n的单位元$
- $f_{n,v}(1)=σ_v(1)=v+1$
- 其中$f_{n,0}=f_{n,0}^{-1}$ 是自逆类的平凡解
- 其中$f_{n,k}=f_{n,t}^{-1}$ ($k \ne t$)是非平凡解
- 非平凡解是非常罕见的, 甚至有很大概率只有 5,2,1 这一组
| n | k | t |
|---|---|---|
| m | 0 | 0 |
| 3 | 1 | 1 |
| 5 | 2 | 1 |
$\mathcal{F}_n$ 不是$S_n$的子群 ($n \ge3$)
因为 $\mathcal{F}_n(n \ge3)$无法构成群,所以$F(n,k,t)$能找到非平凡解的概率很低
假设 $\mathcal{F}_n$ 是$S_n$的1个n阶子群
- $τ=f_{n,1} ∘f_{n,2} \in \mathcal{F}_n$
- $f_{n,0}$是$S_n$的单位元$e$
- $f_{n,v}(1)=σ_v(1)=v+1$
- $τ(1)=f_{n,1}(f_{n,2}(1))=f_{n,1}(3)=3$
- 如果 $τ \in \mathcal{F}n$,则必有$τ=f$
- $τ=f_{n,1}∘f_{n,2}=f_{n,2}$ 所以$f_{n,1}=e$ (矛盾)
- 假设不成立
编程实现 f(n,t,s)
f(5,2,f(5,1)) =[1, 2, 3, 4, 5]
/**
* 模拟 f 操作:将手牌每次将顶部 t 张移到底部,然后取 1 张放到桌上。
* @param {number} n - 牌的总数(用于生成默认牌序)
* @param {number} t - 每次置底的牌数
* @param {number[]} [s] - 初始手牌数组。如果不填,默认生成 [1, 2, ..., n]
* @returns {number[]} - 返回出牌顺序的数组
*/
function f(n, t, s = Array.from({ length: n }, (_, i) => i + 1)) {
// 防御性处理:如果传入的初始牌序为空,直接返回空数组
if (!s || s.length === 0) return [];
// 特殊情况:如果不置底,直接依次取走
if (t === 0) return [...s];
// 复制一份手牌,避免修改外部传入的原数组
let hand = [...s];
const result = []; // 记录桌上的牌(出牌顺序)
while (hand.length > 0) {
// 规则:当手上牌数 <= t,则手上的牌全倒扣在桌子上
if (hand.length <= t) {
result.push(...hand);
break;
}
// 1. 置底操作:将顶部 t 张牌移到底部
const topCards = hand.splice(0, t);
hand.push(...topCards);
// 2. 取顶操作:将新的顶部 1 张牌放到桌上
result.push(hand.shift());
}
return result;
}
// --- 测试验证 ---
f(5,2,f(5,1))
编程寻找 $F(n,k,t)$的非平凡解
随着n的增大,计算量暴涨
已经验证了在$n \le 3000$ 范围内 只有 5,2,1 这一组非平凡解
所以大概率不存在其他非平凡解了
用法: ./pkzh -start_n=<起始n> -max_n=<终止n>
# 已经验证了
./pkzh -start_n=1 -max_n=3000
main.cpp
#include <iostream>
#include <vector>
#include <numeric> // iota 函数头文件
#include <thread> // 多线程支持
#include <mutex> // 互斥锁
#include <fstream> // 文件操作
#include <sstream> // 字符串流
#include <string_view> // C++17 字符串视图
#include <cstdint> // 固定宽度整数类型
#include <algorithm> // std::gcd 和其他算法
using namespace std;
// 全局互斥锁,用于保护多线程环境下的输出
mutex io_mutex;
// 缓存每个 v 值对应的排列数据
struct SigmaCache {
vector<uint16_t> perm; // σ_v 的排列结果 (长度为 n)
vector<uint16_t> inv_perm; // σ_v 的逆排列 (满足 inv_perm[perm[i]-1] = i+1)
int ord; // 该排列的阶(最小正整数 k 使得 σ^k = identity)
};
/**
* 模拟"跳过 v 张牌取一张"的发牌过程
*
* 规则说明:
* 1. 初始牌堆为 [1, 2, 3, ..., n]
* 2. 从第 1 张牌开始,每次跳过 v 张牌(循环计数),取下一张牌
* 3. 重复直到牌堆为空
*
* @param n 牌堆总张数
* @param v 每次跳过的牌数
* @param init 初始牌堆(应为 [1,2,...,n])
* @param out 输出排列(结果将存储在此向量)
*/
void simulate_fast(int n, int v, const vector<uint16_t>& init, vector<uint16_t>& out)
{
out.clear();
vector<uint16_t> hand = init; // 复制初始牌堆(避免修改原数据)
int ptr = 0; // 当前指针位置(指向待跳过的起始位置)
while (!hand.empty()) {
// 情况1:剩余牌数 ≤ v,直接输出所有剩余牌(无需跳过)
if (hand.size() <= v) {
out.insert(out.end(), hand.begin(), hand.end());
break;
}
// 情况2:跳过 v 张牌(循环处理,自动处理越界)
ptr = (ptr + v) % hand.size();
// 取当前指针指向的牌并移除
out.push_back(hand[ptr]);
hand.erase(hand.begin() + ptr);
// 注意:erase 后 ptr 自动指向被删除元素的下一个位置
// 例如:[A,B,C] 删除 ptr=1(B) 后,ptr 自动指向 C(索引1)
}
// 保证输出长度恒为 n(关键修复点)
}
/**
* 计算排列的阶(order)
*
* 排列阶的定义:最小正整数 k 使得 σ^k = identity
* 计算方法:分解为不相交轮换,阶 = 各轮换长度的最小公倍数
*
* @param p 排列数组(0-indexed,p[i] 表示 i+1 映射到的值)
* @param n 排列长度
* @return 排列的阶
*/
int permutation_order(const vector<uint16_t>& p, int n)
{
if (n <= 1) return 1;
vector<bool> vis(n + 1, false); // 访问标记数组(1-indexed)
int order = 1; // 当前计算的阶
for (int i = 1; i <= n; ++i) {
if (vis[i]) continue; // 跳过已访问元素
// 追踪当前轮换
int cur = i;
int len = 0; // 轮换长度
while (!vis[cur]) {
vis[cur] = true;
cur = p[cur - 1]; // 注意:p 是 0-indexed,cur 是 1-indexed
len++;
}
// 更新阶:LCM(当前阶, 轮换长度)
int g = std::gcd(order, len);
order = order / g * len;
}
return order;
}
/**
* 工作线程函数:在指定区间 [start_k, end_k) 内搜索解
*
* 搜索条件:找到所有 (k,t) 满足 σ_t = σ_k^{-1} 且 ord(σ_k) = ord(σ_t)
*
* @param n 当前处理的 n 值
* @param start_k k 的搜索起始值(包含)
* @param end_k k 的搜索结束值(不包含)
* @param cache 预计算的 SigmaCache 数组(索引 0~n-1)
* @param local_results 线程本地结果存储
*/
void worker(int n, int start_k, int end_k,
const vector<SigmaCache>& cache,
vector<pair<int, int>>& local_results)
{
local_results.clear();
if (n <= 1) return;
for (int k = start_k; k < end_k; ++k) {
const auto& ck = cache[k]; // 获取 σ_k 的数据
int ok = ck.ord; // σ_k 的阶
const auto& inv_k = ck.inv_perm; // σ_k^{-1}
// 仅需检查 t > k(避免重复和自反情况)
for (int t = k + 1; t < n; ++t) {
// 剪枝1:阶必须相等
if (cache[t].ord != ok) continue;
// 剪枝2:检查 σ_t 是否等于 σ_k^{-1}
if (cache[t].perm == inv_k) {
local_results.emplace_back(k, t);
}
}
}
}
/**
* 并行搜索主函数
*
* 流程:
* 1. 对每个 n ∈ [start_n, max_n]
* 2. 生成所有 v ∈ [0, n-1] 对应的 σ_v
* 3. 并行搜索满足 σ_t = σ_k^{-1} 的 (k,t) 对
* 4. 将结果写入文件
*
* @param start_n 起始 n 值
* @param max_n 最大 n 值
* @param output_filename 输出文件名
* @param custom_threads 指定线程数(0 表示自动检测)
*/
void find_solutions_parallel(int start_n, int max_n,
const string& output_filename,
unsigned int custom_threads)
{
ofstream out_file(output_filename, ios::app); // 追加模式打开文件
if (!out_file.is_open()) {
cerr << "错误:无法打开文件 " << output_filename << endl;
return;
}
// 自动检测硬件线程数
unsigned int num_threads = custom_threads;
if (num_threads == 0) {
num_threads = thread::hardware_concurrency();
if (num_threads == 0) num_threads = 4; // 检测失败时默认值
}
cout << "搜索区间 n = " << start_n << " ~ " << max_n << endl;
cout << "并发线程数:" << num_threads << "\n------------------------\n";
for (int n = start_n; n <= max_n; ++n) {
// 处理 n=1 的特殊情况
if (n == 1) {
cout << ">>> n=1 遍历完成 \n";
continue;
}
// 创建初始牌堆 [1, 2, ..., n]
vector<uint16_t> identity(n);
iota(identity.begin(), identity.end(), 1);
// 预计算所有 v ∈ [0, n-1] 对应的排列数据
vector<SigmaCache> cache(n);
for (int v = 0; v < n; ++v) {
auto& entry = cache[v];
// 生成 σ_v 排列
simulate_fast(n, v, identity, entry.perm);
// 计算逆排列:inv_perm[x-1] = y+1 当且仅当 perm[y] = x
entry.inv_perm.resize(n);
for (int i = 0; i < n; ++i) {
uint16_t val = entry.perm[i]; // val = σ_v(i+1)
entry.inv_perm[val - 1] = i + 1; // σ_v^{-1}(val) = i+1
}
// 计算排列阶
entry.ord = permutation_order(entry.perm, n);
}
// ========== 并行搜索阶段 ==========
vector<thread> threads;
vector<vector<pair<int, int>>> thread_results(num_threads); // 每个线程的结果存储
int total_k = n; // k 的取值范围 [0, n-1]
int base = total_k / (int)num_threads; // 基础任务量
int rem = total_k % (int)num_threads; // 余数(分配给前 rem 个线程)
int cur = 0; // 当前任务起始位置
// 创建工作线程
for (unsigned int i = 0; i < num_threads; ++i) {
int chunk = base + (i < rem ? 1 : 0); // 任务块大小
int sk = cur; // 任务块起始 k
int ek = cur + chunk; // 任务块结束 k
cur = ek;
if (sk < n) {
// 启动线程处理 [sk, ek) 区间
threads.emplace_back(worker, n, sk, ek, cref(cache), ref(thread_results[i]));
}
}
// 等待所有线程完成
for (auto& th : threads) th.join();
// ========== 结果收集阶段 ==========
bool found = false;
{
// 加锁保护共享输出资源
lock_guard<mutex> lock(io_mutex);
// 合并所有线程的结果
for (auto& res : thread_results) {
for (auto [k, t] : res) {
// 打印发现的解(包含对称解)
cout << "[找到解] n=" << n << " k=" << k << " t=" << t << "\n";
cout << "[对称解] n=" << n << " k=" << t << " t=" << k << "\n";
// 发现解后立刻写入文件
out_file << n << "," << k << "," << t << "\n";
out_file << n << "," << t << "," << k << "\n";
// 立刻刷新到磁盘,确保数据不丢失
out_file.flush();
found = true;
}
}
// 输出当前 n 的状态
cout << ">>> n=" << n << " 遍历完成 " << (found ? "【存在解】" : "") << "\n";
}
}
out_file.close();
cout << "\n全部区间搜索完成,结果已保存\n";
}
/**
* 打印程序使用说明
*
* @param prog 程序名(argv[0])
*/
void print_usage(const char* prog)
{
cout << "用法: " << prog << " -start_n=xx -max_n=xx [选项]\n"
<< "必选:\n"
<< " -start_n=NUM 起始n\n"
<< " -max_n=NUM 终止n\n"
<< "可选:\n"
<< " -threads=NUM 线程数\n"
<< " -output=FILE 输出文件\n"
<< "示例:\n"
<< " ./pkzh -start_n=500 -max_n=1000 -threads=12 -output=500_1000.txt\n";
}
/**
* 主函数:解析命令行参数并启动搜索
*/
int main(int argc, char* argv[])
{
// 无参数时显示帮助
if (argc == 1) {
print_usage(argv[0]);
return 0;
}
int start_n = -1, max_n = -1;
unsigned int threads = 0;
string out_name = "solutions_output.txt"; // 默认输出文件
// 解析命令行参数
for (int i = 1; i < argc; ++i) {
string_view arg(argv[i]);
size_t eq = arg.find('=');
string_view key, val;
// 处理 key=value 形式
if (eq != string_view::npos) {
key = arg.substr(0, eq);
val = arg.substr(eq + 1);
}
// 处理 key value 形式(下一个参数作为值)
else {
key = arg;
if (i + 1 >= argc) {
cerr << "参数 " << arg << " 缺少值\n";
print_usage(argv[0]);
return 1;
}
val = argv[++i];
}
try {
if (key == "-start_n") start_n = stoi(string(val));
else if (key == "-max_n") max_n = stoi(string(val));
else if (key == "-threads") threads = stoul(string(val));
else if (key == "-output") out_name = string(val);
else {
cerr << "未知参数: " << key << "\n";
print_usage(argv[0]);
return 1;
}
} catch (...) {
cerr << "参数值必须是数字\n";
print_usage(argv[0]);
return 1;
}
}
// 验证必要参数
if (start_n == -1 || max_n == -1) {
cerr << "缺少 -start_n / -max_n\n";
print_usage(argv[0]);
return 1;
}
if (start_n > max_n) {
cerr << "start_n 不能大于 max_n\n";
return 1;
}
// 启动搜索
find_solutions_parallel(start_n, max_n, out_name, threads);
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(pkzh)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(pkzh main.cpp)
# 开启最高优化并针对当前CPU架构
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native")
# 线程依赖
find_package(Threads REQUIRED)
target_link_libraries(pkzh PRIVATE Threads::Threads)
浙公网安备 33010602011771号