火龙信奥
  • 分享
  • 课程
  • 题库
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

下一个排列

作者: 作者的头像   huolong , 时间:2026-08-16 22:07:43 , 所有人可见, 阅读  49

在一个排列集合中,寻找某一个排列 $P_i$ 的下一个字典序排列 $P_{i+1}$ 是一个经典的组合数学与算法问题。著名的“下一个排列”(Next Permutation)算法(在 C++ 标准库中实现为 std::next_permutation)正是解决这一问题的最优方法。

该算法本质上利用了贪心策略来最小化排列变化的幅度。以下为您提供该问题的数学模型、贪心算法的设计、数学证明以及基于 C++11 的代码实现。


一、 数学模型与问题定义

1. 字典序的定义

设有两个长度为 $n$ 的排列 $A = (a_1, a_2, \dots, a_n)$ 和 $B = (b_1, b_2, \dots, b_n)$。 称 $B$ 的字典序大于 $A$(记作 $B > A$),当且仅当存在一个索引 $i \in [1, n]$,满足: * 对于所有 $1 \le j < i$,有 $b_j = a_j$; * 且 $b_i > a_i$。

2. 问题目标

给定当前排列 $P_i$,我们需要找到一个排列 $P_{i+1}$,使得: 1. $P_{i+1} > P_i$ 2. 不存在任何排列 $Q$,满足 $P_{i+1} > Q > P_i$。

即 $P_{i+1}$ 是严格大于 $P_i$ 的所有排列中字典序最小的那一个(即紧随其后的下一个排列)。


二、 贪心算法步骤

为了使增幅最小,我们需要贪心地保留尽可能长的公共前缀,并对后缀进行最小幅度的调整。

设当前排列为 $A = (a_1, a_2, \dots, a_n)$: 1. 寻找转折点(Pivot):从右向左遍历,找到第一个满足 $a_k < a_{k+1}$ 的位置 $k$。 * 如果不存在这样的 $k$(即整个序列是单调递减的),说明当前排列已经是最大的排列,没有下一个排列(通常可以将其翻转为升序,回到最小排列)。 2. 寻找替换数:在区间 $[k+1, n]$ 中,从右向左寻找第一个满足 $a_l > a_k$ 的位置 $l$。由于 $a[k+1 \dots n]$ 是单调递减的,这个 $a_l$ 必然是该区间内大于 $a_k$ 的最小数。 3. 交换:交换 $a_k$ 与 $a_l$。 4. 逆序恢复:将区间 $a[k+1 \dots n]$ 逆序(由于原本是递减的,逆序后将变成递增的)。


三、 数学证明

为了证明上述算法产生的 $P_{i+1}$ 确实是 $P_i$ 的紧邻下一个排列,我们需要证明两个核心命题。

命题 1:为什么要找到最右侧的 $a_k < a_{k+1}$?

证明: 根据字典序定义,要让排列变大,我们必须改变某处的字符,使其变大。 若我们改变位置 $i$ 处的字符,将其替换为一个更大的字符,那么为了保持前缀尽可能一致(即变化最小),我们应该让这个修改发生的位置 $i$ 尽可能靠右。 如果一个后缀 $a[k+1 \dots n]$ 是单调递减的,那么根据排列组合原理,该后缀已经是当前这些元素所能组成的最大字典序子排列。在不修改 $a_1 \dots a_k$ 的情况下,我们无法通过重排后缀来获得更大的排列。 因此,必须修改更靠左的元素。最靠右的、能够通过重排后缀使其变大的位置,就是满足 $a_k < a_{k+1}$ 的最大索引 $k$。保持前缀 $a_1 \dots a_{k-1}$ 不变是贪心策略的核心。

命题 2:交换 $a_k$ 与 $a_l$ 并逆序后,所得排列是最小的“大于 $P_i$ 的排列”。

证明: 1. 替换 $a_k$ 的选择: 为了使新的排列大于原排列,新位置 $k$ 的元素 $a'_k$ 必须大于原 $a_k$。为了让增幅最小,在后缀 $a[k+1 \dots n]$ 中,我们必须选择大于 $a_k$ 的最小元素来替换它。 因为 $a[k+1 \dots n]$ 是单调递减的,从右往左第一个大于 $a_k$ 的元素 $a_l$ 恰好就是该区间内大于 $a_k$ 的最小元素。

  1. 交换后的后缀性质: 在交换 $a_k$ 与 $a_l$ 之前,后缀满足: $$a_{k+1} \ge a_{k+2} \ge \dots \ge a_l \ge \dots \ge a_n$$ 交换后,原 $a_l$ 位置变为了 $a_k$。我们检查交换后的后缀:
  2. 对于 $j > l$,元素未变,单调性保持。
  3. 对于 $j = l$,由于 $a_l$ 是从右往左第一个大于 $a_k$ 的数,说明 $a_{l+1} \le a_k$。因此交换后仍然满足 $a_k \ge a_{l+1}$。
  4. 对于 $j < l$,由于 $a_l > a_k$,因此原序列中的 $a_{l-1} \ge a_l > a_k$,交换后仍满足 $a_{l-1} \ge a_k$。 综上所述,交换后的新后缀依然保持单调递减。

  5. 逆序的合理性: 为了使整个排列在保证前缀 $a'_1 \dots a'_k$ 确定的情况下字典序最小,后缀 $a'[k+1 \dots n]$ 必须处于其最小字典序状态(即单调递增)。 既然当前的后缀已经是单调递减的,我们只需要将其完全逆序(Reverse),即可在 $O(n - k)$ 的时间内将其转化为单调递增,从而获得最小的后半部分。

由于前缀最小化了改变位置,转折点最小化了数值增幅,后缀达到了最小字典序,因此该算法推导出的排列必然是紧邻的下一个排列 $P_{i+1}$。


四、 C++11 代码实现

以下是符合 C++11 标准的实现。我们不直接调用 std::next_permutation,而是手动实现其核心逻辑,以便展示上述数学模型的每一个步骤:

#include <iostream>
#include <vector>
#include <algorithm>

/**
 * @brief 获取当前排列的下一个字典序排列
 * @tparam T 元素类型
 * @param nums 待求下一排列的数组
 * @return bool 如果存在下一个排列返回 true,若已经是最大排列则重置为最小排列并返回 false
 */
template <typename T>
bool next_permutation_custom(std::vector<T>& nums) noexcept {
    if (nums.empty() || nums.size() == 1) {
        return false;
    }

    // 1. 从右向左寻找第一个降序相邻对 (k, k + 1),即满足 nums[k] < nums[k + 1]
    auto k_it = nums.end() - 2;
    while (k_it >= nums.begin() && *k_it >= *(k_it + 1)) {
        --k_it;
    }

    // 如果不存在这样的 k,说明当前排列已是最大(单调递减)
    if (k_it < nums.begin()) {
        std::reverse(nums.begin(), nums.end()); // 翻转为最小排列
        return false;
    }

    // 2. 从右向左在区间 [k + 1, end) 中寻找第一个大于 nums[k] 的元素 nums[l]
    auto l_it = nums.end() - 1;
    while (l_it > k_it && *l_it <= *k_it) {
        --l_it;
    }

    // 3. 交换 nums[k] 与 nums[l]
    std::swap(*k_it, *l_it);

    // 4. 将区间 [k + 1, end) 逆序,使其变为升序(最小化后缀)
    std::reverse(k_it + 1, nums.end());

    return true;
}

int main() {
    std::vector<int> p = {1, 2, 3};

    std::cout << "当前排列: ";
    for (int num : p) std::cout << num << " ";
    std::cout << "\n\n";

    std::cout << "生成后续所有排列:\n";
    // 循环打印接下来的排列,直到回到初始状态
    do {
        for (int num : p) {
            std::cout << num << " ";
        }
        std::cout << "\n";
    } while (next_permutation_custom(p));

    return 0;
}

复杂度分析

  • 时间复杂度:$O(n)$。在最坏情况下,我们需要扫描整个数组两次(一次找 $k$,一次找 $l$),并对后缀进行一次逆序操作。所有操作的时间复杂度均为线性。
  • 空间复杂度:$O(1)$。算法在原数组上进行原地(In-place)交换与逆序,仅需要常数级别的辅助变量。

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

关于火龙

  • 关于我们
  • 学员获奖
  • 预约试听
  • ACM课程
  • CSP课程
  • 学习指南

帮助中心

  • 用户协议
  • 打字练习
  • 在线画图
  • DevC++下载
  • CSP报名
  • GESP官网

推荐课程

  • C++零基础入门(可试看)
  • C++进阶提升
  • GESP考级辅导
  • GESP打卡
  • CSP-J/S打卡

公众号

火龙信奥公众号二维码

地址:义乌市北门街188号新天地商厦二楼2F 邮箱:wdlok305@126.com

© 2017-2026 义乌市睿码科技有限公司版权所有 浙ICP备2021013995号

火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码