扑克置换问题

参考

二面体群的表示.csdn

问题

在玩扑克牌时定义出一种扑克置换$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)
posted @ 2026-06-28 08:01  minglie  阅读(3)  评论(0)    收藏  举报