从洛谷P1603 斯诺登的密码说起:深入解析 C++ std::next_permutation,为什么会有缺失的排列。

在做洛谷P1603 斯诺登的密码这个题目的时候,我使用了全排列来求解,但是发现一个问题,就是next_permutation并没有输出一个向量的全排列,这是怎么回事呢?以下是我的一个解法,并没有得满分,相信看完代码后面的解析后,你能完善好这个程序:

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
map<string,int> dict={{"zero",0},{"one",1},{"two",2},{"three",3},{"four",4},{"five",5},
                      {"six",6},{"seven",7},{"eight",8},{"nine",9},{"ten",10},{"eleven",11},
					  {"twelve",12},{"thirteen",13},{"fourteen",14},{"fifteen",15},{"sixteen",16},
					  {"seventeen",17},{"eighteen",18},{"nineteen",19},{"twenty",20},{"a",1},{"both",2},
					  {"another",1},{"first",1},{"second",2},{"third",3} };
int main(){
	vector<string> v;
  string word,square;   //Black Obama is two five zero .
  while(cin >> word){
  	if(word==".")break;
  	if(dict.count(word)>0)
  	{ 
	   square = to_string( ( dict[word]* dict[word] ) % 100 );
  	   if(square.size()==1)square = "0"+ square;
  	   v.push_back(square );  
	}
  }
  if(v.size()==0){ cout << 0;return 0;}   //例:Chen Junao is no so clever . 

  long long ans= 0x3f3f3f3f3f3f3f3f;
  do{
     string s;
     for(string sq:v)s = s + sq;    
     ans = min(ans,stoll(s));
  }while(next_permutation(v.begin(),v.end()));
  if(ans==0x3f3f3f3f3f3f3f3f)cout << 0;
  else cout << ans;
  
  return 0;
}

在 C++ 编程中,生成全排列是一个非常经典的算法需求。标准库 <algorithm> 提供了 std::next_permutation 函数,使得这一操作变得异常简便。然而,许多初学者在使用该函数时,常常会遇到“排列输出不全”的诡异现象。本文将基于一个具体的代码案例,深度剖析 std::next_permutation 的底层逻辑、字符串字典序的比较规则,以及如何正确地生成全排列。

一、 现象重现:消失的 "000425"

让我们先来看一段引发疑问的 C++ 代码:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main(){
   vector<string> v={"00","25","04"};
   //vector<string> v={"A","B","C"};
     
  do{
     string s;
     for(string sq:v) s = s + sq;
     cout << s << endl;
  }while(next_permutation(v.begin(),v.end()));
  
  return 0;
}

运行上述代码,控制台的输出结果如下:

  • 002504
  • 040025
  • 042500
  • 250004
  • 250400

对于包含 3 个元素的集合,其全排列理应有 3! = 6 种可能。但程序只输出了 5 种,唯独缺少了 000425。更令人困惑的是,如果将初始向量替换为 {"A", "B", "C"},程序却能完美输出 6 种全排列。这种差异究竟从何而来?

二、 核心机制:字典序与 next_permutation 的工作原理

要解开这个谜团,我们必须深入理解 std::next_permutation 的设计哲学。该函数并非凭空“随机”生成排列,而是严格遵循字典序(Lexicographical Order)来计算当前序列的“下一个”排列。

1. 什么是字典序?

字典序即如同查字典一般的排序规则。对于 C++ 中的 std::string,比较操作符(如 <)会逐字符比较两个字符串的 ASCII 码值。在我们的案例中,涉及到的三个字符串分别是 "00""25" 和 "04"

按照字典序比较规则:

  • "00" 的第一个字符是 '0'"04" 的第一个字符也是 '0',比较第二个字符 '0' < '4',因此 "00" < "04"
  • "04" 的第一个字符是 '0'"25" 的第一个字符是 '2',因为 '0' < '2',因此 "04" < "25"

所以,这三个元素的严格字典序递增顺序为:"00" < "04" < "25"

2. next_permutation 的状态机逻辑

std::next_permutation 的作用是将给定区间内的元素重新排列为字典序中的下一个更大的排列。如果当前排列已经是字典序最大的排列(即完全降序),函数会将其重置为字典序最小的排列(即完全升序),并返回 false;否则返回 true

基于 "00" < "04" < "25" 的大小关系,这三个元素的 6 种全排列按字典序从小到大依次为:

序号排列状态 (vector 内的顺序)拼接后的输出结果备注
1{"00", "04", "25"}000425字典序最小(完全升序)
2{"00", "25", "04"}002504用户的初始状态
3{"04", "00", "25"}040025
4{"04", "25", "00"}042500
5{"25", "00", "04"}250004
6{"25", "04", "00"}250400字典序最大(完全降序)

三、 案例对比分析:为何表现不同?

剖析 {"00", "25", "04"} 的执行流

在用户的代码中,vector 的初始状态被硬编码为 {"00", "25", "04"}。对照上表可以发现,这实际上是字典序中的第 2 个排列

代码使用了 do-while 循环:

  1. 第一次循环(do):直接处理初始状态 {"00", "25", "04"},输出 002504
  2. 条件判断(while):调用 next_permutation,将状态更新为第 3 个排列 {"04", "00", "25"},返回 true
  3. 后续循环:依次输出第 3、4、5、6 个排列。
  4. 结束:当状态达到第 6 个排列 {"25", "04", "00"} 时,再次调用 next_permutation,由于已经是最大字典序,函数将其重置为第 1 个排列 {"00", "04", "25"},并返回 false,循环终止。

结论:因为初始状态不是字典序最小的排列,next_permutation 只能从当前状态继续向后生成,从而完美地“跳过”了排在它前面的第 1 个排列(即 000425)。

剖析 {"A", "B", "C"} 的执行流

当用户将初始向量替换为 {"A", "B", "C"} 时,情况发生了根本性的变化。在 ASCII 码中,'A' < 'B' < 'C'。因此,{"A", "B", "C"} 恰好是这三个元素字典序最小的排列(第 1 个排列)

由于起点是第 1 个排列,do-while 循环配合 next_permutation 能够顺理成章地遍历完后续的 5 个排列,最终完整输出 6 种全排列。这并非魔法,而是初始状态恰好满足了算法的边界条件。

四、 算法底层探秘:Narayana Pandita 算法

为了更深刻地理解 next_permutation,我们有必要了解其底层实现的数学基础。C++ 标准库通常采用 14 世纪印度数学家 Narayana Pandita 提出的字典序排列生成算法。该算法的时间复杂度为 O(N),其核心步骤如下:

  1. 寻找升序对:从序列的末尾向前查找,找到第一个满足 a[i] < a[i+1] 的索引 i。如果找不到这样的 i,说明整个序列是降序的(即最大排列),算法结束。
  2. 寻找替换元素:再次从序列末尾向前查找,找到第一个满足 a[j] > a[i] 的索引 j
  3. 交换元素:交换 a[i] 和 a[j] 的值。
  4. 反转后缀:将索引 i+1 到序列末尾的所有元素进行反转(使其从降序变为升序),从而保证生成的下一个排列是尽可能小的“大排列”。

以 {"00", "25", "04"} 为例:
1. 从后向前找,"25" > "04" 不满足,"00" < "25" 满足,因此 i=0 (指向 "00")。
2. 从后向前找大于 "00" 的元素,找到 j=2 (指向 "04")。
3. 交换 a[0] 和 a[2],序列变为 {"04", "25", "00"}
4. 反转 i+1 (即索引 1 到末尾) 的元素,"25" 和 "00" 交换,最终得到 {"04", "00", "25"}。这正是我们输出的下一个排列。

五、 解决方案与最佳实践

通过上述分析,问题的症结已经非常清晰:在使用 std::next_permutation 生成全排列之前,必须确保容器处于字典序最小的状态。

为了修复用户的代码并保证其在任何初始输入下都能正确输出全排列,我们需要在进入 do-while 循环之前,显式地对容器进行一次排序(std::sort)。

修正后的代码:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main(){
    vector<string> v = {"00", "25", "04"};
   
    // 关键修复:先将容器排序为字典序最小状态
    sort(v.begin(), v.end());
     
    do {
        string s;
        for(const string& sq : v) {
            s += sq;
        }
        cout << s << endl;
    } while(next_permutation(v.begin(), v.end()));
  
    return 0;
}

加入 sort(v.begin(), v.end()); 后,无论初始向量是 {"00", "25", "04"} 还是 {"25", "04", "00"},程序都会先将其重置为 {"00", "04", "25"},从而确保 do-while 循环能够完整遍历所有 6 种排列。

六、 总结

C++ 的 std::next_permutation 是一个强大且高效的工具,但它的命名有时会给开发者带来错觉,让人误以为它是一个“全排列生成器”。实际上,它仅仅是一个“字典序状态推进器”。

在日常开发中,牢记以下两点可以避免绝大多数与排列相关的 Bug:

  • 全排列的前提是排序:如果需要获取所有排列,务必在循环前使用 std::sort 初始化容器。
  • 理解数据类型的比较规则:无论是数字、字符串还是自定义结构体,next_permutation 的行为完全依赖于元素之间的 < 运算符逻辑。

通过透彻理解标准库算法的契约与前置条件,我们不仅能写出正确的代码,更能体会到 C++ STL 在设计上的严谨与优雅。

内容概要:本文介绍了基于Python与机器学习的农作物产量预测系统的设计与实现,旨在通过整合多源农业数据(如气象、土壤、农事记录、遥感等),构建结构化数据集并应用随机森林、梯度提升等机器学习模型进行产量预测。系统涵盖数据采集、预处理、特征工程、模型训练与评估全流程,重点解决数据质量不稳定、影响因素复杂及模型泛化能力等问题。通过特征构造与模型优化提升预测准确性,并支持精细化农业管理与产业链风险控制。文中提供了完整的代码示例,包括数据清洗、缺失值处理、特征构建、预处理管道搭建、模型训练与保存等关键步骤。; 适合人群:具备一定Python编程和机器学习基础,从事农业信息化、数据分析或智慧农业相关工作的研发人员、数据科学家及农业技术研究人员,尤其是工作1-3年希望将AI技术应用于农业领域的从业者。; 使用场景及目标:① 实现对玉米、小麦、水稻等主要作物的地块级或区域级产量预测;② 支持农业管理部门、种植户、保险公司和供应链企业进行生产决策、风险预警与资源配置;③ 掌握如何将多源异构农业数据融合并构建可落地的预测模型。; 阅读建议:此资源以实际项目为导向,不仅展示模型构建过程,更强调农业业务逻辑与数据科学的结合。建议读者结合代码实践操作,深入理解特征工程设计与模型选型背后的农业机理,并尝试扩展至更多作物或区域数据以提升模型泛化能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李兴球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值