MoreRSS

site iconJustYY | 小赖子修改

小赖子的英国生活和资讯,以及投资和个人生活。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

JustYY | 小赖子的 RSS 预览

使用 local_infile=0 和 secure_file_priv=NULL 加固 MySQL 文件访问安全

2026-08-23 05:59:29

MySQL 需要读写自身的数据库文件,但大多数应用数据库并不需要通过 SQL 语句与操作系统交换任意文件。如果一台数据库服务器主要用于 WordPress 或其他 Web 应用,那么启用这些并未使用的文件传输功能,只会增加不必要的攻击面。 下面两个 MySQL 配置可以提供一层非常实用的安全保护:

[mysqld]
local_infile=0
secure_file_priv=NULL
它们保护的是两条不同的文件访问路径。local_infile=0 禁止从数据库客户端所在的机器加载文件,而 secure_file_priv=NULL 则禁止通过相关 SQL 操作直接读写 MySQL 服务器上的文件系统。两者的安全目标有所重叠,但不能互相替代。

“Local”的两种含义

这里的术语很容易令人困惑,因为 LOCAL 指的是 MySQL 客户端所在的机器,而不是数据库服务器本身。 对比下面两条 SQL 语句:
LOAD DATA INFILE '/path/data.csv' INTO TABLE records;

LOAD DATA LOCAL INFILE '/path/data.csv' INTO TABLE records;
不带 LOCAL 时,MySQL 服务器进程会打开数据库服务器上的文件;带有 LOCAL 时,则由客户端程序打开本地文件,再把文件内容传送给 MySQL 服务器。 这一区别也解释了为什么两个配置都需要启用:
配置 保护的文件位置 主要影响的操作
local_infile=0 客户端主机,可能是应用服务器或 Web 服务器 LOAD DATA LOCAL INFILE
secure_file_priv=NULL MySQL 数据库服务器 LOAD DATA INFILESELECT ... INTO OUTFILESELECT ... INTO DUMPFILELOAD_FILE()

为什么要禁用 LOAD DATA LOCAL INFILE

管理员需要导入大型 CSV 文件时,LOAD DATA LOCAL INFILE 确实非常方便。不过,这项功能也跨越了一条重要的安全边界:MySQL 服务器会要求已连接的客户端读取本地文件,并把文件内容传送到服务器。 MySQL 官方文档指出了两个主要风险。首先,恶意或经过修改的数据库服务器可能要求客户端发送另一个文件,而不是客户端原本准备导入的文件。其次,在 Web 环境中,MySQL 的“客户端”通常实际上是 Web 服务器进程,因此可能受到威胁的是该进程有权读取的所有文件,而不只是管理员电脑上的文件。 还有一个原因需要谨慎对待 LOCAL:执行 LOAD DATA LOCAL 并不要求数据库账户拥有 MySQL 的全局 FILE 权限。它主要取决于服务器和客户端是否同时允许 LOCAL 功能。因此,在服务器端禁用它,可以建立一道明确而统一的安全边界:
local_infile=0
设置以后,即使某个客户端库或命令行客户端启用了本地文件加载,服务器仍然会拒绝执行 LOAD DATA LOCAL INFILE。WordPress 数据库通常完全不需要这项功能,因此将其禁用是成本很低的最小权限实践。

为什么要设置 secure_file_priv=NULL

secure_file_priv 变量控制可以直接与数据库服务器文件系统交互的 SQL 操作。在 MySQL 中,它的三种取值具有完全不同的安全含义:
取值 行为
空字符串 在数据库权限和操作系统权限允许的范围内,不限制文件导入和导出;MySQL 官方明确将其描述为不安全的配置
指定目录 只允许在指定目录中进行文件操作
NULL 完全禁用相关的文件导入和导出操作
如果应用数据库从来不需要服务器端的文件导入或导出,那么最严格的设置是:
secure_file_priv=NULL
这一配置会阻止相关 SQL 功能让 MySQL 服务器进程读取其能够访问的文件,或者把查询结果写入服务器文件系统。这个限制非常重要,因为数据库层面的入侵一旦获得文件系统读写能力,就可能进一步演变成范围更大的服务器安全事件。例如,不必要的文件写入能力可能让数据被写入本不应该出现的位置;不必要的文件读取能力则可能泄露 MySQL 操作系统账户能够访问的配置文件或应用数据。 这项设置作为纵深防御措施尤其有价值。正常的应用数据库账户原本就不应该拥有全局 FILE 权限,但配置错误可能发生,凭据可能被滥用,软件漏洞也可能让攻击者执行非预期的 SQL。即使管理员不慎授予了 FILE 权限,secure_file_priv=NULL 仍然可以提供一道服务器级别的最后防线。

为什么两个设置缺一不可?

人们很容易误以为 secure_file_priv=NULL 能够禁止所有形式的文件加载,但它并不能保护客户端一侧的 LOCAL 路径。使用 LOCAL 时,读取源文件的是客户端,而不是 MySQL 服务器,因此服务器端的 FILE 权限和 secure_file_priv 限制并不控制这次读取操作。 反过来,local_infile=0 只会禁用带有 LOCAL 的加载方式。它本身无法阻止拥有足够权限的数据库账户使用 SELECT ... INTO OUTFILE 等服务器端文件操作。 因此,这两个配置分别关闭了两条互补的文件访问路径:
客户端文件系统  --X-->  MySQL          local_infile=0
MySQL           --X--> 服务器文件系统   secure_file_priv=NULL
两者配合使用,可以建立一条简单清晰的安全策略:应用数据只能通过预期的数据库查询和经过批准的备份工具进出数据库,而不能通过通用 SQL 文件操作随意读写文件。

配置和验证

在常见的 Debian 或 Ubuntu 系统中,可以把下面的配置添加到 MySQL 服务器配置文件的 [mysqld] 部分。该文件通常位于 /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
local_infile=0
secure_file_priv=NULL
由于 secure_file_priv 只能在 MySQL 启动时设置,因此修改配置后需要重启 MySQL:
sudo systemctl restart mysql
重启后应当检查实际生效的变量值,而不能只是假设 MySQL 已经读取了预期的配置文件:
SHOW GLOBAL VARIABLES LIKE 'local_infile';
SHOW GLOBAL VARIABLES LIKE 'secure_file_priv';
预期结果应该是 local_infile 的值为 OFFsecure_file_priv 的值为 NULL。 重启之后最好再检查一下 MySQL 错误日志。如果配置项放错了位置、存在重复设置、使用了无效值,或者多个配置文件的加载顺序与预期不同,最终生效的安全策略都可能与你写入的配置不同。

哪些功能可能会受到影响?

只有在正常业务确实不需要相关功能时,才适合采用这些设置。 local_infile=0 会使依赖 LOAD DATA LOCAL INFILE 的工具或脚本无法工作,其中可能包括部分批量 CSV 导入工具和 MySQL Shell 导入流程。secure_file_priv=NULL 会阻止服务器端文件导入和导出,也会影响 mysqldump --tab 之类依赖数据库服务器创建文件的工作方式。 下面这种常规备份方式不会受到影响,因为数据库记录通过正常的客户端协议传输,再由 Shell 把输出内容写入文件:
mysqldump --single-transaction database_name > backup.sql
如果确实需要服务器端导入或导出文件,把 secure_file_priv 限制到一个专用目录,要比将它设置为空字符串安全得多:
secure_file_priv=/var/lib/mysql-files/
这个目录必须预先创建,并应配置严格的所有权和访问权限。目录中不应包含任何应用程序代码或敏感配置。只启用真正需要的功能,将启用时间控制在尽可能短的范围内,并在操作完成后重新禁用。

这些设置不能解决哪些问题?

这两个选项只是安全防护措施,并不能代替完整的数据库安全体系。它们无法防止 SQL 注入,无法阻止攻击者通过普通 SELECT 查询读取数据库记录,也无法保护已经泄露的凭据,更无法限制已经获得操作系统访问权限的进程。它们同样无法弥补应用数据库账户权限过大的问题。 一套合理的数据库安全基线还应该包括:
  • 为每个应用创建独立的数据库账户;
  • 只授予应用实际需要的数据库和数据表权限;
  • 确保应用账户没有全局 FILE 权限;
  • 使用参数化查询,并及时更新应用程序、插件和 MySQL;
  • 限制数据库的网络访问范围;
  • 使用低权限的操作系统账户运行 MySQL;
  • 通过合理的文件系统权限保护配置文件、备份文件和数据库文件;
  • 定期测试备份,并监控备份任务是否正常完成。

总结

安全加固并不总是意味着添加复杂的控制措施。很多时候,更有效的方法是直接关闭业务从未使用的功能。典型的 WordPress 或 Web 应用数据库通常没有理由接受客户端本地文件加载,也不需要允许 SQL 语句直接访问数据库服务器的文件系统。 设置 local_infile=0secure_file_priv=NULL,可以在基本不影响正常应用查询和常规逻辑备份的情况下关闭这两条文件访问路径。这并不会让数据库变得绝对安全,但它能够缩小攻击面,并在其他安全措施失效时提供一道更坚固的最后防线——这正是纵深防御的价值所在。

参考资料

兼容性说明:不同数据库产品和版本接受的配置值及默认行为可能不同。本文所介绍的 secure_file_priv=NULL 行为针对 MySQL。将相同配置应用到 MariaDB 或其他兼容 MySQL 的数据库之前,应先查阅对应产品的文档,并检查配置实际生效后的变量值。
英文:Hardening MySQL File Access with local_infile=0 and secure_file_priv=NULL [show_posts keyword="MySQL"]

误删后通过WinFr来恢复SSD固态硬盘的数据

2026-08-22 02:24:07

一次清理Surface Laptop Studio 2硬盘时,我误用Shift+Delete永久删除了C盘下的tests目录,其中还包含未注意到的瑞士旅行照片。本文记录如何使用微软官方文件恢复工具WinFR,通过Regular和Extensive模式尝试恢复数据,并介绍SSD TRIM、恢复目标盘选择、机械硬盘恢复、回收站设置及RAID与真正备份之间的区别。
我的 Microsoft Surface Laptop Studio 2 是两三年前买的,内置一块 2TB SSD,而且整个硬盘几乎都分给了 C 盘。 前不久,我发现 C 盘的剩余空间已经不到 200GB,于是便想清理一下不再需要的大文件。C 盘根目录下有一个名为 tests 的文件夹,我依稀记得这是去年编译 STEEM 区块链程序、下载区块数据时建立的测试目录。因为当时认定里面都是可以重新下载的数据,我连内容都没有仔细检查,就直接删除了。 更糟糕的是,删除时我还顺手按住了 Shift 键。 在 Windows 中,Shift+Delete 会绕过回收站,直接执行永久删除。删除开始后,我突然看到文件列表中出现了去年去瑞士时用单反拍摄的照片。等我反应过来并点击取消,已经太晚了:C:\tests 下面只剩下一些空目录,文件基本都被删掉了。

不幸中的万幸:照片还有备份

幸运的是,我记得曾经把这些照片复制到由两块18TB机械硬盘组成的 RAID 1 存储中。检查后确认,被删除的瑞士照片确实还有一份完整副本。 不过,我当时仍然不能确定:C:\tests 里面是不是还有其他没有备份的文件? 因此,我决定趁这个机会实际测试一下 Windows 自带的文件恢复工具——Windows File Recovery,也就是 WinFR。 这里必须强调一个容易混淆的概念: RAID 1 不等于备份。 RAID 1 的主要作用是磁盘冗余:其中一块硬盘损坏后,另一块仍然可以继续工作。但是,如果误删文件、文件被病毒加密或者数据发生逻辑损坏,这些操作同样会被同步到两块镜像盘。 这次真正救了我的,并不是 RAID 1 本身,而是我曾经把照片从 Surface 的 SSD 复制到了另一个独立存储设备。也就是说,真正起作用的是“另一份独立副本”。

安装 Windows File Recovery

Windows File Recovery(简称 WinFR)是微软官方推出的一款命令行文件恢复工具,适用于 Windows 10 和 Windows 11,可从 Microsoft Store 免费下载安装。它可以尝试恢复从本地SSD、机械硬盘、外置硬盘及USB存储设备中误删、且无法从回收站找回的文件。WinFR支持Regular和Extensive等恢复模式,并可通过文件名、文件类型或原始路径缩小扫描范围。使用时,恢复目标盘必须不同于发生数据丢失的源盘,以免写入新数据后覆盖尚未恢复的内容。 WinFR 默认没有安装在 Windows 中,需要从 Microsoft Store 搜索并安装 Windows File Recovery。 安装后,以管理员身份打开 Windows File Recovery。它的命令格式非常简单:
winfr 源盘: 目标盘: /模式 /n 路径过滤条件
我的误删目录是:
C:\tests
恢复目标是独立的 D 盘,因此首先执行:
winfr C: D: /regular /n \tests\
其中:
  • C: 是发生误删除的源盘;
  • D: 是保存恢复结果的目标盘;
  • /regular 表示使用普通恢复模式;
  • /n \tests\ 表示尽量只恢复原来位于 C:\tests 下的内容。
NTFS 在常规 Windows 配置下对路径大小写不敏感,因此 \tests\\TESTS\ 等写法通常没有区别。程序显示过滤条件为 TESTS\* 也是正常现象。 恢复完成后,WinFR 会在目标盘自动创建一个带有日期和时间的目录,例如:
D:\Recovery_20260813_230934

为什么恢复目标不能仍然是 C 盘?

文件恢复最重要的原则之一是: 不要把恢复出来的数据写回正在恢复的源盘。 删除文件后,相应的磁盘空间会被文件系统标记为可用。文件内容可能暂时仍然存在,但任何新的写入操作都有可能覆盖这些区域。 如果一边扫描 C 盘,一边又把恢复出来的文件写回 C 盘,就可能出现一种非常荒唐的情况:恢复程序正在用已经恢复的数据覆盖尚未恢复的数据。 微软官方要求源盘和目标盘必须不同。如果电脑只有一个物理硬盘,最好连接一个外置USB硬盘作为恢复目标,也不要在发生数据丢失后临时从源盘划分一个新分区。 Microsoft:Windows File Recovery官方文档 我当时使用的 D 盘是另一套独立存储,因此可以作为恢复目标。 [caption id="attachment_72756" align="alignnone" width="651"]WinFr的使用方法还是挺简单的 WinFr的使用方法还是挺简单的[/caption] 还有一个现实问题:我事先没有安装 WinFR,所以从 Microsoft Store 安装工具本身也会向 C 盘写入少量数据。对于普通恢复场景,这种风险或许可以接受;但如果丢失的是唯一一份、价值极高的数据,最稳妥的做法不是继续在原电脑上安装软件,而是立即停止使用甚至关机,考虑制作磁盘镜像或者交给专业数据恢复人员处理。

/regular/extensive 有什么区别?

WinFR 目前主要提供两种基本模式。

/regular 模式

/regular 适合以下情况:
  • 源盘使用 NTFS 文件系统;
  • 文件刚刚被删除;
  • 文件系统本身没有损坏;
  • 希望优先利用现存的文件系统元数据恢复文件。
例如:
winfr C: D: /regular /n \tests\
这是微软针对“最近从正常 NTFS 分区删除文件”所推荐的第一选择。 在我的测试中,/regular 很快找到了原来的目录结构和大量文件记录,扫描过程中显示的候选文件总数超过了18,000个。

/extensive 模式

如果 /regular 没有找到需要的文件,还可以继续尝试:
winfr C: D: /extensive /n \tests\
/extensive 会执行更加彻底的扫描,适合以下情况:
  • 文件已经删除了一段时间;
  • 磁盘被格式化过;
  • 文件系统已经损坏;
  • 使用 FAT 或 exFAT 等非NTFS文件系统;
  • /regular 没有找到目标文件。
这种模式明显更慢。扫描开始时,进度也可能长时间停留在 00%,并不一定代表程序已经死机。可以在任务管理器中观察源盘是否仍然存在持续读取活动。 微软的建议也是:最近从NTFS删除的数据先尝试 /regular,找不到后再使用 /extensive

为什么文件名恢复了,照片却打不开?

我分别运行了 /regular/extensive。两个模式都恢复出了大量文件,而且不少原始文件名和文件夹结构看起来都很完整。 但是,恢复出来的很多照片却无法打开,Windows提示文件已经损坏。 这其实并不矛盾。 NTFS中文件的元数据和文件实际内容并不是一回事。文件名、路径、大小和时间等信息通常记录在主文件表MFT中,而照片的实际内容则存储在其他数据块中。 删除文件之后,可能出现以下情况:
  1. MFT中的文件名和目录关系仍然存在;
  2. 因此WinFR能够恢复文件名和文件夹结构;
  3. 但是存放照片内容的数据块已经被覆盖,或者被SSD内部清理;
  4. 最终得到一个名称和大小看似正常、实际内容却已经损坏的文件。
所以,“恢复出了文件”不等于“文件恢复成功”。 真正的验证方法是实际打开文件。必要时还要检查文件哈希、图片能否正常解码,以及视频是否能够完整播放。

SSD为什么比机械硬盘更难恢复?

传统机械硬盘删除文件时,通常只是将文件记录和相应扇区标记为可用。只要这些扇区还没有被新数据覆盖,原始内容往往仍然保留在磁盘表面,因此恢复成功率可能比较高。 SSD则更加复杂。 Windows可以通过 TRIM 命令告诉SSD控制器:某些逻辑数据块已经不再使用。SSD随后可能在后台垃圾回收过程中清理这些闪存单元。 这个过程不一定在按下删除键的瞬间全部完成,但通常会大幅缩短数据的可恢复窗口。 这也解释了我的恢复结果:文件系统元数据尚未完全消失,所以目录结构和文件名能够找回来;但是照片对应的数据块可能已经被TRIM和SSD垃圾回收处理,最终恢复出来的照片无法打开。 因此,发现SSD数据误删后,应当尽量:
  • 立即停止向源盘写入新数据;
  • 不要继续下载、更新或安装大量软件;
  • 不要把恢复结果写回源盘;
  • 先检查回收站、OneDrive、File History和其他备份;
  • 数据非常重要时,考虑立即关机并寻求专业帮助。
微软也明确提醒,继续使用电脑可能覆盖已经被标记为可用的区域,而SSD上的已删除数据尤其可能已经无法恢复。 [caption id="attachment_72755" align="alignnone" width="993"]WinFr数据恢复过程的日志 WinFr数据恢复过程的日志[/caption]

为什么还恢复出了不属于 tests 的文件?

恢复结束后,我发现输出目录除了 tests,竟然还有:
Documents
Misc
Pictures
tests
其中甚至包含一些我不知道来自哪里的文件。 这并不意味着命令中的路径过滤完全失效。WinFR是在尝试根据残留的NTFS元数据重建文件,有些文件的原始路径可能已经无法可靠确定。此外,Windows系统盘一直在后台创建和删除临时文件,恢复过程中也可能夹带其他文件记录。 微软官方文档同样承认,从操作系统盘恢复数据时,即使指定了过滤条件,仍然可能有额外文件混入恢复结果。 因此,WinFR不是一个精确的备份还原工具,而是一个“尽力而为”的数据抢救工具。恢复结果需要人工检查,不能把整个恢复目录原样复制回C盘。

WinFR可以恢复机械硬盘吗?

可以。 WinFR支持多种本地存储设备,包括:
  • 内置SSD;
  • 内置机械硬盘;
  • 外置机械硬盘;
  • USB存储设备;
  • 使用NTFS、FAT和exFAT等受支持文件系统的存储设备。
对于最近从健康NTFS机械硬盘中删除的文件,可以先尝试 /regular。如果删除时间较长、磁盘经过格式化,或者文件系统不是NTFS,则可以尝试 /extensive。 因为机械硬盘通常没有SSD的TRIM行为,所以在数据没有被覆盖的前提下,误删文件的恢复成功率往往比SSD更高。 但是,这仍然不是保证:只要旧扇区已经被新数据覆盖,恢复工具同样无能为力。

回收站会保留文件7天吗?

Windows本地回收站并没有一个统一固定的“保留7天”规则。 文件能够在回收站中保留多久,取决于:
  • 用户是否手动清空回收站;
  • 回收站的容量设置;
  • 文件是否因为体积过大而无法进入回收站;
  • 是否启用了Storage Sense;
  • Storage Sense中设置了多久后清理回收站内容。
微软说明,Storage Sense默认处于关闭状态,但在系统空间严重不足时,Windows可能会启用部分清理功能并通知用户。 具体的回收站清理期限可以在下面的位置检查和修改:
设置 → 系统 → 存储 → Storage Sense
然后查看:
删除回收站中存在时间超过……的文件
因此,回收站只是一道安全缓冲,并不是可靠备份。 Microsoft:Storage Sense与回收站清理

这次事故给我的教训

这次误删除最终没有造成不可挽回的照片损失,因为原始照片在另一套存储上还有副本。 但是,WinFR恢复出来的照片大部分已经损坏,也说明SSD上的“永久删除”确实可能很难挽回。 以后清理硬盘时,我会坚持以下原则:
  1. 删除整个目录之前,先打开并抽查里面的内容;
  2. 不再随手使用 Shift+Delete
  3. 大目录先移动到一个“待删除”目录,保留几天后再真正删除;
  4. 在回收站属性中启用删除确认,并确认没有选择“删除文件时不将文件移到回收站”;
  5. 检查Storage Sense的自动清理期限;
  6. 重要文件至少保留多份独立副本;
  7. 定期验证备份,而不是只确认“硬盘还在”;
  8. 对真正重要的数据采用3-2-1备份策略:至少三份数据、两种存储介质,其中一份放在异地或云端。
这次经历也让我真正使用了一次WinFR,并理解了文件名恢复、文件内容恢复、SSD TRIM以及备份之间的区别。 最后的结论其实很简单: [bctt tweet="文件恢复只能作为最后一道补救措施,真正可靠的永远是经过验证的独立备份。"] [caption id="attachment_72757" align="alignnone" width="641"]WinFR数据恢复后的日志/开始使用WinFr后建议不要再进行软件操作了/尽可能减少硬盘恢复过程中的操作 WinFR数据恢复后的日志/开始使用WinFr后建议不要再进行软件操作了/尽可能减少硬盘恢复过程中的操作[/caption] [caption id="attachment_72754" align="alignnone" width="993"]WinFr数据恢复过程的日志 WinFr数据恢复过程的日志[/caption] [caption id="attachment_72753" align="alignnone" width="331"]每次恢复都会新建一个文件夹/时间戳/这样就不会覆盖掉现有的恢复的数据 每次恢复都会新建一个文件夹/时间戳/这样就不会覆盖掉现有的恢复的数据[/caption] [caption id="attachment_72752" align="alignnone" width="762"]WinFR恢复到另一个独立机械硬盘 WinFR恢复到另一个独立机械硬盘[/caption] [caption id="attachment_72751" align="alignnone" width="937"]WinFR数据恢复后的日志 WinFR数据恢复后的日志[/caption]

数据只有备份了才是你的

前些年,我曾在旅行途中(2012年去捷克布拉格回英国酒店checkout后)丢过一块硬盘,也遇到过NAS硬盘故障。还有一次,机箱电源接触不良,不知怎么竟把硬盘烧坏了。当时我花了几百英镑请专业的数据恢复公司抢救,最终数据没有恢复成功,费用却依然要付。从那以后,我便深刻认识到:数据只有做好备份,才算真正属于自己。因此,现在的重要数据我都会保留多份本地副本,同时备份到不同的云端或网络平台,尽量避免单点故障造成无法挽回的损失。 用了两三年的笔记本/硬盘寿命/16848开机小时。 CrystalDiskInfo显示这个SSD固态硬盘还有68%剩余时间。 [caption id="attachment_72750" align="alignnone" width="674"]用了两三年笔记 C盘的寿命还有68%(固态硬盘) 用了两三年笔记 C盘的寿命还有68%(固态硬盘)[/caption] [show_posts keyword="硬盘"] 英文:Accidentally Deleting Over 18,000 Files with Shift+Delete: Recovering Data from a Surface SSD Using WinFR

今年的七夕给媳妇挑了一项链/她说她要换成她喜欢的

2026-08-22 00:25:55

今年七夕临时走了两公里去剑桥 Grand Arcade,为老婆挑了一条价值309英镑的9ct黄金项链。虽然她收到礼物后想换成更喜欢、可能也更贵的款式,但我并不介意:七夕当天收礼物开心一次,周末挑新款还能再开心一次。老夫老妻也需要仪式感——礼物可以换,心意不能省。

七夕礼物可以换,心意不能省

每年的2月14日西方情人节、5月20日,还有中国农历的七夕,都是需要一点仪式感的日子。 虽然我和媳妇已经是老夫老妻,但一句“我爱你”、一个亲亲,再加上一束花或者一件小礼物,我觉得还是不能少。仪式未必要多隆重、多昂贵,重要的是让对方知道:这个日子我记得,你在我心里也依然重要。顺便还能避免一些本来没必要出现的小麻烦,何乐而不为。 不过,七夕是我最容易忘记的一个节日。在英国生活久了,平时基本不看农历,每次都是刷微信群或者朋友圈时,才在最后一刻发现:“原来今天是七夕!”等反应过来,再想提前准备礼物,通常已经来不及了。 今年七夕正好赶上我去公司上班。中午吃完饭后,我想着不如走两公里去剑桥市中心最大的购物中心 Grand Arcade,一边消消食,一边给老婆挑一件礼物,顺便给她一个小惊喜。 [caption id="attachment_72744" align="alignnone" width="945"]中午就当消消食去购物商场给老婆挑七夕礼物 中午就当消消食去购物商场给老婆挑七夕礼物[/caption] 从公司走了二十多分钟到达 Grand Arcade 后,我先去了 Pandora/潘多拉,还顺手注册了会员,可以享受九折优惠。随后又逛了两家珠宝店,最后在 H.Samuel 看中了一条9ct黄金项链。当年和媳妇谈恋爱的时候就到这家店买了情侣戒指。 以前我对英国金饰里的9ct、18ct并没有太多概念,于是现场又问了一下 ChatGPT,临时补习了一些金首饰知识。9ct代表黄金含量为37.5%,其余部分是其他金属合金。它的黄金纯度虽然没有18ct或中国常见的足金那么高,但硬度更高,也更适合做款式精细、需要经常佩戴的首饰,在英国珠宝店里很常见。 我最后选的是一条18英寸的9ct黄金长吊坠项链,价格309英镑。买之前还是小小地纠结了一会儿:一方面觉得一条看起来挺轻的项链卖三百多英镑并不便宜;另一方面又觉得,既然是过节送老婆,也没有必要每次都盯着性价比计算半天。于是刷卡、装盒,然后拎着礼物回家。 [caption id="attachment_72743" align="alignnone" width="2048"]H.Samuel 9ct Yellow Gold Drop Lariat Pendant Necklace

商品编号:9179607
价格:£309
材质:9ct Yellow Gold(9K实金,不是镀金或包金)
长度:18英寸
链型:Petali chain
吊坠:由多条细金链组成的长款下垂吊坠
无宝石
意大利制造 H.Samuel 9ct Yellow Gold Drop Lariat Pendant Necklace
商品编号:9179607
价格:£309
材质:9ct Yellow Gold(9K实金,不是镀金或包金)
长度:18英寸
链型:Petali chain
吊坠:由多条细金链组成的长款下垂吊坠
无宝石
意大利制造[/caption] 老婆看到礼物后果然很开心,不过她猜这条项链只值一百多英镑,还说拿在手里感觉很轻。她顺便拿出了我妈和她妈以前送给她的两条金项链作比较,那两条无论重量还是黄金纯度,看起来都更加“货真价实”。她说,如果是100多英镑,她就可以欣然接受/随便戴戴。 我问她:“那你为什么平时不戴?” 她说:“款式太老了。” 说完,她又把那两条更重、更值钱的“传家宝”收了起来。我笑着说以后可以传给两儿子的老婆。 这其实也说明,金饰不能只看克重和含金量。黄金本身是否保值是一回事,首饰愿不愿意戴又是另一回事。款式不喜欢,再重、再贵,最后也只能长期躺在盒子里;一件经常佩戴、每次看到都会想起送礼人的首饰,才真正发挥了它作为礼物的价值。 后来她问我: [bctt tweet="这条能不能换成我更喜欢的款式?"] 我说:“当然可以,收据都留着呢。” 她马上表示周末要一起去换,而且很可能会换一条更贵的。 我对此倒是无所谓。七夕当天收到礼物,她已经开心了一次;周末再去店里挑选自己真正喜欢的款式,还能再开心一次。一份礼物带来了两次快乐,也算把情绪价值发挥到了极致。 有人觉得夫妻结婚久了,钱都在同一个家庭账户里,送红包就像左手把钱交给右手,没有什么意义。这个说法在账面上没错,但感情从来不只是账面上的数字。直接转一笔钱当然最简单,却少了“我专门为你来回走了4公里、逛了几家店(1小时)、纠结了一会儿,最后挑了这件礼物”的过程。 礼物最后可以退,也可以换,甚至未必是最划算的选择;但收到礼物那一刻的惊喜,以及背后那份惦记,却是真实存在的。 生活还是需要一些这样的小惊喜。婚姻当然不是一笔“我让你开心,你就必须回报我”的交易,但好的情绪会在两个人之间流动。我愿意花一点时间和心思让她高兴,她感受到被重视后,也会更愿意在生活的其他细节里关心我。 老夫老妻并不意味着可以省掉表达。恰恰因为一起生活了很多年,那些看起来微不足道的仪式,才更值得保留。 毕竟,礼物可以换,款式可以重新挑,但心意不能省。 [caption id="attachment_72742" align="alignnone" width="1152"]上个月去泽西岛旅游 在一个海边的海鲜餐厅/媳妇 上个月去泽西岛旅游 在一个海边的海鲜餐厅/媳妇[/caption]

媳妇给我省了80英镑

周六的时候一起去换了,媳妇在那里纠结了很久,还实时问了ChatGPT的意思,选了一个229英镑的,省了80英镑,本来想着80英镑能买个啥,结果也没有合适的,只能作罢。店员换完还打趣的说,你们现在有80英镑可以吃午饭了,哈哈。 我本来说给媳妇挑个耳环啥的,她说,在TEMU上很便宜,反正也看不出来是不是真的金的,相反,要是戴个金的,掉了就很可惜了,况且还不知道戴金的耳环会不会过敏。 换完后就开心的去购物商场的COSTA坐下喝咖啡了,等哥哥下班(哥哥这个暑假在图书馆当志愿者,听小孩讲故事) [caption id="attachment_72769" align="alignnone" width="1152"]和媳妇一起去喝咖啡休息 和媳妇一起去喝咖啡休息[/caption] H.Samuel 9ct Yellow Gold Cubic Zirconia Channel Set Cross 商品编号:4321227 当前价格:£229 材质:9ct黄金 镶嵌:透明立方氧化锆(Cubic Zirconia,简称CZ,不是钻石) 镶法:槽镶/轨道镶(Channel Set) 项链长度:16英寸 十字架宽度:12毫米 [caption id="attachment_72768" align="alignnone" width="1152"]媳妇换了这个项链 媳妇换了这个项链[/caption] [show_file file="/var/www/wp-post-common/justyy.com/wife.php"] [show_posts keyword="情人节"]

从每天记录到真正行动:减重3公斤后,ALT从78降到38

2026-08-20 23:38:31

去年虽然每天记录体重,却几乎没有变化。今年在收到NHS寄来的蓝牙体重秤后,我开始尝试16+8间歇性断食,并坚持每天做俯卧撑、纵跳和深蹲。几个月减重约3公斤后,ALT从最高的78降至38,重新回到正常范围。体重变化虽然不算巨大,身体却已经给出了积极反馈。

从ALT 78降到38:减重3公斤后,身体先给出了反馈

去年,我几乎每天都会称体重、记录体重,但一年下来,曲线基本没有什么变化。 后来我才意识到:记录本身并不会让人变瘦。体重秤只能告诉我发生了什么,却不能替我做出改变。 今年,NHS给我寄来了一台蓝牙体重秤,数据会自动同步到Second Nature App。每天看着体重曲线,我开始觉得不能再只是记录,而是真的应该采取行动了。 更重要的原因,是今年几次抽血都有一些指标偏高:血糖、血脂和肝脏相关指标都不太理想。医生提醒我,如果再不控制体重和改善生活方式,未来患糖尿病的风险可能会越来越高。 这句话还是给了我一些压力。人到中年,减肥已经不只是为了外表,而是为了让身体少承担一点风险。

尝试16+8间歇性断食

最近几周,我基本戒掉了早餐,尽量按照“16+8”的方式控制进食时间。 对我来说,早上晚一点吃,比晚上看着家人吃饭、自己饿着肚子更容易坚持。所以早上通常只喝一杯咖啡,把当天的第一顿饭推迟到中午左右,晚餐则正常吃,但尽量避免太晚进食。 我没有严格计算每一顿饭的卡路里,也没有采取极端节食。核心思路很简单:减少进食时间,少吃一些不必要的东西,让每天摄入的总热量自然降下来。 这种方式未必适合所有人,但至少对我来说,目前比较容易坚持。

每天晚上九点的固定运动

除了饮食调整,我还给自己安排了一套简单的晚间运动。 每天晚上九点左右,我通常会完成:
  • 31个俯卧撑;
  • 3组原地纵跳摸天花板,每组100次;
  • 160个深蹲。
31个俯卧撑是Second Nature App里的每日打卡项目。其他运动则由我自己安排,并记录在Excel表格里。除了外出旅游、实在没有条件完成以外,最近基本每天都在坚持。 这套动作谈不上专业,也不需要健身房或昂贵器械,但它最大的优点是足够简单:在家就能完成,也方便每天打卡。 是否是最科学、最高效的训练方案可以继续研究,但一套普通却能够坚持执行的计划,通常比一套完美却只做几天的计划更有价值。

体重下降不多,身体却先给出了反馈

这几个月,我的体重大约下降了3公斤。单看数字似乎不算特别多,但身体指标已经出现了比较明显的变化。 [caption id="attachment_72727" align="alignnone" width="1788"]每天睡前称,其实不太准确,应该是早上起来拉完粑粑再称。不过我一般是睡前称,这样不容易忘记,和早上称的体重一般差1kg左右(一个晚上代谢1kg左右) 每天睡前称,其实不太准确,应该是早上起来拉完粑粑再称。不过我一般是睡前称,这样不容易忘记,和早上称的体重一般差1kg左右(一个晚上代谢1kg左右)[/caption] 其中最直观的是ALT,也就是丙氨酸氨基转移酶。它主要存在于肝细胞中,升高往往意味着肝脏正在承受某种压力。 我的ALT变化如下:
  • 2026年2月:58 U/L;
  • 2026年3月16日:60 U/L;
  • 2026年3月26日:78 U/L;
  • 2026年8月19日:38 U/L。
正常范围是7—40 U/L。也就是说,ALT从3月份最高的78降到了现在的38,下降约51%,终于重新回到正常范围。 [caption id="attachment_72724" align="alignnone" width="937"]ALT(Alanine Transaminase,丙氨酸氨基转移酶/谷丙转氨酶)指标这次终于正常了。NHS抽血早上9:20左右,晚上18:05收到邮件说结果已经可以登陆mychart查询,这效率还可以。 ALT(Alanine Transaminase,丙氨酸氨基转移酶/谷丙转氨酶)指标这次终于正常了。NHS抽血早上9:20左右,晚上18:05收到邮件说结果已经可以登陆mychart查询,这效率还可以。[/caption] 这次的AST是33,也在正常范围内;FIB-4为0.89,提示晚期肝纤维化风险较低;血小板等其他相关指标也正常。 这次抽血还检查了AST(Aspartate Transaminase),中文通常称为天门冬氨酸氨基转移酶或谷草转氨酶。AST与ALT类似,都是判断细胞是否受到损伤的重要指标,但ALT主要来自肝脏,而AST还广泛存在于肌肉和心脏等组织中,因此AST升高不一定完全由肝脏问题引起,剧烈运动或肌肉损伤也可能影响结果。我的AST是33 U/L,处于13—40 U/L的正常范围;结合已经恢复正常的ALT以及0.89的FIB-4评分,目前的肝脏相关检查结果总体令人放心。 当然,一次抽血结果恢复正常,并不能证明脂肪肝已经彻底消失,也不能把所有改善都简单归功于减掉的3公斤。但至少从目前的趋势来看,我所做的这些改变正在朝正确的方向发展。

NHS与公司的双重监督

前几次抽血时,我有几项肝脏相关指标出现异常。检查结果似乎提示我过去可能感染过乙肝,但后来身体自行清除了病毒。我曾问GP,这些异常是否可能由小时候接种乙肝疫苗引起,得到的答复是否定的。医生还提醒我,将来如果需要做手术或接受某些治疗,应当主动告知医务人员这段病史。为了确认目前肝脏的状况以及是否需要进一步治疗,GP又安排了血液检查和肝脏超声;转诊等待了两三个月后,我在Cambourne的一家小诊所完成了超声检查。所幸结合目前的抽血和检查结果来看,暂时应该不需要进一步治疗。 上个月,我在Cambourne做了肝脏超声检查。上周,NHS又安排我重新抽血。医生表示,如果这次复查结果正常,现阶段应该不需要进一步处理。
Dear XXX, I heard back from the liver specialist - they are suggesting one further blood test to look at how the liver is functioning (a Fib4 test) and if that is OK we need take no further action at this stage. I'll add a link for you to book in for it. 我收到了肝病专科医生的回复——他们建议再做一次血液检查(Fib4 检查)以评估肝脏功能;如果检查结果正常,现阶段就无需采取进一步措施了。我会附上一个链接,方便你预约这项检查。
肝脏专科后来回复说,还需要通过抽血做一次FIB-4评估;如果结果理想,现阶段就不需要采取进一步行动。严格来说,FIB-4并不是直接测量肝脏工作能力的单项检查,而是根据年龄、AST、ALT和血小板数量计算出来的肝纤维化风险指数,主要用来评估是否可能存在较严重的肝脏瘢痕。对于65岁以下成年人,FIB-4低于1.3通常属于低风险;我的结果是0.89,因此晚期肝纤维化的风险较低,也符合专科医生所说的“暂时无须进一步处理”。不过,低风险并不等于脂肪肝已经完全消失,今后仍需要继续控制体重、血糖和血脂,并按照医生建议定期复查。 下个月,Nuffield Health也会安排公司年度体检后的复查。Nuffield还提供一对一的健康指导,可以根据体重、饮食、运动和各项检查结果提出建议。 不过,无论是NHS、Nuffield、蓝牙体重秤,还是Second Nature App,它们能做的都只是提醒、记录和提供建议。最后真正需要执行的人,仍然是自己。

记录不是改变,行动才是

去年我每天记录体重,却几乎没有变化;今年我依然每天记录,但同时开始调整饮食、坚持运动,结果才真正出现。 这大概就是记录和行动之间的区别。 成年人的快乐,有时候并不是体重秤突然少了多少,而是看到体检报告中的箭头终于朝着正确的方向移动。 减重3公斤只是一个开始。接下来不求速度有多快,只希望能够稳定坚持,不反弹,把血糖、血脂和肝脏指标继续控制好。 慢一点没有关系,只要方向是对的,就值得继续走下去。 [caption id="attachment_72726" align="alignnone" width="939"]准确地说,FIB-4不是直接检测“肝脏工作能力”的单项指标,而是利用年龄、AST、ALT和血小板数量计算出来的肝纤维化风险评分。对于65岁以下成年人,低于1.3通常属于低风险;你的结果是 0.89,因此结果很好。 准确地说,FIB-4不是直接检测“肝脏工作能力”的单项指标,而是利用年龄、AST、ALT和血小板数量计算出来的肝纤维化风险评分。对于65岁以下成年人,低于1.3通常属于低风险;你的结果是 0.89,因此结果很好。[/caption] [caption id="attachment_72725" align="alignnone" width="939"]Aspartate transaminase(AST),中文叫天门冬氨酸氨基转移酶,也常称为谷草转氨酶。 Aspartate transaminase(AST),中文叫天门冬氨酸氨基转移酶,也常称为谷草转氨酶。[/caption] [caption id="attachment_72723" align="alignnone" width="1047"]NHS抽血指标都正常(红白细胞/血小板等) NHS抽血指标都正常(红白细胞/血小板等)[/caption] [show_file file="/var/www/wp-post-common/justyy.com/nhs.php"] [show_posts keyword="体检"]

教娃编程第710天:用迭代加深 DFS 求完全平方数的最少数量

2026-08-20 17:50:10

本文解析 LeetCode 279「完全平方数」的一种迭代加深递归解法。借助拉格朗日四平方和定理,算法只需依次判断一个数能否由一个、两个或三个完全平方数组成;如果都不能,答案必然是四。文章还分析了该递归实现的复杂度及优化方法,并与动态规划、广度优先搜索和纯数论解法进行比较。 视频:油管/Youtube | B站/小破站 | 微博视频 | 公众号视频 | 西瓜视频 | 微信视频号 | X/推特 | 小红书 | Facebook | Instagram

LeetCode 279:完全平方数

这是一题非常经典的面试题,而且有多种解法。 给定一个正整数 n,题目要求找出和为 n 的完全平方数的最少数量。 例如:
  • 12 = 4 + 4 + 4,所以答案是 3
  • 13 = 4 + 9,所以答案是 2
  • 16 本身就是完全平方数,所以答案是 1
这道题的常规解法包括动态规划和广度优先搜索。不过,我们还可以利用限深搜索和拉格朗日四平方和定理,写出一个非常简洁的递归解法。

由数学定理引导的递归搜索

代码如下:
from math import isqrt

class Solution:
    def numSquares(self, n: int) -> int:
        sqrs = [i * i for i in range(1, isqrt(n) + 1)]

        def f(cur, i):
            if i == 1:
                return cur in sqrs

            for x in sqrs:
                if f(cur - x, i - 1):
                    return True

            return False

        for i in range(1, 4):
            if f(n, i):
                return i

        return 4
这段代码虽然很短,但其中包含了几个非常重要的思想。

生成所有完全平方数

第一行代码生成所有不大于 n 的正完全平方数:
sqrs = [i * i for i in range(1, isqrt(n) + 1)]
例如,当 n = 13 时:
sqrs = [1, 4, 9]
我们需要考虑的最大平方数是:
isqrt(n) * isqrt(n)
Python 的 isqrt() 会直接返回准确的整数平方根,不需要使用浮点数运算。通常情况下,它比下面这种写法更合适:
int(n ** 0.5)
n 较小时,两种写法都可以正常工作。但是,isqrt() 的含义更加明确,并且可以避免大整数可能遇到的浮点数精度问题。

f(cur, i) 表示什么?

递归函数 f(cur, i) 回答的是一个“是或否”的问题: cur 能否恰好表示为 i 个正完全平方数之和? 例如:
  • f(13, 1) 判断 13 本身是不是一个完全平方数。
  • f(13, 2) 判断 13 能否表示为两个完全平方数之和。
  • f(12, 3) 判断 12 能否表示为三个完全平方数之和。
当只剩下一个平方数需要选择时,问题就变成了一次简单的成员查找:
if i == 1:
    return cur in sqrs
如果 cur 是一个完全平方数,就说明找到了满足条件的表示方法。 否则,函数会选择一个平方数,将它从当前数值中减去,然后递归判断剩余部分能否由更少的平方数组成:
for x in sqrs:
    if f(cur - x, i - 1):
        return True
每一层递归都会重新从 sqrs 的开头遍历,因此同一个平方数可以被重复选择。这一点非常重要,因为有些答案需要重复使用相同的平方数,例如:
12 = 4 + 4 + 4

为什么只需要检查一个、两个和三个平方数?

外层循环按照平方数数量从少到多依次检查:
for i in range(1, 4):
    if f(n, i):
        return i
需要注意的是,range(1, 4) 只会生成:
1, 2, 3
算法并没有真正搜索由四个平方数组成的情况。如果前三次搜索全部失败,就直接返回 4。 这样做的依据是拉格朗日四平方和定理: 每一个正整数都可以表示为至多四个整数平方数之和。 因此,这道题的答案只可能是 123 或者 4。 算法按照从小到大的顺序检查这些答案,所以第一次成功时,得到的一定是最少数量。如果使用一个、两个或者三个完全平方数都无法组成 n,那么答案就只能是 4。 这个数学定理不仅仅是一个小优化。它正是递归搜索深度可以被限制在常数范围内的根本原因。

示例:n = 13

算法首先计算:
f(13, 1)
因为 13 不在 [1, 4, 9] 中,所以结果为 False。 接下来计算:
f(13, 2)
假设循环选择了 4,递归调用就会变成:
f(13 - 4, 1)
f(9, 1)
因为 9 是一个完全平方数,所以函数返回 True。也就是说:
13 = 4 + 9
最终答案是 2

示例:n = 12

只使用一个平方数的搜索会失败,因为 12 不是完全平方数。 只使用两个平方数的搜索也会失败,因为 12 无法表示为两个正完全平方数之和。 在搜索三个平方数时,递归可以找到:
12 - 4 = 8
8 - 4 = 4
剩余的 4 是完全平方数。因此:
12 = 4 + 4 + 4
答案是 3

一个性能细节:sqrs 是列表

下面的表达式会进行线性查找,因为 sqrs 是一个列表:
cur in sqrs
m = floor(sqrt(n)),那么列表中一共有 m 个完全平方数。 在搜索三个平方数时,递归可能需要先选择两个平方数,然后才执行最后的成员查找。在最坏情况下,操作次数大约为:
m * m * m
因此,原始代码最坏情况下的时间复杂度是:
O(m³) = O(n^(3/2))
递归深度最多只有三层,所以递归栈使用的空间是常数级别。存储所有平方数的列表需要 O(sqrt(n)) 空间。 对于这道题相对较小的数据范围,这个简洁的实现仍然可以通过。不过,如果使用集合进行成员查找,就可以将平均查找时间降低到常数级别。

改进后的递归版本

我们可以保留原来的核心思路,同时增加一个集合用于快速查找,并在当前平方数已经过大时提前结束循环:
from math import isqrt

class Solution:
    def numSquares(self, n: int) -> int:
        squares = [i * i for i in range(1, isqrt(n) + 1)]
        square_set = set(squares)

        def can_sum(cur, count):
            if count == 1:
                return cur in square_set

            # 剩余的 count - 1 个平方数都至少为 1
            limit = cur - (count - 1)

            for square in squares:
                if square > limit:
                    break

                if can_sum(cur - square, count - 1):
                    return True

            return False

        for count in range(1, 4):
            if can_sum(n, count):
                return count

        return 4
递归仍然执行限深搜索,但最后一步判断一个数是否为完全平方数时,现在平均只需要 O(1) 时间。 对于三个平方数的情况,算法最多枚举两层平方数,第三个平方数通过集合直接判断。其最坏时间复杂度大约可以降低为:
O(m²) = O(n)
剪枝条件还可以阻止递归继续探索那些已经不可能容纳足够数量正完全平方数的分支。

动态规划

这道题最常见的通用解法是动态规划。 定义 dp[x] 表示组成 x 所需要的最少完全平方数数量。如果最后选择的平方数是 s,那么状态转移方程为:
dp[x] = min(dp[x], dp[x - s] + 1)
完整实现如下:
from math import isqrt

class Solution:
    def numSquares(self, n: int) -> int:
        squares = [i * i for i in range(1, isqrt(n) + 1)]
        dp = [0] + [float("inf")] * n

        for value in range(1, n + 1):
            for square in squares:
                if square > value:
                    break

                dp[value] = min(
                    dp[value],
                    dp[value - square] + 1
                )

        return dp[n]
n = 12 时,部分状态如下:
  • dp[1] = 1,使用一个 1
  • dp[4] = 1,使用一个 4
  • dp[8] = 2,使用 4 + 4
  • dp[12] = 3,使用 4 + 4 + 4
算法一共有 n 个状态,每个状态最多需要检查 sqrt(n) 个完全平方数。 复杂度为:
  • 时间复杂度:O(n sqrt(n))
  • 空间复杂度:O(n)
动态规划的正确性并不依赖拉格朗日四平方和定理,而且很容易扩展到其他最少硬币数、最少元素组合等类似问题。

广度优先搜索

我们还可以把这道题理解为一个无权图中的最短路径问题。 将每一个“剩余数值”看成图中的一个节点。从数值 x 出发,可以减去任何一个不大于 x 的完全平方数,从而到达下一个节点。 例如,从 13 出发,可以到达:
13 - 1 = 12
13 - 4 = 9
13 - 9 = 4
每一条边代表选择了一个完全平方数。因此,从 n 到 0 的最短距离,就是所需要的最少平方数数量。
from collections import deque
from math import isqrt

class Solution:
    def numSquares(self, n: int) -> int:
        squares = [i * i for i in range(1, isqrt(n) + 1)]

        queue = deque([(n, 0)])
        seen = {n}

        while queue:
            remaining, depth = queue.popleft()

            for square in squares:
                if square > remaining:
                    break

                next_remaining = remaining - square

                if next_remaining == 0:
                    return depth + 1

                if next_remaining not in seen:
                    seen.add(next_remaining)
                    queue.append((next_remaining, depth + 1))
BFS 会先检查所有只使用一个平方数的表示方法,然后检查使用两个平方数的情况,再检查三个平方数的情况,以此类推。因此,它第一次到达 0 时,所经过的层数就是最少平方数数量。 最坏情况下的复杂度为:
  • 时间复杂度:O(n sqrt(n))
  • 空间复杂度:O(n)
从本质上看,BFS 和动态规划解决的是同一个状态转移问题。动态规划按照数值顺序填充状态,而 BFS 按照距离起点的层数逐层探索状态。

纯数学解法

我们还可以进一步利用数论,几乎完全避免动态规划、BFS 和递归枚举。 这个解法结合了两个数学定理:
  • 拉格朗日四平方和定理保证答案最多为四。
  • 勒让德三平方和定理可以判断一个整数什么时候无法表示为三个平方数之和。
勒让德三平方和定理指出,一个正整数无法表示为三个整数平方数之和,当且仅当它可以写成下面的形式:
4^a * (8b + 7)
因此,可以得到下面的实现:
from math import isqrt

class Solution:
    def numSquares(self, n: int) -> int:
        def is_square(value):
            root = isqrt(value)
            return root * root == value

        if is_square(n):
            return 1

        for a in range(1, isqrt(n) + 1):
            if is_square(n - a * a):
                return 2

        reduced = n

        while reduced % 4 == 0:
            reduced //= 4

        if reduced % 8 == 7:
            return 4

        return 3
整体逻辑如下:
  1. 如果 n 本身是完全平方数,返回 1
  2. 如果存在某个 a,使得 n - a² 也是完全平方数,返回 2
  3. 不断移除因子 4 后,如果剩余数字模 8 等于 7,则返回 4
  4. 否则,在已经排除答案 1 和 2 的情况下,答案一定是 3
这种方法的时间复杂度为 O(sqrt(n)),额外空间复杂度为 O(1)。从渐进复杂度来看,这是最快的解法,但它依赖特定的数学定理,无法像动态规划那样直接推广到普通的硬币组合问题。

不同解法对比

解法 时间复杂度 空间复杂度 主要优点
原始限深 DFS O(n^(3/2)) O(sqrt(n)) 代码非常简洁,直接利用四平方数上界
使用集合查找的 DFS O(n) O(sqrt(n)) 保留优雅的递归结构,同时提高查找效率
动态规划 O(n sqrt(n)) O(n) 通用性强,容易扩展到类似问题
广度优先搜索 O(n sqrt(n)) O(n) 具有直观的最短路径解释
数论 O(sqrt(n)) O(1) 理论复杂度最优

总结

这个递归解法最有意思的地方,在于它将暴力搜索与一个强有力的数学上界结合起来。如果没有拉格朗日四平方和定理,在只检查前三种情况后直接返回 4 是缺乏逻辑依据的。有了这个定理,递归搜索的深度就永远不需要超过三层。 因此,与其将这个算法简单地称为暴力递归,不如称为“由数学定理引导的迭代加深限深搜索”。 原始实现已经非常简洁、易懂。它最主要的性能问题是 cur in sqrs 会对列表进行线性查找。增加一个集合后,就可以将完全平方数判断的平均时间复杂度降低到 O(1),从而显著降低整个搜索的最坏时间复杂度。 对于这道特定题目,数论解法的效率最高。不过,从学习可复用算法模式的角度来看,动态规划和 BFS 更有价值。限深递归解法则处于两者之间:代码简洁、思路直观,同时很好地展示了数学知识如何大幅缩小算法的搜索空间。 [show_file file="/var/www/wp-post-common/justyy.com/teaching-kids-programming.php"] [show_posts keyword="教娃"] 英文:Teaching Kids Programming - Minimum Number of Perfect Squares via Theorem-guided iterative deepening DFS

小时候嫌土,长大后才听懂《欢喜就好》

2026-08-18 23:43:29

多年后重听闽南语歌曲《欢喜就好》,才发现小时候觉得又土又俗的歌词,唱尽了普通人的欲望、纠结与不满足。得不到时嫌不够,得到了又怕失去;兜兜转转,人生最通透的道理或许就是:欢喜就好。 多年后重听《欢喜就好》,才发现唱的全是人生 小时候嫌土,长大后才听懂《欢喜就好》 《欢喜就好》:小时候听热闹,长大后听人生 人到中年,终于听懂了《欢喜就好》 《欢喜就好》:看似诙谐,唱的却是人生 从“嫌土”到“听懂”:多年后重听《欢喜就好》 一首《欢喜就好》,唱透普通人的纠结 得不到时嫌不够,得到了又怕失去 人生海海,欢喜就好 《欢喜就好》:兜兜转转,开心最重要 闽南语神曲《欢喜就好》:唱尽人生百态
[caption id="attachment_72708" align="alignnone" width="2048"]浙江卫视《天赐的声音》里再次听到《欢喜就好》:嫌饭菜煮得不好吃,嫌老婆不够漂亮 。 浙江卫视《天赐的声音》里再次听到《欢喜就好》:嫌饭菜煮得不好吃,嫌老婆不够漂亮 。[/caption] [caption id="attachment_72707" align="alignnone" width="2048"]浙江卫视《天赐的声音》里再次听到《欢喜就好》:整天嫌车不够拉风,嫌房车不够大。 浙江卫视《天赐的声音》里再次听到《欢喜就好》:整天嫌车不够拉风,嫌房车不够大。[/caption] 多年后,在浙江卫视《天赐的声音》里再次听到《欢喜就好》,一下子勾起了许多回忆。这是我最喜欢的一首闽南语歌。我喜欢的还有《爱拼才会赢》——这首应该是最家喻户晓的;刘德华的《世界第一等》也很经典;《风真透》很难唱,却有一种莫名的魔性;还有相对小众的《金包银》,也是越听越上头。 《欢喜就好》的原唱是陈雷,如今也已经六十多岁了。小时候听这首歌,只觉得又土又俗;到了这个年纪再听,才发现唱的哪里是玩笑,分明都是人生。 “吃得太好怕血压高”,一句话就把人的纠结唱明白了。整首歌看似也挺“矛盾”:嫌老婆不够漂亮,老婆太漂亮了又怕她跟别人跑,哈哈!嫌车不够气派,真开上好车又担心被偷;嫌房子不够大,住进大房子又嫌难打扫。 特别是这几句抱怨连在一起,听起来有些滑稽,却唱尽了人生百态:得不到时嫌不够,得到了又怕失去。人好像总能给自己找到不满足的理由,真是“自是人生长恨水长东”。 兜兜转转,才发现最通透的并不是什么大道理,而是最朴素的四个字:欢喜就好。😄 PS:我媳妇从小在莆田长大,后来才搬到福州,所以不太会说福州话。不过她觉得,闽南语和莆田话听起来有些相似。

闽南神曲《欢喜就好》歌词

人生海海 甘需要拢了解(人生茫茫,难道每件事都需要弄明白吗?) 有时仔清醒 有时青菜(有时候认真清醒,有时候随性一点) 人生海海 甘需要拢了解(人生茫茫,难道每件事都需要弄明白吗?) 有时仔清醒 有时青菜(有时候认真清醒,有时候随性一点) 有人讲好 一定有人讲歹(有人说你好,就一定会有人说你不好) 若麦想吓多 咱生活卡自在(如果不要想那么多,我们就能活得更自在) 归工嫌车无够叭 嫌厝无够大(整天嫌车子不够气派,嫌房子不够大) 嫌菜煮了无好吃 嫌某尚歹看(嫌饭菜做得不好吃,又嫌老婆不好看) 驶到好车惊人偷 大厝歹拼扫(开上好车又怕被偷,住进大房子又难打扫) 吃甲尚好惊血压高(吃得太好,又怕血压升高) 水某会兑人走(老婆太漂亮,又怕她跟别人跑了) 人生短短 好亲像块七逃(人生短暂,就像来世间游玩一趟) 有时仔烦恼 有时轻可(有时候烦恼,有时候轻松自在) 问我到底 腹内有啥法宝(问我心里到底藏着什么法宝) 其实无撇步 欢喜就好(其实没有什么秘诀,开心就好) 人生海海 甘需要拢了解(人生茫茫,难道每件事都需要弄明白吗?) 有时仔清醒 有时青菜(有时候认真清醒,有时候随性一点) 有人讲好 一定有人讲歹(有人说你好,就一定会有人说你不好) 若麦想吓多 咱生活卡自在(如果不要想那么多,我们就能活得更自在) 归工嫌车无够叭 嫌厝无够大(整天嫌车子不够气派,嫌房子不够大) 嫌菜煮了无好吃 嫌某尚歹看(嫌饭菜做得不好吃,又嫌老婆不好看) 驶到好车惊人偷 大厝歹拼扫(开上好车又怕被偷,住进大房子又难打扫) 吃甲尚好惊血压高(吃得太好,又怕血压升高) 水某会兑人走(老婆太漂亮,又怕她跟别人跑了) 人生短短 好亲像块七逃(人生短暂,就像来世间游玩一趟) 有时仔烦恼 有时轻可(有时候烦恼,有时候轻松自在) 问我到底 腹内有啥法宝(问我心里到底藏着什么法宝) 其实无撇步 欢喜就好(其实没有什么秘诀,开心就好) 归工嫌车无够叭 嫌厝无够大(整天嫌车子不够气派,嫌房子不够大) 嫌菜煮了无好吃 嫌某尚歹看(嫌饭菜做得不好吃,又嫌老婆不好看) 驶到好车惊人偷 大厝歹拼扫(开上好车又怕被偷,住进大房子又难打扫) 吃甲尚好惊血压高(吃得太好,又怕血压升高) 水某会兑人走(老婆太漂亮,又怕她跟别人跑了) 人生短短 好亲像块七逃(人生短暂,就像来世间游玩一趟) 有时仔烦恼 有时轻可(有时候烦恼,有时候轻松自在) 问我到底 腹内有啥法宝(问我心里到底藏着什么法宝) 其实无撇步 欢喜就好(其实没有什么秘诀,开心就好) 视频:油管/Youtube | B站/小破站 | 微博视频 | 公众号视频 | 西瓜视频 | 微信视频号 | X/推特 | 小红书 | Facebook