题目要求 输入输出 输入:字符串 s 和字符串列表 wordDict wordDict 中的每个元素都是一个可以使用的单词;同一个单词可以重复使用 输出:如果 s 可以被拆成一个或多个字典单词,返回 True;否则返回 False 拆分必须覆盖整个字符串,不能跳过字符,也不能改变字符顺序 示例 输入:s = "leetcode", wordDict = ["leet", "code"] 输出:True 解释:"leetcode" 可以拆成 "leet" + "code" 输入:s = "applepenapple", wordDict = ["apple", "pen"] 输出:True 解释:"apple" + "pen" + "apple";同一个单词可以重复使用 输入:s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 输出:False 解释:前面可以拼出一些片段,但最后不能覆盖完整字符串 约束 1 <= s.length <= 300 1 <= wordDict.length <= 1000 1 <= wordDict[i].length <= 20 s 和 wordDict[i] 只包含小写英文字母 wordDict 中的单词互不相同 Step 1:先让“已完成的前缀”变得可见 先只问一个具体问题:对于 s = "leetcode",在确认整串能否拆开之前,我们能不能先记录“已经拆完了哪一段”? ...
LeetCode 213:打家劫舍 II,环形拆成两个线性问题
题目要求 输入输出 输入:整数数组 nums,nums[i] 表示第 i 间房子的金额。 房子首尾相连,不能偷相邻的两间房子,因此第 0 间和第 n - 1 间也不能同时偷。 输出:在不触发警报的前提下,最多能偷到的金额。 约束:1 <= nums.length <= 100,0 <= nums[i] <= 1000。 示例 输入:nums = [2,3,2] 输出:3 解释:第 0 间和第 2 间首尾相邻,不能同时偷;选择金额 3 的房子。 输入:nums = [1,2,3,1] 输出:4 解释:可以偷下标 0 和下标 2,也可以偷下标 1 和下标 3。 输入:nums = [1,2,3] 输出:3 这篇只用 Python,先从最小的首尾冲突开始。 Step 1:先处理首尾相邻的冲突 先问一个具体问题:nums = [2,3,2] 时,能不能沿用直线街道的做法,直接在整个数组上选择不相邻的房子? 当前基线只有一条规则:相邻房子不能同时偷。若把这三个数当成直线,可能会把下标 0 和下标 2 当成彼此不相邻,从而错误地选择两端,得到金额 4。 这在环形输入上就会失败:首尾也相邻,0 和 2 必须互斥。要消除这一个冲突,只增加一个分支规则:任何合法方案至少属于下面一个候选分支。 ...
LeetCode 322:零钱兑换,从更小金额推出完全背包 DP
题目要求 给定一个整数数组 coins,其中 coins[i] 是一种硬币的面额;再给定一个非负整数 amount,要求凑出刚好等于 amount 的金额。 每种硬币都可以使用任意次。 返回凑出 amount 所需的最少硬币数量。 如果无法凑出,返回 -1。 如果 amount = 0,不需要使用硬币,答案是 0。 约束是:1 <= coins.length <= 12、1 <= coins[i] <= 2^31 - 1、0 <= amount <= 10^4。硬币面额互不相同。 例如: 输入:coins = [1, 2, 5], amount = 11 输出:3 解释:5 + 5 + 1 一共使用 3 枚硬币。 输入:coins = [2], amount = 3 输出:-1 解释:只使用面额 2 无法凑出 3。 输入:coins = [1], amount = 0 输出:0 解释:目标金额已经是 0,不需要再选硬币。 Step 1:先把目标金额拆成更小的问题 先看一个会逼出问题结构的小例子: ...
LeetCode 300:最长递增子序列,从“以 i 结尾”推出序列 DP
题目要求 输入输出 输入:整数数组 nums 输出:返回 nums 的最长严格递增子序列长度 子序列保持原数组中的相对顺序,但不要求连续,可以跳过元素 如果选择的下标是 i_1 < i_2 < ... < i_k,还必须满足 nums[i_1] < nums[i_2] < ... < nums[i_k] 子序列非空,因此至少选择一个元素 约束:1 <= nums.length <= 2500,-10^4 <= nums[i] <= 10^4 示例 输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:[2,5,7,101] 是一个长度为 4 的严格递增子序列;它不要求在原数组中连续。 输入:nums = [0,1,0,3,2,3] 输出:4 解释:[0,1,2,3] 的长度为 4;`[0,1,3]` 也是合法但更短的选择。 这篇只用 Python,从“每个位置结尾能形成多长”这个局部问题开始,逐步写出可运行解法。 Step 1:先问“以哪个位置结束?” 先看第一个例子: 下标: 0 1 2 3 4 5 6 7 数值: 10 9 2 5 3 7 101 18 答案 [2,5,7,101] 跳过了 9 和 3。当我们走到值为 7 的位置时,不能只问“它左边相邻的数是谁”,而要问: ...
LeetCode 416:分割等和子集,从目标总和推到 0/1 背包倒序遍历
题目要求 给定一个只包含正整数的数组 nums,判断能不能把所有元素分成两个子集,使两个子集的元素总和相等。 输入与输出 输入:整数数组 nums 输出:如果能平分,返回 True;否则返回 False 每个元素必须放入两个子集之一,不能丢弃,也不能把同一个元素使用两次 示例 输入:nums = [1,5,11,5] 输出:True 解释:[1,5,5] 和 [11] 的总和都是 11 输入:nums = [1,2,3,5] 输出:False 解释:总和是 11,奇数不能平分 输入:nums = [1,1] 输出:True 解释:两个子集分别拿一个 1 约束 按 LeetCode 416 题面: 1 <= nums.length <= 200 1 <= nums[i] <= 100 sum(nums) <= 20000 Step 1:先排除总和为奇数的数组 先问一个还不涉及选数的问题:两个整数子集的和相等时,整个数组的总和必须满足什么条件? 问题压力 看 nums = [1,2,3,5]。它的总和是 11。如果两个子集的和都叫 x,那么必须有: 2 * x = 11 整数 x 不可能满足这个等式,所以这个输入可以在尝试任何选数之前直接判定为 False。 ...
Python 中 @staticmethod 和 @abstractmethod 的区别
在阅读 Repository 或 Service 代码时,经常会同时看到 @staticmethod 和 @abstractmethod。它们都写在方法上方,很容易被误认为是同一类装饰器。 实际上,两者解决的是不同问题: @staticmethod 解决方法如何被调用 @abstractmethod 约束子类必须提供什么行为 理解这个区别后,方法应该放在类中、模块中,还是抽象基类中,会清楚很多。 @staticmethod:不依赖实例状态的方法 普通实例方法默认接收 self: class KeywordMatcher: def contains(self, text: str, keyword: str) -> bool: return keyword.casefold() in text.casefold() 如果方法体不读取或修改 self,可以写成静态方法: class KeywordMatcher: @staticmethod def contains(text: str, keyword: str) -> bool: return keyword.casefold() in text.casefold() 调用时不需要创建实例: KeywordMatcher.contains("变压器采购项目", "变压器") 静态方法没有自动传入的 self,因此不能访问实例属性: class KeywordMatcher: def __init__(self, case_sensitive: bool): self.case_sensitive = case_sensitive @staticmethod def contains(text: str, keyword: str) -> bool: # 这里不能访问 self.case_sensitive return keyword in text 如果函数只是通用计算,并不属于某个类的概念,直接定义为模块级函数通常更自然: def contains(text: str, keyword: str) -> bool: return keyword.casefold() in text.casefold() 所以“不需要 self”是使用 @staticmethod 的重要条件,但还要问一句:这个行为是否真的属于该类?如果答案是否定的,就不必为了使用装饰器而把函数塞进类里。 ...
让领域模型和仓储模型各自负责
在业务系统逐渐变复杂时,一个对象往往同时被拿来承载业务规则、数据库字段和 HTTP 响应。开始时这很省事,后来却会出现连锁问题:数据库新增一个内部字段,接口结构跟着变化;领域代码为了查询数据而依赖 ORM;仓储把 SQLAlchemy 行对象直接交给控制器。 更稳定的做法,是让不同边界拥有不同模型,并通过转换连接它们: Controller -> Domain Model -> Repository Contract -> ORM Model -> Database | ^ +------ API Request/Response ---+ 这里的重点不是增加更多目录,而是明确每个模型对谁负责。 四类模型,各自回答一个问题 领域模型回答“什么状态符合业务规则”。例如商品必须有 SKU 和名称,价格不能为负数。SKU 是业务标识,数据库自增 id 则不必进入领域对象。领域模型不应该导入 SQLAlchemy、Pydantic 或 Web 框架。 ORM 模型回答“如何把数据存进数据库”。它可以拥有自增主键、外键、索引、时间戳和数据库约束,但这些细节不必进入领域对象。 Repository 契约回答“应用可以怎样保存和读取数据”。它的参数和返回值属于仓储边界:保存商品可以返回 StoredProduct,其中包含领域对象和 updated_at;分页查询可以返回 ProductPage 这样的查询模型。 API 模型回答“客户端能发送和接收什么”。请求模型负责输入形状,响应模型负责 HTTP 输出。例如商品响应可以包含 updated_at,并将其序列化为日期字符串;这不意味着商品领域对象也需要这个字段。 一个最小例子 假设一个后台允许按 SKU 保存商品,再查询商品信息。下面使用同步 SQLAlchemy 2.x 展示关键代码,省略数据库连接和 HTTP 路由配置。 领域对象放在 app/domain/product.py,只表达业务状态。价格统一使用人民币分,避免浮点数表示金额: from dataclasses import dataclass @dataclass(frozen=True) class Product: sku: str name: str price_cents: int def __post_init__(self): if not self.sku or not self.name: raise ValueError("商品 SKU 和名称不能为空") if self.price_cents < 0: raise ValueError("商品价格不能为负数") 数据库模型放在 infra/db/models.py。自增主键、唯一约束和更新时间属于存储设计: ...
JSON 不只是键值对:Python 列表如何映射到 SQLAlchemy JSON 列
JSON 不只有键值对,还可以表示数组、字符串、数字和布尔值。本文用关键词订阅示例解释 Python tuple、list、dict 与 SQLAlchemy JSON 列之间的映射关系。
Python 类型标注中的 tuple[T, ...]:任意长度和固定长度
Python 类型标注中的 tuple[T, ...]:任意长度和固定长度 副标题: tuple[DocumentBlock, ...] 中的 ... 不是一个要放进元组的值,而是 类型标注中的“任意数量”记号。理解它,才能看懂解析结果为什么使用元组,以及它和列表的边界。 适读人群: 正在使用 Python 类型标注、dataclass 或文档解析模型的开发者 阅读时间: 6 分钟 从 ParsedDocument 的 blocks 字段说起 在文档解析服务中,可以把解析结果表示成一个不可变的数据对象: from dataclasses import dataclass @dataclass(frozen=True) class DocumentBlock: block_id: str raw_text: str @dataclass(frozen=True) class ParsedDocument: blocks: tuple[DocumentBlock, ...] 这里最容易让人疑惑的是: blocks: tuple[DocumentBlock, ...] 这个标注到底表示什么?为什么不是: blocks: tuple[DocumentBlock] 答案是:前者表示“任意长度、每一项都是 DocumentBlock”,后者表示“固定只有一项, 而且这一项是 DocumentBlock”。 tuple[T, ...] 表示任意长度 把它拆开看: tuple[T, ...] ^ ^ | └── 元素数量不限 └────── 每一项的类型是 T 因此: ...
Python 中 if not value 与 is None 的区别
Python 中 if not value 与 is None 的区别 副标题: if not line 判断的是“这个值是否为假值”,if line is None 判断的是“这个值是否就是 None”。两者可能在某些输入上得到相同结果,但表达的业务语义并不相同。 适读人群: 正在学习 Python 条件判断,或需要处理文本、接口参数和可选字段的开发者 阅读时间: 6 分钟 从文档解析中的空行过滤说起 逐行解析文档时,经常会看到下面的代码: for raw_line in lines: line = raw_line.strip() if not line: continue # 继续处理非空内容 这里为什么使用: if not line: 而不是: if line is None: 关键不在于哪种写法更简短,而在于程序此时要识别的是空字符串,不是 缺失值 None。 先跟踪 line 是怎么产生的 这一段代码的执行顺序是: raw_line = " " line = raw_line.strip() str.strip() 会删除字符串首尾的空白字符,并返回一个字符串: assert " 投标人资格要求 ".strip() == "投标人资格要求" assert " ".strip() == "" assert "".strip() == "" 只包含空格的行经过 strip() 后会变成空字符串 "",不会变成 None。 因此后面的条件实际需要回答: ...