news 2026/10/12 19:12:58

Sine之舞递归详解:蓝桥杯字符串拼接与An/Sn规律全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sine之舞递归详解:蓝桥杯字符串拼接与An/Sn规律全解析

Sine之舞这道题,刷过蓝桥杯基础练习的人多少都见过。题目本身不给任何高级算法,就一串sin来回套,却能把不少新手卡在原地一整天。当初我练的时候也栽过跟头,后来把An和Sn的生成规律彻底捋了一遍,发现其实就是“递归规律”和“字符串拼接”两件事。这篇文章就把推导过程、完整代码和我在赛场上踩过的坑一次性写清楚,适合正在备赛蓝桥杯的选手,也适合刚学完递归想找实战练手的初学者。

1. 题目还原:Sine之舞的输入输出到底长什么样

1.1 An的题目定义与样例展开

蓝桥杯原题里定义了一组字符串,用字母A表示,前几项是这样的:

  • A1 = sin(1)
  • A2 = sin(1-sin(2))
  • A3 = sin(1-sin(2+sin(3)))
  • A4 = sin(1-sin(2+sin(3-sin(4))))

初看会觉得它就是在sin的参数里不断往里嵌新的sin。每增加一项,就在最内层继续套一个sin(k),而且数字之间的符号是交替变化的:A2里1和sin(2)之间是减号,A3里2和sin(3)之间变成加号,A4里3和sin(4)之间又变成减号。换句话说,数字1后面跟负号,数字2后面跟正号,数字3后面跟负号,如此交替。

题目要求输入一个整数n,输出对应的Sn。Sn不是直接等于An,而是把An当作零件,和数字n、n-1、...、1拼起来。

1.2 Sn的组装规则初印象

Sn的样例也给了几项:

  • S1 = A1+1 = sin(1)+1
  • S2 = (A1+2)A2+1 = (sin(1)+2)sin(1-sin(2))+1
  • S3 = ((A1+3)A2+2)A3+1

稍微展开一点:

S2左边多了一个左括号,A1后面跟的是“+2”,然后一个右括号,接着是A2,最后“+1”。S3左边是两个左括号,A1后面跟“+3”,右括号,A2后面跟“+2”,又一个右括号,A3后面跟“+1”。

这个结构和An很像,区别在于An是往sin的参数里嵌套,Sn是往表达式外层不断加括号。Sn需要你把An先求出来,再按规则拼装。很多人在这一步就开始乱套,因为Sn的左括号、加号、右括号、A串的位置太接近了,手写都能抄错,更别说写成代码。

我看到这道题的第一反应是:必须先找到形式化的规律,再动手写代码。如果对着样例硬拼字符串,n稍微大一点就会彻底乱掉。

2. 拆解An:一个隐藏着符号交替的递归串

2.1 从A1到A4看结构变化

把A1到A4并排看:

A1 = sin(1) A2 = sin(1-sin(2)) A3 = sin(1-sin(2+sin(3))) A4 = sin(1-sin(2+sin(3-sin(4))))

可以把它理解成一个“洋葱”结构:最外面是sin(1),1里面包着第二层;第二层是sin(2),2里面包着第三层;一直包到sin(n)为止。每一层往里包的时候,需要带一个符号,这个符号就写在这一层数字的后面。比如A3里,1后面是减号,表示要减去下一层;2后面是加号,表示要加上再下一层。

于是An就能写成这种通式:

An = sin(1 - sin(2 + sin(3 - sin(4 + ... sin(n))...)))

从1开始,数字i后面如果还有下一层,就根据i的奇偶性决定符号:i为奇数用减号,i为偶数用加号。这个规则其实非常固定,比背样例可靠得多。

2.2 用递归函数生成An:代码与逐层推演

既然An是典型的自相似结构,用递归是最自然的写法。定义一个函数g(i, n),它负责生成“从数字i开始,一直到数字n”的那一层sin字符串。也就是说:

  • g(n, n) = "sin(n)"
  • 当 i < n 时,g(i, n) = "sin(" + i + 符号 + g(i+1, n) + ")"

其中符号由i的奇偶决定:i是奇数时取减号,i是偶数时取加号。

对应的C++代码:

string g(int i, int n) { if (i == n) { return "sin(" + to_string(i) + ")"; } string op = (i % 2 == 1) ? "-" : "+"; return "sin(" + to_string(i) + op + g(i + 1, n) + ")"; } string A(int n) { return g(1, n); }

拿n=3走一遍:g(1,3)发现1不等于3,1是奇数走减号,返回"sin(1-" + g(2,3) + ")";g(2,3)发现2是偶数走加号,返回"sin(2+" + g(3,3) + ")";g(3,3)等于n,返回"sin(3)"。最后拼出来就是"sin(1-sin(2+sin(3)))",和题目样例完全一致。

这个写法的好处是把符号规律收敛到一行判断里。只要记住了“奇数减、偶数加”,整个An生成就永远不会错。

2.3 一个容易写错的非递归写法

我也见过有人尝试用循环直接拼An,写法大概是:

string badA(int n) { string s = "sin(1"; for (int k = 2; k <= n; k++) { s += (k % 2 == 0 ? "-" : "+") + string("sin(") + to_string(k) + ")"; } s += ")"; return s; }

这个写法对n=3会得到"sin(1-sin(2)+sin(3))",根本不是题目要的"sin(1-sin(2+sin(3)))"。问题出在哪?它把加号和sin(3)拼在了sin(2)的外面,实际上是“1减去sin(2),再加上sin(3)”,而题目要求的是“1减去sin(2+sin(3))”。差别就是一层括号的位置。

所以An绝对不能靠简单循环从左往右平铺,必须用递归让每一层sin的参数都“包住”右边的剩余部分。这个坑我印象很深,因为很多初学递归的人第一次意识到“字符串也是可以递归构造的”。

3. 拆解Sn:括号、数字和An的排列顺序

3.1 Sn的外层框架规律

An解决了,Sn本质上就是把An当作一个字符块插入到另一个规律字符串里。再看一遍题目给的样子:

  • S1 = A1+1
  • S2 = (A1+2)A2+1
  • S3 = ((A1+3)A2+2)A3+1
  • S4 = (((A1+4)A2+3)A3+2)A4+1

这里有一个非常清晰的规律:Sn最前面连续出现n-1个左括号;然后依次是A1、A2、...、An;每个Ai后面跟着一个数字n-i+1;除了最后一个An后面不跟右括号,其余每个Ai后面都要写一个右括号。

写成形式化描述就是:

Sn = "("(重复n-1次) + A1 + "+n" + ")" + A2 + "+(n-1)" + ")" + ... + An + "+1"

比如n=4时:

S4 = "(((" + A1 + "+4" + ")" + A2 + "+3" + ")" + A3 + "+2" + ")" + A4 + "+1"

和题目样例完全吻合。

3.2 循环拼接Sn的完整思路

这个规律用循环实现非常直接:

string S(int n) { string s; for (int i = 1; i < n; i++) { s += "("; } for (int i = 1; i <= n; i++) { s += A(i); s += "+" + to_string(n - i + 1); if (i < n) { s += ")"; } } return s; }

注意A(i)在这里不是An,而是一个“从i层开始生成到n”的调用,因为Sn里的每个Ai用的都是同一个总长度n:A1 = g(1,n),A2 = g(2,n),...,An = g(n,n)。

拿n=3试一下:先加两个左括号变成"((";然后A(1)即g(1,3)"sin(1-sin(2+sin(3)))",加上"+3",再加右括号;接着A(2)即g(2,3)"sin(2+sin(3))",加上"+2",再加右括号;最后A(3)即g(3,3)"sin(3)",加上"+1"。

合成结果就是"((sin(1-sin(2+sin(3)))+3)sin(2+sin(3))+2)sin(3)+1",展开后和题目S3样例一致。

3.3 为什么嵌套括号的数量是n-1

很多人容易忽略一个细节:为什么Sn前面要有n-1个左括号,而不是n个或者别的数量?从S1到S3观察,括号数正好等于n-1。

这个括号的作用是把前面的“A+数字”整体包住,让后面的运算保持嵌套。比如S3里,两个左括号先连续打开两层,然后A1+3之后闭合一层,A2+2之后闭合第二层。如果括号数量改成n个,整个表达式的括号配对就会失衡,输出格式直接错误。

做题的时候不需要纠结这个括号在数学上的运算优先级,但写代码时必须严格按n-1来。我见过有人拿n去凑括号数,结果S1就输出多一个左括号,被评测系统判格式错误。

4. 完整参考实现与边界用例验证

4.1 完整C++代码

把递归生成An和循环拼接Sn合在一起,就是一个完整的解法:

#include <iostream> #include <string> using namespace std; string g(int i, int n) { if (i == n) { return "sin(" + to_string(i) + ")"; } string op = (i % 2 == 1) ? "-" : "+"; return "sin(" + to_string(i) + op + g(i + 1, n) + ")"; } int main() { int n; cin >> n; string ans; for (int i = 1; i < n; i++) { ans += "("; } for (int i = 1; i <= n; i++) { ans += g(i, n); ans += "+" + to_string(n - i + 1); if (i < n) { ans += ")"; } } cout << ans << endl; return 0; }

这个代码包含了全部逻辑,没有额外的头文件依赖,蓝桥杯的评测环境可以直接运行。

4.2 对n=1和n=4两个用例的验证

n=1时,第一个for循环不添加任何左括号;第二个for循环只执行i=1,ans = g(1,1) = "sin(1)",加上"+1",i不小于n不加右括号。最终输出"sin(1)+1",正是S1。

n=4时,按照我们前面的形式化公式逐步拼接:

第一步:添加3个左括号 "((("。 第二步:g(1,4)="sin(1-sin(2+sin(3-sin(4))))",加"+4",加右括号。 第三步:g(2,4)="sin(2+sin(3-sin(4)))",加"+3",加右括号。 第四步:g(3,4)="sin(3-sin(4))",加"+2",加右括号。 第五步:g(4,4)="sin(4)",加"+1"。

最终结果:

S4 = (((sin(1-sin(2+sin(3-sin(4))))+4)sin(2+sin(3-sin(4)))+3)sin(3-sin(4))+2)sin(4)+1

把题目给的结构代进去,完全对应。

4.3 复杂度分析与能否优化

从时间复杂度看,生成g(i,n)时,每一层都要进行字符串拼接,字符串长度是O(n)级别,递归深度O(n),所以单个g(i,n)的复杂度是O(n^2)。main循环里最多调用n次g(i,n),整体复杂度是O(n^3)。n在蓝桥杯这类题里通常不超过两位数或三位数,这个复杂度完全够用。

如果追求效率,可以用string的reserve提前分配容量,减少拼接时的反复扩容。比如在main里先算个大概长度,调用ans.reserve(n * n * 2),对性能有一点帮助,但在这道题里不是必需。

也可以把An的生成改成自底向上递推,先算g(n,n),再往外拼,但那样反而要维护一个数组,代码没有递归版直观。竞赛里“能AC且好读”比“极致性能”更重要,所以我个人推荐递归写法。

5. 蓝桥杯赛场上的实操心得与避坑建议

5.1 字符串拼接的效率陷阱

C++里直接用加号拼字符串确实方便,但频繁拼接会反复产生临时对象,时间复杂度翻倍。蓝桥杯这类题数据量小,通常看不出问题,但如果你把同样的思路带到n=1000甚至更大的场景,就可能超时。

我的建议是:main里初始化ans时顺手写一行ans.reserve(10000),量级大致按n*n估算。不用精算,多点没坏处。这个习惯能让你从“蓝桥杯基础题”顺利过渡到“算法提高题”,不会在字符串处理上吃暗亏。

5.2 小心递归爆栈与函数签名设计

g(i,n)的递归深度是n,n不大的时候毫无压力。但如果题目某天把n加大到几万,递归肯定爆栈。届时候要么改成递推,要么把函数设计成尾递归形式。不过就Sine之舞这道题而言,完全不需要担心。

比较关键的是函数签名。我第一次写的时候把n写成全局变量,函数只传i,看起来简洁,但遇到多组测试数据时容易忘记更新n。蓝桥杯的题往往单组输入,全局变量问题不大,可一旦你养成这种习惯,在其他OJ题里就容易翻车。建议函数参数把i和n都显式传进去,自包含、可读性强、也好调试。

5.3 如何快速验证输出正确性

赛场时间紧张,不可能每次都用肉眼对比长字符串。我的调试套路是:先测试n=1、n=2、n=3,这三个值的输出短,能直接和题目样例逐字符比对。比对通过后,再测n=4,重点看右括号数量和左括号数量是否匹配。

一个小技巧:在代码里临时加一个统计左右括号数的helper,如果左右括号数不相等,说明拼接逻辑有问题。写完AC代码后记得删掉调试代码。这个习惯帮我在不少字符串类题目里省了大量时间。

另外,输出时别忘了换行。很多新手把逻辑写对了,最后因为没输出endl被判定presentation error,这种丢分最冤。

Sine之舞这道题,说难不难,说简单也不简单。它真正训练的是“观察样例、形式化规律、用递归表达自相似结构”这套通用能力。我后来做很多字符串递归题,都会先想起这道题的洋葱结构。如果你练的时候卡住了,不要盯着代码发呆,先在纸上把A4和S4写出来,规律往往比你想的更简单。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 19:12:55

Ansible自动化部署Prometheus监控:从设计到实践

1. 项目概述与需求拆解1.1 这个项目的核心是什么简单说一下这个事情的背景。Easypack这个名字你可能不熟&#xff0c;它本质上是一个围绕常用基础组件部署的自动化工具集&#xff0c;底层用Ansible来编排&#xff0c;目标就是让你不用手动一行一敲命令&#xff0c;就能把一套中…

作者头像 李华
网站建设 2026/10/12 19:10:30

Apriori实战:购物篮关联规则挖掘与Python代码详解

简介&#xff1a;这份资源面向数据挖掘初学者与商业分析方向的学习者&#xff0c;围绕Apriori关联规则展开市场购物篮分析实战&#xff0c;帮助读者从零理解频繁项集发现、支持度与置信度度量、强关联规则生成及规则评估的完整流程。压缩包共4个文件&#xff0c;约20.36MB&…

作者头像 李华
网站建设 2026/10/12 19:07:24

大额充值避坑指南:渠道选择、风控安全与实操全攻略

最近经常有人问我&#xff1a;“大额充值到底怎么充才划算&#xff1f;网上那些折扣渠道靠不靠谱&#xff1f;有没有一个首选方案&#xff1f;” 做运营这几年&#xff0c;我处理过最多的线下支出就是大额充值。话费、流量包、平台会员、游戏点券&#xff0c;甚至公司集采的云…

作者头像 李华
网站建设 2026/10/12 19:02:48

SpringBoot药房管理系统实战:从需求拆解到库存并发与部署

每年毕业季&#xff0c;总有一批做“药房管理系统”的同学来找我调代码&#xff0c;题目大同小异&#xff0c;踩坑却惊人地一致&#xff1a;库存扣成负数、订单状态对不上、药品编码重复、数据库表字段乱得连跑都跑不起来。这个选题表面看就是标准的SpringBoot增删改查&#xf…

作者头像 李华
网站建设 2026/10/12 19:02:11

C# WinForm 部署 YOLOv11-Pose ONNX 实时姿态估计

简介&#xff1a;本资源是一套基于C# WinForm开发的YOLOv11-Pose人体姿态估计ONNX模型部署源码&#xff0c;面向具备.NET开发基础与计算机视觉入门经验的工程师和学习者&#xff0c;解决在Windows桌面端快速集成轻量级姿态估计算法的实际需求&#xff0c;适用于智能监控、人机交…

作者头像 李华