参考
问题
在玩扑克牌时定义出一种扑克置换 fn,v (共n张牌,v张置底取顶这种操作形成的置换) 在无意的尝试中发现了 f5,2=f5,1−1的奇特现象 计算机验证了n在3000以内, 找不到 fn,k=fn,t−1(k=t) 的其他解, 因此有极大概率 n,k,t= 5,2,1 以外的特解不存在。
扑克置换定理

f 表示对单张牌状态的变换, f~表示对整个牌状态的变换
f 是一种操作,牌的初始状态为 1,2,3,...,n,经过 f 作用后,牌的顺序变成了 f~(1,2,...,n)= f(1),f(2),f(3),...,f(n)
这个操作实际上定义出了一个函数,该函数是 {1,2,3,...,n} 上的一个置换。
(1f(1)2f(2)⋯⋯nf(n)):=f~(1,2,...,n)=f(1),f(2),...,f(n)
目标 :求一个初始状态 s,使得 f~(s)=1,2,...,n。
很容易猜出 s=f−1(1),f−1(2),...,f−1(n),这里 f−1 是指 f 的反函数。所以扑克置换问题最终归结为求函数 f−1。
证明
- P1 : 将 s=f−1(1),f−1(2),...,f−1(n) 代入 f~(x)
- P2 :
f~(s)=f~(f−1(1),f−1(2),...,f−1(n))=f(f−1(1)),f(f−1(2)),...,f(f−1(n))=1,2,3,...,n
注 :具体的操作 f 可能很复杂,因此函数 f 通常用列表法表示。
f−1 的求法
具体求 f−1,总结了如下三种方法:
方法 1: 列表法
对 1,2,...,n 施加 f 得到状态 f(1),f(2),...,f(n),根据 f 再列出 f−1(1),f−1(2),...,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=f0=fn。 两侧乘以 f−1 得到 f−1=fn−1。
具体做法 :对 1,2,...,n 连续施加 n−1 次 f,即可得到 f−1。
例子
用循环置换将 f1,f2,f3,f4,f5 列出如下(其中 f4=f−1):
| x | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| f1(x) | 2 | 4 | 1 | 5 | 3 |
| f2(x) | 4 | 5 | 2 | 3 | 1 |
| f3(x) | 5 | 3 | 4 | 1 | 2 |
| f4(x) | 3 | 1 | 5 | 2 | 4 |
| f5(x) | 1 | 2 | 3 | 4 | 5 |
方法 3: 对偶原理
这个方法最野,也最快。 这不是严谨数学, 这里只是一个能解决问题的技巧 牌区分 手上,桌上,不是简单的一个牌序 这要靠想象力,将电影倒放 这里分析了词项,猜测尝试得到了有效的对偶操作 两类对偶的结果 对 词项的解释不同
对偶原理.csdn 博大精深,但找到恰当的对偶对是有点困难的。
F 是一个操作, F 的对偶是 F−1。 F−1=逆操作F。
F是一个操作单元,重复施加5次F 形成了最终的置换f
操作约定
桌底的牌比手上的牌更靠下方 手上的牌经过一次F操作,会转移到桌子最上方
- 置底: 把手上最顶的牌放到手上最底
- 取顶: 把手上最顶的牌放到桌子上
- 置顶: 把手上最底的牌放到手上最顶
- 取底: 把手上最底的牌放到桌子上
- 个体词:底,顶
- 谓词:置,取
逆变换操作
这是不严格的,是复合运算求逆的尝试和拼凑
复合的逆公式为: (A∘B)−1=B−1∘A−1
(置底∘取顶)−1=取顶−1∘置底−1=置顶∘取底
F : 置底∘取顶 F−1 : 置顶∘取底
Fi :指 F操作了 i次 Fi−1 : 指 F−1操作了 i次
初始状态手上牌的状态
生成状态S的过程
| 初始状态F0−1 | F1−1 | F2−1 | F3−1 | F4−1 | F5−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的过程 先放桌子上的牌是先亮出来的
| 初始状态F0 | F1 | F2 | F3 | F4 | F5 |
|---|---|---|---|---|---|
| 手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: 卓 A0 : 手下 A1 : 手上 B0 : 卓下 B1 : 卓上 A1A0 : 手上 转移 到 手下 A0B0 : 手下 转移 到 卓下 A1B1 : 手上 转移 到 卓上 (A1A0)′=A0A1: 手下 转移 到 手上
(置底∘取顶)′=取顶′∘置底′=取底∘置顶 左侧= A1A0∘ A1B0 (A1A0∘ A1B0)′=B0A1∘A0A1
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):=fn,k=fn,t−1 σv:=fn,v(n固定)
因为fn,v 的解析式很难构造出,让人感觉无从下手
扑克置换fn,v : 共n张牌,手上顶部的v张牌,置手底.再取手顶上的牌,倒扣到桌上的操作(当手上牌数 ≤v,则手上的v张牌全倒扣在桌子上) 是否存在 n,k,t使得 fn,k=fn,t−1 首先已经验证 n,k,t=5,2,1 是成立的, 问题是 当 n,k,t满足什么关系时 fn,k=fn,t−1 才成立
或者说 Fn={fn,v∣0≤v<n}
F=n≥1⋃Fn={fn,v∣n≥1, 0≤v<n}.
中 n,k,t=5,2,1 是 fn,k=fn,t−1 成立的一个组合,是否有高效的算法找到更多的其他组合?
分析 F(n,k,t)
- 首先 fn,v和 fn,v−1的阶一样,即 ord(fn,k)=ord(fn,t)这能减少一些计算量
- fn,v=fn,v−1 是满足自逆的大类
- 可以通过对 1,2...n连续施加两次能回到 1,2...n验证
- Fn={fn,v∣0≤v<n} 只是 Sn的一个很小子集,一般不构成群 如果 Fn能够构成一个群,则有概率存在非平凡特解
- fn,0=σ0是Sn的单位元
- fn,v(1)=σv(1)=v+1
- 其中 fn,0=fn,0−1 是自逆类的平凡解
- 其中 fn,k=fn,t−1 ( k=t)是非平凡解
- 非平凡解是非常罕见的, 甚至有很大概率只有 5,2,1 这一组
| n | k | t |
|---|---|---|
| m | 0 | 0 |
| 3 | 1 | 1 |
| 5 | 2 | 1 |
Fn 不是 Sn的子群 ( n≥3)
因为 Fn(n≥3)无法构成群,所以 F(n,k,t)能找到非平凡解的概率很低
假设 Fn 是 Sn的1个n阶子群
- τ=fn,1∘fn,2∈Fn
- fn,0是 Sn的单位元 e
- fn,v(1)=σv(1)=v+1
- τ(1)=fn,1(fn,2(1))=fn,1(3)=3
- 如果 τ∈Fn,则必有 τ=fn,2
- τ=fn,1∘fn,2=fn,2 所以 fn,1=e (矛盾)
- 假设不成立
编程实现 f(n,t,s)
f(5,2,f(5,1)) =1, 2, 3, 4, 5
js
/**
* 模拟 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≤3000 范围内 只有 5,2,1 这一组非平凡解 所以大概率不存在其他非平凡解了
bash
用法: ./pkzh -start_n=<起始n> -max_n=<终止n>
# 已经验证了
./pkzh -start_n=1 -max_n=3000
main.cpp
c
#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
bash
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)