在当今这个网络蓬勃发展,信息量极其巨大的社会中,大家都在提倡独立思考,但究竟什么才是独立思考,却从没有人在意或提及过。我只记得在初中政治书上对如何独立思考有过一个大致的定义:“不人云亦云“,可什么叫做“不人云亦云”?有时,当我将要做出某种决策,我也会做出与大多数人相同的选择,那是不是我为了要“独立思考”,就要放弃这种选择,违背我内心的意愿?这似乎和“不人云亦云”比较相符了,但我的决策好像还是和大多数人的决策有着紧密的联系,那这还能叫做“独立思考”吗? 我始终没有搞清楚,只好迷迷糊糊地将它当作一个简单的政治知识点背了下来,尽管多次考试没有用到它早已让我将它淡忘,但最近的一次经历却又让它回到了我的脑海,让我开始对它有了一些理解。 A是我的一个初中同学,周末他突然告诉我说,他想要买一辆电动车,希望我能够陪他一起选一辆合适的,尽管我对电动车可以说是毫无兴趣,但终究敌不过好奇心的诱惑,便答应了下来。我们一开始就去看了当下最热门的九号,A告诉我说,他看到好多和我们年龄相仿的学生都在骑九号,他也想要买一台,我笑他真是人云亦云,能不能有点自己的主见,但他并没有理会,只是兴奋地在店里面到处看。 终于看到一台喜欢的,一问价钱,他的笑容瞬间就凝固在了脸上,和老板说了几句客套话后便拉着我走了。我问他说你不是喜欢吗,怎么又不要了,他沉默了半晌后告诉我,虽然在他的预算之内,但他觉得太贵了,不值。我心中暗想,对嘛,大家都说好的东西,肯定都不怎么样。 在电动车城里面又绕了好几圈,看了好几个牌子,最终他的目光锁定在了另一个牌子的一辆车上,看着大小和九号那辆差不多,但价钱却是它的一半。A和我商量时一直在说那辆车的优势,但在听从我的建议试驾了那辆车之后,却出乎意料地沉默了。 他又一次拉着我走出了店,一边走一边,说那个车骑着一点都不好,仪表盘就是最垃圾的那一种,整个车给他的感受还不如城市里面的共享电单车呢。突然之间,我们又失去了目标。我问他现在到底要哪一辆,他不说话,只是低着头,慢慢地走着,持续了大概两分钟,他突然说,”就那辆九号。“ 我很吃惊,没想到刚刚被否定的选项竟然又变成了最终选项,但我没有否认他,只是问他为什么。他说,每个人的评价标准都不一样,对于电动车,有人觉得只要拧一下车把能走就够了,但他希望的是一个舒服且现代化的驾驶体验,如果这样,九号绝对是他的最佳选择。 后来回到家,我把选购电动车的整个过程想了一遍,单从结果上看,他最初冲动的选择和经过长时间的比较后做出的选择好像没有区别,中间的犹豫和比较似乎都是浪费时间,与其浪费时间到处看,还不如上来就买节省时间。可是这两者的区别究竟在哪里? 我想了很久,其实正是区别在中间犹豫和比较时的思考。一开始他直接跑到九号的店里面买车,其实是完全基于别人的选择和看法所做出的选择,用文章最开始的话来说就是人云亦云,但后来,经过长时间的比较与思考之后,他基于自己对电动车评判的标准又选择了九号,这其实就是独立思考。 很多时候,人云亦云和独立思考导向的结果很可能完全相同,但两者之间所差的,则可能是长时间的徘徊,犹豫,探索与思考,人所以区别于动物,就是因为人拥有一个可以深度思考的大脑,我们不必在乎别人说什么做什么,只需要以自己为本,根据自己的评价标准做出选择,就是独立思考。独立思考没有一个能够准确表达的标准,但只有一点:遵从自己的内心。
致三年后自己的一封信
三年后的自己,你好哇! 我曾见过聿怀初中二楼的墙上贴着的许多人给三年后自己的一封信,那些刚刚上初一的新生都在展望自己美好的未来,尽管他们还从来没有真正经历过几天初中生活。而现在,我并不想带着同样盲目的自信,去想象我从未接触过的未来。 高中我做出的与众不同的选择本身就已经表明,我的人生将与别人的人生截然不同,我不需要依靠班级同学的氛围来督促自己的学习,我也不在乎那些所谓名校的校友圈,我就是我,纵使周围环境大乱,我也将保持自己的节奏,朝着自己的目标,稳步向前,希望身处高三的你,依然要坚定自己的目标与节奏,高考固然重要,但永远不要让自己的热情在高考之前就消耗殆尽,同样不要让一个大学成为自己人生最高光的时刻,始终记得,你的未来会更屌。 也许你会紧张,但坐上车,听听你曾经收藏的歌单,或许曾经的旋律可以放松你紧绷的神经;到高速上面跑一跑,看看外面向后飞速逝去的景色,远方悬浮在蓝天之中的白云,或许可以让你回想起旅行时拥有无限可能的下一刻;到海边坐一坐,看夕阳西下,海面上金波荡漾,大海或许可以静静地聆听你的倾诉;翻一翻你曾经从各地寄回家的明信片,那上面的话语虽短,但或许可以让你记起快乐的旅行时光,让思绪向远方飘一飘。平静是金,希望你能够始终保有一颗宁静的心。 我以为初中三年很漫长,谁知过着过着竟就过完了,你一定也有同感,高中三年只会比初中的更短,只是不知你是否遇到了志同道合的好朋友,你们可以倾心交谈,并为了共同的目标进行奋斗。至少我还没有遇到,看起来我和同学关系都不错,但都只是维持着表面的客气罢了,初一时我曾尝试着融入他们,和他们到处跑,但这没有意义,希望你也不要为了别人面子上的客气去花费精力融入他们那些群体,你的精力太宝贵了,值得更好的事情。 提到浪费精力,我不由自主地想起了谈恋爱,这件事可以说是最浪费精力的事情了,我并不是说不能谈恋爱,但选对一个人是非常困难的事情,不要因为一时的心动就莽撞地献出一生,我在初中看过许多人合了又分,分了又换,一次吵架或分手甚至可以影响到一个人一整个星期的心情,要是你在恋爱过程中感觉到对方对你产生了负面影响,很可能说明你得换一个了。 我现在主要精力集中在积木车设计和智能小车开发上,不知道你的爱好是否发生了变化?我很好奇你已经将小车开发到什么地步了,还是说仍然保持着现在这个状态,没有时间再继续研究?你一定要清楚自己喜欢干什么,未来想要干什么,否则大学提供再好的平台都只是浪费,我听说过好多人好不容易考上了一个好大学,却忘了自己真正喜欢什么,这未免太可惜了。 想问问你,你的驾照拿到了吗?就算没拿到,我也相信你的水平,高考结束就去考驾照,一定会非常顺利!再者,以前我总觉得一辆车几年之后可能看着都会很老土,不知道你看着我们的车感觉如何?我猜应该不会太老,不过也许这是因为对它产生了感情的缘故吧! 到这里,似乎这场跨越时空的对话就要结束了,有些时候在没有动笔的时候我总觉得有万千话语可以说,但真正开始写了以后,似乎又有一种不知道该说些什么的迷茫。三年之后,如果你通过博客再看到这封信,希望你可以再写一封回信,当然,我肯定是看不到回信的了,但你可以以更丰富的人生为素材,对我这封信给予回答。 2026.8.20 于我的电脑
Raspbot改造远程联网监控
既然$Raspbot$小车上面既有摄像头又有轮子,那么也许可以尝试做一个可以遥控行驶的监控器,这样即可非常方便地远程观察家中的情况,甚至脱离了固定监控的位置限制,它可以去到各个房间探查。下面就是实现这个功能的完整过程。 设备联网 (注:电脑上需要下载的应用有VNCviewer以及nmap,其中nmap可以通过其官网直接下载安装,而VNCviewer可以通过RealVNC官网安装,如果连接不上官网,可以在亚博智能网站提供的资源中下载远程登录软件.zip文件,解压后同样可以找到VNCviewer的安装包) 如果想要通过网络对小车进行控制,首先要让小车联网。本身按照官方的说法,直接用手机App中的配网按钮,生成一个专用的联网二维码,放到小车摄像头面前扫描即可,但这种方式实测并不可靠,本身如果有一根Micro-HDMI转HDMI的传输线,就可以把小车主板直接接到显示器上面,选择网络连接即可,但如果手头并没有线,必须通过远程才能访问到小车的桌面,那么就需要按照以下步骤联网: 用一根网线直接插入主板上的网线连接口,和路由器连接,强制通过有线连接的方式让它联网; 将电脑连入和小车同样的网络,运行ipconfig命令,找到WLAN的IPV4地址,可以发现显示的是一个形似A.B.C.D的局域网地址,记下四个数字,并将下文中的字母均替换为查询到的数字; 由于此时小车已经有线连接在同一个网络下,故在命令行中运行nmap -sn A.B.C.1/24命令,即可查找和电脑连接在同一网络下的所有设备。此时nmap会默认在括号中打印出设备的制造商和其局域网地址,找到括号中写着Raspberry Pi (Trading)的设备,下面的IPV4一栏即为小车的局域网地址; 打开VNCviewer,在左上角File一栏中找到New Connection,点击添加新的连接,并在弹出对话框的VNCserver一栏中填写刚刚查到的小车局域网地址,Name中填上Raspbot,其他默认即可,点击OK; 这时VNCviewer的主页中新增了一个名为Raspbot的设备,双击即可进入小车的桌面啦!至此,远程访问小车系统就已经实现了。 在小车桌面的左上角点击命令行的图标,打开命令行,开始设置无线网卡的联网: 运行以下命令,查询附近可用网络,这时会列出附近的网络名称和信号强度,如果要连接的网络在显示的列表中,即可连接; sudo nmcli device wifi list 按q退出网络查找模式,运行以下命令,将无线网卡连入网络:(注意双引号不要删掉了) sudo nmcli device wifi connect "你的WiFi名称" password "你的WiFi密码" ifname wlan0 如果连接成功,命令行就会返回: Device 'wlan0' successfully activated with 'X-X-X-X-X'. 这时,在小车的命令行中运行ifconfig,找到wlan0对应的信息,其中第二行的inet后面即为小车无线联网的局域网IPV4地址了,此时,再在VNCviewer中新建一个连接,将无线联网的这个地址输入VNCserver,重新取个名字,这样,就算将网线拔了,双击这个新增的设备也可以通过局域网无线访问小车桌面了! 但这时如果重启小车,它依然会优先进入热点模式而非连接网络,所以需要修改小车系统中连接网络和开启热点的优先级,用以下命令: sudo nmcli connection modify "你的WiFi名称" connection.autoconnect-priority 10 sudo nmcli connection modify "Raspbot" connection.autoconnect-priority 1 第一句设置连接wifi的优先级为10,第二句设置开启热点的优先级为1,提高wifi优先级,即可保证小车开机首选自动连接wifi。 至此,小车启动自连网设置完毕。 搭建网页 联网之后就需要选择一个信号传输的方式,我个人认为搭建一个网页并通过网络发送信号是最方便的,当然可以开发一个手机App,再通过网络连接小车,但这就和亚博智能官方的应用没什么区别了,甚至还更麻烦。以下就是搭建网页的步骤。 这里通过python第三方包flask进行网页搭建,这样,只需要在树莓派中运行一个.py文件,即可打开一个地址类似A.B.C.D:X的端口,其中A.B.C.D是小车自己的IP地址,而X则是一个可以在flask中自定义的一个端口编号,这样,只需用手机访问这个网址,即可开始和小车进行信号传输。 我的小车网页总共分四部分:主页、遥控页面、人脸识别页面、聊天室,其中: 主页集成了监控画面和三个功能页面的跳转按钮; 遥控页面中功能较多,集成了以下功能: 监控画面; 摄像头角度控制; 小车行驶控制; 蜂鸣器控制; 抓拍并下载监控当前画面; 人脸识别页面在进入时,小车会将摄像头转到几个设定的角度,寻找人脸,在找到人脸后自动调整摄像头角度使得人脸居于画面中央,并在对应人脸上标明绿色方框,如果人脸已经录入在人脸数据库中则会在方框右上角标注人名; 聊天室中集成以下功能: 发送信息,记录并显示聊天历史; 根据发送信息的IP地址不同,调用不同的人声对信息进行阅读; 可在后台修改不同IP对应的昵称,用于聊天历史的显示。 下面是具体代码,注意这些网页均需要用.py文件和.html文件联合搭建。 一些基本搭建网页的代码如下: python语句: from flask import Flask, request, render_template, url_for, Response, send_file, jsonify # 初始化网页对象,之后所有网页地址对应功能的实现都需要靠它 app = Flask(__name__) # @app.route(path)命令需要配合在其下方的函数一起执行,表示当网址"A.B.C.D:X"+path被访问的时候,下方的函数就会开始运行。传入path表示网页路径,单一的"/"表示根目录 @app.route("/") def home(): # render_template函数中传入的是当前要渲染的网页对应html文件的名字,通常文件结构如下: # │————当前.py文件 # └────templates # │————homePage.html # └────所有html文件均在templates目录下 return render_template("homePage.html") # 以下语句需要写在@app.route()语句下面的函数中 # 返回一个字典化的东西,使得html可以读取到函数返回的信息 return jsonify({"status":"ok"}) # 当html在运行时转到了类似:A.B.C.D:X/something?button=xxx的网址时,"?"后面的东西相当于对python函数中给出了一些变量,必须通过request.args.get()语句获得,并且里面填入的名称必须和传入的变量名相同 button = request.args.get("button") # 实现抓拍并下载监控当前画面功能时,需要用到send_file命令,它会将抓拍下来的照片tmp.jpg作为网页附件发送给手机并自动开始下载,其中download_name表示手机下载时的图片名 send_file('tmp.jpg', as_attachment=True, download_name=curTime+'.jpg') # 实现实时预览监控画面的功能时,需要用到Response函数,其中generate_frames()函数是自己定义的一个获取图像并用yield函数生成数据流的函数,后面的mimetype告诉了浏览器这个地方需要用多个图像不断替换的 return Response( generate_frames(), mimetype="multipart/x-mixed-replace; boundary=frame" ) # 可以在下面的函数中写入资源释放的语句,例如car.Car_Stop(),当Ctrl+C中断程序时就会自动执行该函数 def cleanup_and_exit(signum, frame): sys.exit(0) signal.signal(signal.SIGINT, cleanup_and_exit) signal.signal(signal.SIGTERM, cleanup_and_exit) # 这一句放在if __name__ == "__main__":中,启动网页,开启5000端口 app.run(host = '0.0.0.0', port = 5000, use_reloader = False) html语句: ...
1563.石子游戏V
题目分析 本题首先规定了游戏规则如下: 将当前余下的石头分成左右非空的两部分; 分别计算两部分的石头价值总和,若两者不等,则保留总和较小的一部分,否则任选一部分保留; 将留下的石子价值总和计入$Alice$的得分; 按照上述规则一直进行游戏,直到只剩下一个石子为止。求$Alice$的最大得分。 基本思路 如果进行了一次分割,那么无论最终留下哪一部分,其实问题都变成了一个规模更小但相似的子问题,故可以想到用动态规划进行解决,定义动态规划数组$dp$,其中$dp[i][j]$表示在区间$[i,j)$中$Alice$能够取得的最大得分,那么将$k$在$(i,j)$中遍历,则$dp[i][j]$的计算需要分以下两种情况: 为方便叙述,记$stoneValues[i:k]$的总和为$left$,$stoneValues[k:j]$的总和为$right$。 $$ dp[i][j] = max \begin{cases} dp[i][k]+left &,left\leq right\\ dp[k][j]+right &,left\geq right \end{cases} $$不难发现,由于$stoneValues$中的所有值都是正数,那么随着$k$的增加,$left$必定越来越大,$right$必定越来越小,故一定存在下标$p$,使得当$k\in (i,p]$时均满足$left\leq right$,同理,一定存在下标$q$,使得当$k \in [q,j)$时均满足$left \geq right$,可以发现,当存在一个$k$使得$left=right$时,$q=p$,否则,$q=p+1$。 因此,如果用前缀和数组$s$表示出$left$和$right$,并结合上述分析即可得到: $$ dp[i][j] = max \begin{cases} \max\limits_{k=i+1}^{p}(dp[i][k]+s[k]-s[i])\\ \max\limits_{k=q}^{j-1}(dp[k][j]+s[j]-s[k]) \end{cases} $$将与$k$无关的项提取出来即可得到: $$ dp[i][j] = max \begin{cases} -s[i]+\max\limits_{k=i+1}^{p}(dp[i][k]+s[k])\\ s[j]+\max\limits_{k=q}^{j-1}(dp[k][j]-s[k]) \end{cases} $$可以发现,后面求最大值的项中,$k$都是在区间$[i,j]$的前缀或后缀中遍历的,故如果能维护一个特定区间中指定式子的前缀最大值和后缀最大值,就可以快速求出$dp[i][j]$的值,定义前缀最大值数组$preMax[i][j]$表示$\max\limits_{k=i+1}^{j}(dp[i][k]+s[k])$,同时定义后缀最大值数组$sufMax[i][j]$表示$\max\limits_{k=i}^{j-1}(dp[k][j]-s[k])$,那么上面的递推关系式就变成了: $$ dp[i][j] = max \begin{cases} -s[i]+preMax[i][p]\\ s[j]+sufMax[q][j] \end{cases} $$同时根据定义可以得知$preMax$和$sufMax$数组的递推关系如下: $$ \begin{align} preMax[i][j] = max(preMax[i][j-1], dp[i][j]+s[j])\\ sufMax[i][j] = max(sufMax[i+1][j], dp[i][j]-s[i]) \end{align} $$有了递推关系式,接下来就需要确定区间左右端点遍历的顺序,注意到,在计算$preMax$的值时,位置$(i,j)$是由位置$(i,j-1)$转移过来的,说明$j$的遍历应当从前往后遍历,同时在计算$sufMax$的值时,位置$(i,j)$是由位置$(i+1,j)$转移过来的,说明$i$的遍历应当从后往前遍历。不过需要注意的是,并不是两个数组中的值都需要在$dp[i][j]$计算完毕后才更新,因为在计算$dp[i][j]$时,实际上需要用到一个$preMax[i][p]$,而$p$是在$i$之后的,故应当在向后计算$p$的位置时同步更新$preMax$中的值。 最后在$dp$中所有值计算完毕后,返回$dp[0][n]$即为答案。 实现细节 如果对于每一个区间都用遍历去寻找$p$和$q$的位置,时间复杂度则会到达$O(n^3)$,故需要将其简化,注意到当确定区间左端点$i$之后,由于$j$的遍历顺序是从前往后,因此$p$和$q$的位置一定是不断右移的,故对于每一个固定$i$的循环,都可以初始化一个$p$的位置,随着$j$的增加,每次都用while循环增加$p$直到满足要求,这样,对于每一个不同的$i$,$p$最多也就循环$n$次,而非暴力的$n^2$次,复杂度即可降为$O(n^2)$。 ...
3518.最小回文排列II
题目分析 本题要求的是将$s$重排形成的所有回文串进行排序,并返回第$k$小的那个回文串。 基本思路 首先,既然题目要求将本就是回文串的$s$重排形成另外的回文串,那么实际上需要重排的只是$s$的左边一半,只要得到左边一半的所有字符组成的所有排列中第$k$小的排列,那么将它倒序后和它进行拼接即可得到答案(注意,如果原字符串长度为奇数,则需要将$s$的中间字符插在两个拼接的排列中间,因为整个字符串中只有这一个字符出现了奇数次,无论如何它都必须放在中间)。 因此,问题就转换成了:取出$s$的左边一半$left$,并求它形成的第$k$小的排列。 但,怎么找出第$k$小的排列呢?注意到,如果将一个字符串重排形成的所有排列放在一个列表中进行从小到大排序,那么排完之后,列表中所有元素的首字母一定从左到右是非递减的,同样的,在首字母相同的一段排列中,第2个字母一定从左到右也是非递减的,以此类推,只要一段排列的前$x$个字符都相同,那么它们的第$x+1$个字符一定从左到右是非递减的。综上所述,如果要找到第$k$小的那个排列,可以考虑在每一个位置按照字母从小到大的顺序依次尝试填入,即可保证得到的排列一定是不断变大的。 不过如果直接这样暴力模拟填写字母直到第$k$个,最坏情况下可能要将所有可能的排列都遍历完,复杂度可能达到$O(26^n)$,一定超时。那么如果能够在每一个位置都可以只循环一遍剩余能填的字母就判断出这个位置该填哪个,时间复杂度最坏情况下也只有$O(26n)$(每个位置都遍历所有字母),就可以通过。 如何实现?对于第一个位置,首先从小到大遍历可以选的所有字母,假如现在在第一个位置填上某一个字母,那么后面能填的各种字母的个数必定发生变化,根据剩余字母的个数,结合组合数学即可求出后面$n-1$个位置所有排列的总情况数,加上之前累计的情况数即可得到在该情况下能够达到最大的排列是第几小,如果还未超过$k$,就直接加到累计情况总数中,继续尝试下一个字母,否则,只要大于等于$k$,就不累加并进入到下一个位置的选择中。 这样,从头开始逐位挑选合适的字符,即可让累计情况数不断向$k$逼近,最终到达$k$,将填出来的字串还原即得答案。当然,如果最终选不出和$left$一样长的字符串,就说明总情况数小于$k$,返回"“即可。 计算组合数方法即代码优化 根据上面所说,程序中需要根据一个字符串中各个字符的个数计算它们能够组成的不同排列总数。假设剩余字符总个数为$m$,且$cnt[x]$表示字符$x$的剩余个数,那么不同的排列总数就是: $$ \frac{m!}{cnt[a]!\times cnt[b]!\times \cdots \times cnt[z]!} $$ 上面是未考虑重复字符情况下的总排列数,下面是针对每个字符的重复次数进行去重。 但如果每次计算时都遍历一次所有字母计算排列总数,大数计算次数太多,太慢了,需要进行优化。注意到,如果位置$i$选择填字母$x$,令$m$为从$i$开始到左半边字符串结尾的长度,那么在修改$cnt$数组数据之前计算出来的排列总数为: $$ \frac{m!}{cnt[a]!\times \cdots \times cnt[x]!\times \cdots \times cnt[z]!} $$ 而在修改后计算出来的排列总数为: $$ \frac{(m-1)!}{cnt[a]!\times \cdots \times (cnt[x]-1)!\times \cdots \times cnt[z]!} $$注意到,后者比前者的分母少了$m$,分子少了$cnt[x]$,因此,如果能够保存下来上一个位置选定字母后剩余位置的排列总数$last$,当前选定字母后剩余位置的排列总数就是: $$ last\times \frac{cnt[x]}{m} $$综上所述,如果最开始能够计算出来总共的排列总数,即可在循环中用滚动计算得到当前情况下剩余位置的排列总数。 复杂度 时间复杂度:$O(nlogn)$ 空间复杂度:$O(n)$ 代码 maxNum = 10001 prod = [1]*maxNum for i in range(1, maxNum): prod[i] = prod[i-1]*i def comb(m, n): return prod[m]//(prod[m-n]*prod[n]) class Solution: def smallestPalindrome(self, s: str, k: int) -> str: n = len(s) left = s[:n//2] cnt = [0]*26 for c in left: cnt[ord(c)-ord('a')] += 1 val = prod[n//2] # 计算所有位置的排列总数 for i in cnt: if i == 0: continue val //= prod[i] left = ''.join(sorted(left)) res = "" counted = 0 # 累计排列总数 for i in range(n//2): for j in range(26): # 从小到大试填 if cnt[j]: # 第j个字符还有剩余 cnt[j] -= 1 # 计算当前前缀下剩余位置的排列总数 cur = val*(cnt[j]+1)//(n//2-i) if counted+cur >= k: # 超过了k,可以填入 val = cur # 准备下次计算 res += chr(ord('a')+j) break counted += cur # 计入累计排列总数 cnt[j] += 1 if len(res) < n//2: return "" res = res+(s[n//2] if n&1 else '')+res[::-1] # 还原成回文串 return res
4002.统计有效序列数目
题目分析 本题要求的是长度为$k$的数组数量,且这些数组需要保证: 数组中数字总和为$n$; 数组中数字乘积为偶数; 注意需将求出的数组数量对$10^9+7$取余后返回答案。 基本思路 DFS+记忆化搜索(超时) 分析题目可以发现,实际上这个数组中只需要保证至少有一个偶数,即可满足题目的第2个要求,因此可以用$DFS$进行填数字,最终判断所填的数字中是否有偶数出现,即可判定填出来的数组是否符合要求,具体$DFS$函数如下: def dfs(pos, left, hasEven): if pos == k-1: return int(hasEven | (left%2==0)) res = 0 for i in range(1, left-(k-pos-1)+1): res = (res+dfs(pos+1, left-i, hasEven|(i%2==0)))%modNum return res 需要注意的是,在从左向右第$pos$位上填数字时,若在保证总和为$n$的前提下剩下数字的总和为$left$,那么为保证后面的位置还可以至少填1,当前所能填的最大数字只能是$left-(k-pos-1)$。 不过可惜的是,如此简洁优雅的代码只能通过369/803个测试用例,因此只能更换算法。 组合数计算 前置知识 现有一个问题如下: 已知一个列表长度为$a$,总和是$b$($a\leq b$),且其中所有元素均为正整数,那么共有多少个该种列表? 为解决这个问题,可以这样考虑,如果有$b$个小球顺次排开,在所有缝隙之中插入$a-1$个挡板,只要保证没有两个挡板插在同一个缝隙,且没有挡板插在最左或最右边,那么即可将这$b$个小球分成$a$堆,同时满足每堆数量均为正整数。这样,将每一堆小球的数量组成一个列表,这就是一个符合要求的列表。 而$b$个小球中总共有$b-1$个空隙,现在又要插$a-1$个挡板,那么总共合规的情况数,即满足要求的列表数量即为:$C_{b-1}^{a-1}$. 可以发现,利用组合数其实可以很简单计算出来总情况数量,即$C_{n-1}^{k-1}$,那么如果能用同样的方式计算出来无效序列数目,两个相减即可得到有效序列数目了。 什么是无效序列数目呢?题目要求数组中所有数字的乘积是偶数时才表明该数组是有效序列,那么无效序列就是所有数字乘积为奇数的数组,这么看来,只有全部都是奇数的数组才是无效序列。下面来求全部都是奇数,且总和为$n$,长度为$k$的数组数量。 每一个奇数都可以表示为$2x_i - 1(x_i\geq 1)$,因此要求的实际上就是数列$[x_0, x_1, \dots, x_{k-1}]$的数量,且数列满足: $$ \sum_{i=0}^{k-1} (2x_i-1) = n $$将求和式子展开后可得: $$ 2\sum_{i=0}^{k-1} x_i - k = n $$化简可得: $$ \sum_{i=0}^{k-1} x_i = \frac{n+k}{2} $$因此,问题就转换成了:求长度为$k$,总和为$\frac{n+k}{2}$且均为正整数的数列数量,和「前置知识」中的问题相同,注意到如果$n+k$是奇数,不可能有符合要求的任何数列,故可知,无效序列的总数为: $$ \begin{cases} C_{\frac{n+k}{2}-1}^{k-1} &, n+k为偶数\\ 0 &, n+k为奇数 \end{cases} $$综上所述,即可得知有效序列的总数为: ...
2026年浦东新区人工智能创新应用竞赛Q3:无线数据传输
竞赛链接:2026年浦东新区人工智能创新应用竞赛 如打不开,本文最后有原题截图。 题目分析 本题要求的是工程师的最小移动距离,并且其移动路径应满足: 起始点可以任选; 先检修低阶段的设备,再检修高阶段的设备。 同时,要检修一个设备$i$,工程师的位置必须处在$[segments[i][1], segments[i][2]]$范围内。 基本思路 由于检修设备的阶段应当保持非递减顺序,因此应当按照阶段对所有设备进行分组,并先将低阶段的组内所有设备检修完毕后,才去检修高阶段设备。 检修单个阶段内所有设备 现在先考虑对于一个阶段内的所有设备,如何才能够使将其检修完所移动的距离最小化。这时候就需要将所有可能性划分成两种情况: 这些设备所覆盖的区间均有交集,这时,如果工程师站在这个交集内,他即便是完全不移动,也可以检修完这个区间内的所有设备。并且可以发现:这个交集区间的左端点是所有区间左端点的最大值,右端点则是所有区间右端点的最小值。具体如下图: 这些设备所覆盖的区间并没有交集,这时,工程师无论如何都需要移动,但他移动的最小距离是什么呢?可以发现,如果同样求出来所有区间左端点的最大值$L_{max}$和右端点的最小值$R_{min}$(这时一定是$R_{min} < L_{max}$),那么工程师向左只需要移动到$R_{min}$的位置即可检修左侧需要走最远的设备,同时向右只需要移动到$L_{max}$的位置即可检修右侧需要走最远的设备,因此在这种情况下,工程师至少要走过区间$[R_{min}, L_{max}]$中的所有点,才能处理完所有设备。具体如下图: 综上所述,无论如何都要求出来一个阶段内的左端点最大值和右端点最小值,才能知道处理完这个阶段的所有设备最少需要移动的距离,以及这个移动距离适用的起点范围。 整合多个阶段并计算最小移动距离 有了检修单个阶段内所有设备的最小移动距离,接下来要求的就是将每个阶段连接起来所需要的最小移动距离。按照阶段从小到大处理,对于第$i$大的阶段,如果能够知道从第$i-1$大的阶段转移到当前阶段所需要的最小移动距离,那么问题就转换成了处理第$i-1$大的阶段及之前所有阶段所需要的最小移动距离,这么看来,可以用动态规划来解决该问题。 定义动态规划数组$dp$,其中$dp[i]$表示检修完第$i$大阶段以及之前所有阶段中所有设备所需要的最小移动距离,并且$dp[i]$中存储着多个三元组$[cost, l, r]$,表示在完成这些任务后,工程师到达区间$[l,r]$中任意一个位置所需移动距离均为$cost$。 考虑转移方程,对于第$i$大阶段的处理,总共有以下两种情况: 该阶段内所有区间均有交集,且交集区间为$[L_{max}, R_{min}]$。这样,遍历$dp[i-1]$中的所有数据$[c_{i-1}, l_{i-1}, r_{i-1}]$,总共分三种情况进行讨论: $R_{min} < l_{i-1}$,也就意味着遍历到的这个上一个阶段对应的区间是在当前交集区间右侧的,而只要移动到这个交集区间内就可以不做任何移动处理完所有设备,因此可以从上一个阶段区间的左端点移动到当前交集区间的右端点,这样总共移动的距离就是$c_{i-1}+l_{i-1}-R_{min}$,同时这个距离只能适配一个位置$R_{min}$,故$dp[i]$中应当加上$[c_{i-1}+l_{i-1}-R_{min}, R_{min}, R_{min}]$; $r_{i-1} < L_{max}$,也就意味着上一个阶段遍历到的区间在当前交集区间左侧,与上面的情况同理,需要从上一个区间的右端点移动到当前交集区间的左端点才能使总移动距离最小,故总共移动距离就是 $c_{i-1}+L_{max}-r_{i-1}$,同样,这个距离也只对应一个位置$L_{max}$,故$dp[i]$中应加上$[c_{i-1}+L_{max}-r_{i-1}, L_{max}, L_{max}]$; 剩下情况一定意味着两个区间有交集,那么如果工程师能够处在这两个区间的交集中,他就不需要移动也可以处理完当前阶段内的所有设备,对应的,$dp[i]$中应当加上$[c_{i-1}, max(l_{i-1}, L_{max}), min(r_{i-1}, R_{min})]$。 该阶段所有区间并没有交集,即$R_{min} < L_{max}$,并且工程师至少要经过区间$[R_{min}, L_{max}]$内的所有位置,因此同样遍历$dp[i-1]$的所有数据$[c_{i-1}, l_{i-1}, r_{i-1}]$,分两种情况讨论,注意每种情况需要先遍历一遍求出最小距离后才加入$dp[i]$中: 如果要先到$R_{min}$,再到$L_{max}$,就要找到区间$[l_{i-1}, r_{i-1}]$中的所有位置到$R_{min}$的最短距离,加上$c_{i-1}$,再加上区间$[R_{min}, L_{max}]$的长度,即为当前情况下处理完第$i$大阶段的最小移动距离 (注意:这个距离适配的位置只有$L_{max}$,故加入$dp[i]$时三元组的区间应为$[L_{max},L_{max}]$); 如果要先到$L_{max}$,再到$R_{min}$,就要找到区间$[l_{i-1}, r_{i-1}]$中的所有位置到$L_{max}$的最短距离,加上$c_{i-1}$,再加上区间$[R_{min}, L_{max}]$的长度,即为当前情况下处理完第$i$大阶段的最小移动距离 (注意:这个距离适配的位置只有$R_{min}$,故加入$dp[i]$时三元组的区间应为$[R_{min},R_{min}]$)。 最终返回最后一个$dp$列表中每个三元组中代价的最小值即可。 实现细节 需要注意的是,在动态规划转移方程的情况2的两个子情况中,需要求一个区间$[l,r]$中任意一点到达一个位置$x$的最短距离,可以用一个小函数来分情况计算: $x < l$,则返回$l-x$; $r < x$,则返回$x-r$; 否则,$x$在区间中,返回0. 复杂度 时间复杂度:$O(n\cdot logn)$ 空间复杂度:$O(n)$ ...
Raspbot拍照实现(带预览)
这辆小车最有特色的地方就是它的摄像头以及由此派生出来的视觉处理程序,因此玩视觉的前提就是先将小车的摄像头调用出来,查看它的图像信息。在程序中,可以利用小车自带的$picamera2$库中的函数对摄像头进行读取,具体实现代码如下。 初始化摄像头 from picamera2 import Picamera2, Preview picam2 = Picamera2() # 获取摄像头组件信息,准备好之后调用 调整颜色及方向 摄像头默认的读取参数并不能直接用在显示照片的函数中,两者的不同之处主要有以下几点: 由于安装时的物理问题,摄像头默认是倒过来装的,因此如果直接读取并显示,就会出现照片被旋转了180°的情况; 摄像头对颜色的处理默认是按照$RGB$格式进行转换的,但用$cv2$展示图片时却是按照$BGR$格式进行展示的,因此如果直接读取并显示,就会出现颜色混乱的情况,配色极不正常。 解决上述问题有两种调整方式,一种是直接配置摄像头读取图像时的参数,一种是先按照默认的配置读取图像,之后对读取到的图像用$cv2$库进行调整。 方案1:配置摄像头参数 这里可以直接利用$picam2$和$libcamera$自带的配置函数对摄像头读取参数进行配置。 比较奇怪的一点是,摄像头本身确实是用$RGB$格式读取信息的,这与$cv2$展示时的格式不符合,但在代码中仍然要规定format:RGB888,这是因为根据官方文档的下面一段表述可知,为了让$cv2$用户用得更加方便,在自定义颜色读取时所写的$RGB$实际上代表着底层读取时要用$BGR$。这确实容易让人搞混。 cameraConfig = picam2.create_preview_configuration( main={"format":"RGB888", # 这就是比较奇怪的地方 "size":(picWidth, picHeight)} # 设定摄像头读取图片时的大小 ) cameraConfig["transform"] = libcamera.Transform(vflip=1, hflip=1) # 规定读取图片时将图像旋转180度,即左右和上下均进行翻转。vflip表示垂直翻转,hflip表示上下翻转 picam2.configure(cameraConfig) # 设定完配置后一定要用这句话启用配置! 方案2:读取照片后操作 若已经按照默认参数读取了照片,则需用$cv2$库中自带的函数进行转换。 frame = picam2.capture_array() # 先获取一帧 frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # 将RGB格式转为BGR格式 frame = cv2.flip(frame, -1) 增加预览功能 上面只是说了拍照如何实现,但如果只是拍一张照片,在此之前其实并不能知道摄像头的取景框现在在哪里,有多大,能不能拍全,因此最好加一个拍照前预览几秒的功能。这个功能同样有两种方式可以实现,一种是直接在程序中调用命令行中的预览指令,一种是自己写一个可以持续几秒钟的无限循环。 方案1:模拟命令行执行预览指令 这里需要用到$python$自带的模拟命令行的库以及树莓派提供的预览指令,具体实现如下: import subprocess # 可以模拟在命令行中执行指令 subprocess.run(['rpicam-vid', '-t', '5000', '--vflip', '1', '--hflip', '1']) # 调用树莓派自带的预览指令,其中-t后的数字表示预览持续多少毫秒,--vflip后的数字表示是否进行垂直翻转,--hflip后的数字表示是否进行水平翻转 方案2:编写循环实现 本质上和视频是一个道理,间隔一定时间后更新一帧图片,只要时间间隔足够短,就可以营造出视频的效果,而自定义持续时间即可用$time$库的时间戳之差控制循环的结束。 需要注意的是,如果采用这种方案,必须先采用上面「调整颜色及方向」中的方案1,提前对摄像头进行配置,否则会按照默认的参数进行显示。 import time startTime = time.time() # 获取开始时的时间戳 while time.time()-startTime < 5: # 当预览到达5秒时结束预览 cur = picam2.capture_array() # 获取当前帧 # 将当前帧更新到创建的Preview窗口中,不必担心创建很多窗口,因为imshow函数是根据窗口名称进行更新的 cv2.imshow('Preview', cur) cv2.waitKey(1) # 等待1毫秒,让新的一帧加载到窗口中 cv2.destroyAllWindows() # 删除预览窗口,等待拍照 写入照片文件 写入照片文件非常简单,只需要调用$cv2$中自带的$imwrite$函数即可将照片写入对应名称的文件中。 ...
Pico2小车初始化及功能简介
配置主板 拿到小车,首先将它组装好,然后即可开始配置$Pico2$主板,总共需要下载两个文件,一个是配置$micropython$的$.uf2$文件,一个是官方自带的$pico\_car.py$文件。 下载后,将主板接入电脑,然后会弹出一个类似U盘的文件,首先将$.uf2$文件直接复制到U盘中,这时它会自动重新启动,配置$micropython$环境,之后就会被电脑识别成一个端口,可以通过系统的设备管理器中的端口(COM和LPT)栏目下看到新增的端口,这就是小车的主板,并且在此之后不需要将它作为U盘操作,直接连入电脑,用编辑器写指令即可。 之后是编辑器,为了更方便地控制小车执行指令,需要下载一个Thonny编辑器,在这个编辑器中可以通过上方栏目中的工具->选项->解释器中将解释器设置为MicroPython (Raspberry Pi Pico),这样,在将主板接入电脑后,Thonny就会自动识别该主板,在下方的Shell窗口执行指令即可直接被小车运行。 两个文件均可从官方下载网址中进行下载。 小车基本功能 板载LED灯 该功能可控制$Pico2$主板上的LED灯开关,具体代码如下: import time import machine led_onboard = machine.Pin(25, machine.Pin.OUT) led_onboard.value(1) # 开启LED灯 time.sleep(1) # 等待1秒 led_onboard.value(0) # 关闭LED灯 板载温度传感器 该功能可以利用主板上的温度传感器读取当前环境温度,具体代码如下: import machine sensor_temp = machine.ADC(29) # 29号接口可以传递温度信号 conversion_factor = 3.3 / (4096-1) # 读数与实际温度之间转换因子 reading = sensor.read_u16()*conversion_factor # 读取并转换为实际温度 无源蜂鸣器 该功能可以控制小车车尾部的蜂鸣器发出声音,具体代码如下: from machine import Pin, PWM BZ = PWM(Pin(22)) # 22号针脚连接蜂鸣器,通过发射PWM信号可控制其工作 BZ.duty_u16(500) # 设定为大于0的整数即为开启蜂鸣器,数字越大,蜂鸣器的响度越大 BZ.freq(330) # 设定频率 # CM = [0, 330, 350, 393, 441, 495, 556, 624] 这是各个音调的频率,从1到7 BZ.duty_u16(0) # 设定为0,蜂鸣器关闭 小车灯光 该功能可以独立控制小车下方的总共8个灯的亮度以及颜色,具体代码如下: import time from pico_car import ws2812b # ws2812b为每个灯的芯片名称 num_leds = 8 # 总共8个灯 pixels = ws2812b(num_leds, 0) # 初始化控制灯光的变量 pixels.set_pixel(0, 0, 255, 255) # 将0号灯的RGB设置为(0,255,255) # set_pixel中,第一个变量为灯的编号,其余3个分别为颜色的RGB值,取值从0到255 pixels.show() # 注意!只有执行了这一句话,设定的灯才会亮! time.sleep(1) pixels.set_pixel(0, 0, 0, 0) # 关闭0号灯 小车OLED屏幕显示 该功能可以控制小车外挂的OLED屏幕上面显示文字,具体代码如下: ...
3985.回文子数组求和
题目分析 本题要求在给定的数组$nums$中找到最大的回文子数组的总和。 基本思路 找回文子数组,首先要确定这个子数组的中间点,由于题目数据保证了$1 \leq nums[i] \leq 10^9$,即$nums$中所有数字都是正数,因此如果确定了一个回文子数组的中间点,为了让总和最大化, 就要让回文子数组的长度尽可能大,故问题转换成了:对于每一个位置,求出以它为中间点的回文子数组的最大长度,剩下的求和即可用前缀和直接求解。 根据这个问题,可以想到用$Manacher$算法进行求解。下面详细介绍该算法。 Manacher算法详解 正常来说,如果要用中心扩展法(即选定一个中间点,然后暴力模拟向左右两边扩展回文子数组)来检索数组中的回文子数组,不仅效率低下,同时还要根据子数组长度的奇偶性不同分两种情况考虑,比较复杂。为了解决分情况复杂的问题,$Manacher$算法提出了一个新思路:在原数组每相邻两个数字之间插入相同的分隔数字,然后只寻找长度为奇数的回文串,即可囊括原先的所有情况。 这是为什么呢?如果原数组中有一个回文串,长度是奇数,那么这个回文串中的间隔数量就是偶数,故在这个回文串中的每相邻两个数字之间都插入分隔数字后,整个数组的长度就变成了奇数;同理,如果原回文串长度是偶数,那么间隔数量就是奇数,故插入之后数组长度同样变成了奇数。综上所述,只需在新数组中寻找所有可能的奇数长度的回文串即可。这就简化了原问题。 之后就是优化计算的部分。当使用中心扩展法计算以位置$i$为中心位置的最大回文子数组长度时,如果之前在计算某一个位置的时候已经将子数组右端点扩展超过位置$i$,那么重新开始扩展的前半部分进程其实是和之前重复的,这就可以用以下方法进行简化。 定义数组$P$,其中$P[i]$表示以$i$为中点的回文子数组的最大半径。假设现在要计算$P[i]$,且已经计算出$P[:i]$中的所有值,在已知信息中,曾扩展到达的最右侧位置记为$maxRight$,其对应的子数组中点记为$maxCenter$,那么对$P[i]$的计算就可以分以下两种情况: $i> maxRight$,这时没有任何之前的信息可以利用,只能从它开始用中心扩展法计算; $i \leq maxRight$,这时$i$在以$maxCenter$为中点的最大回文子数组内,也就意味着区间$[2\times i-maxCenter, maxCenter]$在之前已经被遍历过,可以利用之前的信息: 求出$i$关于$maxCenter$的对称点$j$,即得到$j=2\times maxCenter-i$,由于两者都在回文子数组内,所以$i$周围的数字情况一定范围内和$j$周围的数字情况是相同的,因此$P[i]$的值可能是$P[j]$; 由于无法保证在位置$maxCenter$右侧的所有数据都和$2\times maxCenter-maxRight$左侧的数据相同,因此$P[i]$的值应当先和$maxRight-i+1$取一个最小值,防止错算; 从已经确定的$P[i]$最小值开始继续用中心扩展法,寻找更大的可能值。 情况2.2可以借助以下图例进行理解: ①. $i+P[j] \leq maxRight$: ②. $i+P[j]>maxRight$: 综上所述,$P[i]$可以根据情况首先确定初始值,然后再用中心扩展法继续计算,这样就可以优化掉最初循环的那一部分。 实现细节 在本题中,由于数据保证所有数字都大于等于1,且最终要求和,所以分隔数字可以采用0. 在最终计算答案时,对于每一个位置$i$求出其对应的$P[i]$后,即可得到以$i$为中点的最大回文子数组的做右端点,用前缀和进行计算并求最大值即可。 注意在循环过程中需要不断维护已知最大右端点和其对应的子数组中点位置。 复杂度 时间复杂度:$O(n)$ 空间复杂度:$O(n)$ 代码 class Solution: def getSum(self, nums: List[int]) -> int: newNums = [0] for i in nums: newNums.append(i) newNums.append(0) n = len(newNums) preSum = [0]*(n+1) for i in range(n): preSum[i+1] = preSum[i]+newNums[i] res = 0 P = [0]*n P[0] = 1 mxCenter, mxRight = 0, 0 for i in range(1, n): if i <= mxRight: j = 2*mxCenter-i P[i] = min(P[j], mxRight-i+1) l, r = i-P[i], i+P[i] while l>=0 and r<n and newNums[l] == newNums[r]: l -= 1 r += 1 P[i] += 1 res = max(res, (preSum[r]-preSum[l+1])) if r-1 > mxRight: mxRight = r-1 mxCenter = i return res