news 2026/10/2 3:52:05

滑动窗口巧解两端取卡片的最大点数问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
滑动窗口巧解两端取卡片的最大点数问题

LeetCode 1423 这道 Maximum Points You Can Obtain from Cards(可获得的最大点数),在我的提交记录里耗时稳定在 100ms 附近。第一次看到题目,我以为又是一道“每次从两头挑大的拿”的水题,结果一个反例就让我老老实实坐下来画图。这篇文章我会完整记录一下,我是怎么把“从两端拿卡片”翻译成“找固定长度最小连续子数组”的,为什么贪心不靠谱,以及滑动窗口为什么是这道题最舒服的解法。适合刚接触滑动窗口、还不太会识别题型的读者,也适合刷题时被“两端取数”类题目绕进去的朋友。

1. 题目到底在问什么:一张张从两端拿卡片的取舍问题

1.1 题意与约束条件速览

题目给你一个数组cardPoints,每个元素代表一张卡片的点数。你从数组的最左端或者最右端拿一张卡片,拿走的卡片直接计入得分,总共要拿k张。问最终能拿到的最大点数是多少。

举几个例子。

  • 示例 1:cardPoints = [1,2,3,4,5,6,1],k = 3。最优策略是先拿右边 1,再拿右边 6,再拿左边 1,得分 1+6+1=8 吗?其实不是,我们也可以先拿左边 1,再拿右边 6,再拿右边 1,得分 8;但这道题示例输出是 12,它是左边拿 1、右边拿 6 和 5?注意只能先拿端点,所以 5 必须等 6 被拿掉后才能拿。目标组合是左边拿 1,右边拿 5、6、1 中的两个?这里最优先:先拿左边 1,再拿右边 6,再拿左边 2?不对,我们直接看结论:最优是拿左边 1、右边 6、右边 5?不行,5 不是端点。手动推一下:n=7,k=3,最后拿到的 3 张牌在原始数组中一定是一段前缀加一段后缀。最优是左边取[1],右边取[6,1]? 那是 1+6+1=8。 不对,示例输出 12。再想:左边取[1,2],右边取[6]得分 9。左边取[1],右边取[5,6]?可不可能?拿牌过程:先拿右边 1,再拿右边 6,再拿右边 5,得分 1+6+5=12。是的,因为每次拿右边端点,连续从右边拿三张,最后拿到的就是原数组最右边三个[4,5,6]?不对,数组中右边是 6,1,没有 5。啊,示例数组是[1,2,3,4,5,6,1],右边连续三个是[5,6,1]?端点拿三张:先拿 1,再拿 6,再拿 5,得分 12。对,最右三个是 5,6,1,得分 12。所以最优是全右。这恰好说明要枚举左边取几个、右边取几个。
  • 示例 2:cardPoints = [2,2,2],k = 2,左右各拿一张,得分 4。
  • 示例 3:cardPoints = [9,7,7,9,7,7,9],k = 3,输出 15。
  • 示例 4:cardPoints = [1,100,1],k = 1,输出 1,因为只能拿两端的 1 或 1,拿不到 100。

约束条件很关键:1 <= cardPoints[i] <= 10^4,1 <= k <= cardPoints.length。数组长度最大可以到10^5,这意味着任何O(2^k)的暴力枚举都不可能通过。看到这个数据范围,基本就该往“一趟遍历”或“滑动窗口”的方向想。

1.2 贪心直觉为什么是错的

第一次做这道题的人很容易产生一个直觉:既然每次只能从两端拿,那每次比较一下左端和右端谁大,拿大的不就行了?这是典型的贪心想法,可惜它不保证全局最优。

反例很简单:cardPoints = [8, 100, 1, 9],k = 2。

  • 贪心:左端 8,右端 9,先拿 9,数组变成[8,100,1],再拿 8,总分 17。
  • 最优:拿左端 8,再拿左端 100,总分 108。

为什么贪心错?因为你先拿走某一端后,那一端后面更“肥”的元素才有机会暴露出来。这个决策不是一个独立的端点比较,而是一个“选择左侧连续段还是右侧连续段”的结构性问题。等价地说,你是在决定“从左边拿几个、从右边拿几个”,而不是每一步单独比较端点值的大小。

生活化的类比:排队打饭,你和朋友只能从队伍的两端往里接人,每次只能接队伍最外面的人。队伍开头是两个普通同学,但再往里有几个“大胃王”,而队伍末尾后面全是小胃口。这时候只比较当前最外面的两个人,往往会漏掉藏在里面的高价值目标。

所以看到“只能从两端取”的题,第一反应不要是模拟,而是画一条数组,想想“最后被我拿走的那批元素,在原始数组里会是什么形状”。

1.3 从暴力枚举到可计算模型

既然每一步选左或选右,暴力模拟是2^k种路径。但注意一个关键事实:因为每次都只能从端点取,最终拿走的k张牌,一定是原始数组的左边连续若干个加上右边连续若干个,不可能出现“左侧拿一个,隔一个不拿,再在左侧拿一个”的情况。

这就像你从香蕉的两端剥皮,每个人只能从已经暴露的那端继续往里剥,剥出来的果肉在原始果皮上永远是左边一截加右边一截。

于是我们只需要枚举一个数字:左边拿i张,右边拿k - i张,其中i的取值范围是0到k。如果能在O(k)或O(n)内快速算出每种方案的总分,问题就解决了。

这一段理解是整道题的分水岭。很多题解直接甩出滑动窗口代码,如果你不知道“左连续 + 右连续”这个前提,代码看着就像魔法。

2. 核心思路:把“拿两端”翻过来想

2.1 最终的k张卡片在原始数组里长什么样

先固定一个视角:与其关心“我拿了哪些卡片”,不如关心“我没拿哪些卡片”。

数组长度是n,要拿k张,也就是剩n - k张不被拿走。由于拿走的卡片是左边一段加右边一段,那么没被拿走的卡片自然就是数组正中间的一段连续子数组。注意这段子数组的长度是固定的n - k,并且它在原数组中的位置必须是连续的。

反过来思考:拿走的点数最大,等价于剩下的点数最小。因为整个数组的总点数和是固定的:

拿走的最大值 = 总和 - 剩下部分的最小值

剩下的部分,是一个长度为n - k的连续子数组。于是问题变成了:

在cardPoints中找到一个长度为n - k的连续子数组,让它的和最小。

这比原题直观多了。原题让你从两端抽牌,每次还要考虑左右顺序,很烧脑;转换后就是一个标准的固定长度滑动窗口求最小值问题。

2.2 等效问题:找长度固定的最小连续子数组

来验证一下这个等价关系。

cardPoints = [1,2,3,4,5,6,1],n = 7,k = 3,那么n - k = 4。

我们需要在数组中找一个长度为 4 的连续子数组,使其和最小。

  • [1,2,3,4]和 = 10
  • [2,3,4,5]和 = 14
  • [3,4,5,6]和 = 18
  • [4,5,6,1]和 = 16

最小和是 10,数组总和是 22,所以答案等于 12。这和示例输出完全一致。也就是说,中间留下[1,2,3,4],两头拿[5,6,1],得分 12。拿牌过程就是连续从右端拿 1、6、5。

再看反例[8, 100, 1, 9],n = 4,k = 2,则n - k = 2。

长度为 2 的连续子数组和分别为:

  • [8,100]和 = 108
  • [100,1]和 = 101
  • [1,9]和 = 10

最小和是 10,总和 118,答案 108。刚才那个 108 的最优方案,正是把中间[1,9]留下,两头拿[8,100]。

到这里你应该能感受到,反向思考的威力在于把“决策问题”变成了“查找问题”。

2.3 为什么滑动窗口能拿到正确答案

固定长度连续子数组的最值问题,最经典的做法就是滑动窗口。窗口长度固定为len = n - k,一开始覆盖数组前len个元素,然后窗口整体向右滑动一格。

滑动的时候,窗口左边的元素离开,右边的元素进入。我们维护一个变量win_sum记录当前窗口内所有元素的和。

窗口从[0, len-1]滑到[1, len],只需要做两步:

新的窗口和 = 旧窗口和 - cardPoints[left] + cardPoints[right]

其中left是即将离开窗口的元素下标,right是即将进入窗口的元素下标。每次滑动后更新一次最小值,最后用总和减去这个最小值。

这样做的复杂度是O(n):每个元素被加入窗口一次、移出窗口一次,所有操作都是常数级。空间复杂度O(1),只用了几个变量。

为什么不是O(k)而是O(n)?因为窗口长度是n - k,窗口从头滑到尾需要遍历整个数组。如果k接近n,窗口长度很小,滑动很快;如果k很小,窗口长度接近n,滑动仍然需要遍历完整个数组。无论哪种情况,O(n)都能轻松跑过n <= 10^5的测试数据。

3. 三种可落地写法与复杂度对比

3.1 前缀和枚举法:最符合直觉的写法

如果你一下想不到反向滑动窗口,用“枚举左边拿几张”的思路也能解。思路是:

  1. 先算好前缀和数组prefix,prefix[i]表示前i个元素的和。
  2. 枚举left_count从0到k,表示左边拿left_count张,右边拿right_count = k - left_count张。
  3. 当前方案得分 = 左边left_count张的和 + 右边right_count张的和。
  4. 左边和直接用prefix[left_count];右边和用总和减去前n - right_count个元素的和。

Python 代码如下:

class Solution: def maxScore(self, cardPoints: List[int], k: int) -> int: n = len(cardPoints) prefix = [0] * (n + 1) for i, x in enumerate(cardPoints): prefix[i + 1] = prefix[i] + x total = prefix[n] ans = 0 for left_count in range(k + 1): right_count = k - left_count # 左边前 left_count 个 left_sum = prefix[left_count] # 右边最后 right_count 个 = 总和 - 前 n - right_count 个 right_sum = total - prefix[n - right_count] ans = max(ans, left_sum + right_sum) return ans

这个写法有两个容易踩的坑。

第一,left_count必须从0开始,到k结束,不能少掉“全部取右边”和“全部取左边”两种极端情况。

第二,right_count = 0时,n - right_count = n,prefix[n] = total,所以right_sum = 0,逻辑没有问题。prefix数组长度设为n + 1就是为了应对这种取不到右边的情况。

前缀和枚举的时间复杂度是O(n + k),空间复杂度O(n)。代码很直白,适合在面试中边写边解释。缺点是额外开了一个n+1的数组,在内存限制紧张时不够优雅。

3.2 反向滑动窗口法:最推荐的标准答案

回到我们前面推导出的核心结论:总和固定,剩下中间连续n - k个元素的和最小,拿走的值就最大。

实现的时候,窗口长度我建议用变量名window_len = n - k,不要直接写n-k,不然很容易和“拿k张”搞混。

class Solution: def maxScore(self, cardPoints: List[int], k: int) -> int: n = len(cardPoints) total = sum(cardPoints) # 如果 k 等于 n,代表全部拿走,直接返回总和 if k == n: return total window_len = n - k # 先算第一个窗口的和 win_sum = sum(cardPoints[:window_len]) min_win_sum = win_sum # 窗口向右滑动 for right in range(window_len, n): win_sum += cardPoints[right] - cardPoints[right - window_len] if win_sum < min_win_sum: min_win_sum = win_sum return total - min_win_sum

解释一下win_sum += cardPoints[right] - cardPoints[right - window_len]这一行。

窗口在滑动前覆盖的是下标[right - window_len, right - 1],滑动后覆盖的是[right - window_len + 1, right]。新进入窗口的是cardPoints[right],离开窗口的是窗口最左边的cardPoints[right - window_len]。所以窗口和的变化就是“加新元素、减旧元素”。

C++ 版本同样简洁:

class Solution { public: int maxScore(vector<int>& cardPoints, int k) { int n = cardPoints.size(); int total = accumulate(cardPoints.begin(), cardPoints.end(), 0); if (k == n) return total; int window_len = n - k; int win_sum = accumulate(cardPoints.begin(), cardPoints.begin() + window_len, 0); int min_win_sum = win_sum; for (int i = window_len; i < n; ++i) { win_sum += cardPoints[i] - cardPoints[i - window_len]; min_win_sum = min(min_win_sum, win_sum); } return total - min_win_sum; } };

这个版本的优点是时间O(n)、空间O(1),面试官最喜欢看这种。缺点是需要你先想通“找最小连续子数组”的逆向思维,否则代码容易背错。

3.3 正向双端窗口法:另一种练习思路

既然枚举的是“左边拿几个、右边拿几个”,也可以直接维护一个代表“已拿牌集合”的窗口。先假设全部拿左边的k张,然后逐步把左边牌“吐出来”,换成右边牌。

class Solution: def maxScore(self, cardPoints: List[int], k: int) -> int: n = len(cardPoints) # 全部拿左边 k 张,作为初始方案 cur = sum(cardPoints[:k]) ans = cur # 枚举左边保留的张数 left_count,从 k-1 一直减到 0 for left_count in range(k - 1, -1, -1): # 右边需要拿 right_count 张 right_count = k - left_count # 丢掉当前左边方案中最右边的那张,补上右边新加入的一张 cur += cardPoints[n - right_count] - cardPoints[left_count] ans = max(ans, cur) return ans

我还是用[1,2,3,4,5,6,1],k=3验证一遍。

初始cur = 1+2+3 = 6,方案是左边拿 3 张。

  • left_count = 2,right_count = 1,cur = 6 + cardPoints[6] - cardPoints[2] = 6 + 1 - 3 = 4,方案是左边 2 张[1,2],右边 1 张[1],得分 4。
  • left_count = 1,right_count = 2,cur = 4 + cardPoints[5] - cardPoints[1] = 4 + 6 - 2 = 8,方案是左边[1],右边[6,1],得分 8。
  • left_count = 0,right_count = 3,cur = 8 + cardPoints[4] - cardPoints[0] = 8 + 5 - 1 = 12,方案是右边[5,6,1],得分 12。

这个正向写法的时间复杂度其实是O(k),因为循环次数是k,初始sum也是O(k)。如果k明显小于n,它比反向滑动窗口更快。但从面试稳定性来看,我更推荐反向滑动窗口,因为下标不容易写晕。正向写法适合作为练习,帮助自己加深对“左连续 + 右连续”的理解。

3.4 复杂度对比表

写法核心思路时间复杂度空间复杂度面试推荐度
暴力 DFS 模拟每一步递归选左或选右O(2^k)O(k)不推荐,必超时
贪心每次取大只看当前端点,局部最优O(k)O(1)结果是错的
前缀和枚举枚举左边拿几张O(n + k)O(n)适合先讲思路
反向滑动窗口求最小连续子数组和O(n)O(1)最推荐
正向双端窗口左边逐步换成右边O(k)O(1)适合练习下标控制

很多题解只给出反向滑动窗口,但我建议你把前缀和枚举也写一遍。两种方法看似不同,实际上都是在对“左边取几、右边取几”这个枚举做加速。前缀和枚举是“用空间换时间”,滑动窗口是“用数学等价换空间”。

4. 边界条件、易错点与实测细节

4.1 五个最容易写错的地方

第一个,忽略k == n的情况。如果k == n,代表所有卡片都要拿走,答案就是数组总和。如果不单独处理,window_len = 0,滑动窗口长度为 0,代码会进入一个“空窗口”的奇怪状态,虽然有些写法碰巧能算出正确结果,但逻辑上很危险。

第二个,把窗口长度写成k。这是最经典的错误。反向滑动窗口找的是“中间留下没被拿走的连续段”,长度是n - k,不是k。把窗口长度写成k,代码会去计算“拿走的牌里最小连续子数组”,完全偏离题意。

第三个,前缀和求右侧right_sum时下标写错。右边最后right_count个元素的和,应该是total - prefix[n - right_count],不是prefix[n] - prefix[right_count]。除非你额外维护后缀和数组,否则这里一定要想清楚。

第四个,枚举left_count时漏掉0或k。有些人在for循环里写成range(1, k),结果漏掉了“全部从右边拿”和“全部从左边拿”这两种极端情况。本题两种极端情况完全可能出现,比如示例 1 的最优解就是全部从右边拿。

第五个,试图真的去模拟“从数组里删掉一个元素”。Python 里pop(0)是 O(n) 的操作,在循环里做会直接超时;用deque虽然两端操作是 O(1),但依然要枚举所有选法,指数复杂度躲不掉。正确做法永远是先把问题转化成数学表达式,再用滑动窗口或前缀和去算。

4.2 常见问题速查表

现象可能原因解决办法
提交超时用 DFS 递归枚举左右选择改成滑动窗口 O(n)
答案偏小窗口长度误写成 k改为 window_len = n - k
答案偏大对中间窗口取最大值而不是最小值记录 min_win_sum 而不是 max
结果莫名其妙遗漏 left_count = 0 或 k循环范围改成 range(k + 1)
k == n 报错空窗口导致代码逻辑异常开头直接 return total
下标越界前缀和数组长度不足prefix 开 n+1,多在循环里检查索引

补充一个细节:cardPoints[i]最大是10^4,数组长度最大是10^5,所以总和最大是10^9。C++的int范围大约是-2.1e9到2.1e9,算总和不会溢出。但为了更保险,也为了代码可读性,我建议用long或long long来存储总和与窗口和,特别是当你把这道题的思路迁移到更大数据范围时,这一步能帮你省掉一个隐藏 bug。

4.3 实测耗时与优化心态

我在 LeetCode 上跑 Python 版本的提交,耗时大概在 100ms 左右,内存占用约 14MB。这个数据在同类题目里算正常水平。很多人看到别人用 C++ 可以跑出 30ms,就觉得自己代码写法有问题,实际上不是这样。Python 本身的解释执行开销就摆在那里,只要保证算法是 O(n),100ms 完全足够了。

真正值得优化的不是“从 100ms 压到 80ms”,而是“从 2^k 的超时版本优化到 O(n) 的滑动窗口”。我们要比较的是数量级,不是个位数的毫秒差。

有一次我为了验证滑动窗口逻辑,专门在本地用random生成一个长度为 100000 的随机数组,跑了一遍maxScore,结果不到 0.1 秒。但是同样长度的数组,如果用递归枚举哪怕k只有 30,程序也会卡到怀疑人生。这就是复杂度的意义。

5. 同类题套路与个人刷题建议

5.1 滑动窗口题型的通用模板

做了这道题之后,我最大的收获是终于把滑动窗口的“通用模板”刻进了脑子里。凡是遇到“连续子数组”加“固定长度”或“最大/最小和”这组关键词,都可以往这个框架里套:

  1. 明确窗口长度,或者明确窗口扩张和收缩的条件。
  2. 先初始化第一个窗口,算出初始值。
  3. 用一个变量维护当前窗口的统计值,比如窗口和、窗口最大值、窗口内不同字符数。
  4. 右边界从窗口右端开始向右移动,每次移动一步。
  5. 根据题目要求移动左边界,如果是固定长度窗口,左边界也跟着右移;如果是可变长度窗口,用while收缩左边界。
  6. 每移动一次,更新答案。

对应到本题:

  • 窗口长度固定是n - k。
  • 初始窗口是前n - k个元素。
  • 统计值是窗口和。
  • 右边界从n - k遍历到n - 1,左边界随右边界同步移动。
  • 答案记录的是窗口和的最小值,最后一次用总和减掉它。

如果你已经在心里默念这套模板,那么 643 题(Maximum Average Subarray I)、209 题(Minimum Size Subarray Sum)、1004 题(Max Consecutive Ones III)其实都是一回事,只是统计值和窗口调整条件不同。

5.2 卡片类题型的两个方向

“从两端取数”的题目在 LeetCode 里并不少见,但有一个容易混淆的分支。

第一类,就像今天的 1423 题,限制你”总共取 k 次,每次取左端或右端“。这类题的关键是发现:最后取走的一定是左连续段加右连续段,于是枚举分界点即可。如果你愿意再走一步,还能把它转换成“求剩下连续段的最小和”。

第二类,题目允许你左右端交替取多次,甚至关心取到某个特定值的最小操作数,比如 1658 题(Minimum Operations to Reduce X to Zero)。那道题要求你从两端不断移除元素,使得移除的元素和等于x,问最少移除几个。它等价于:在数组中间找一段连续子数组,使子数组和等于total - x,并且这段子数组要尽可能长。剩下的两端自然就是需要移除的部分。思路和 1423 几乎一模一样,只是把“固定长度窗口”换成了“目标和可变长度窗口”。

我建议你把 1423 和 1658 放在同一天刷。先刷 1423 理解“两端拿牌”的本质,再刷 1658 体验“中间连续段”的变体,你会发现第二道题只需要在第一道题的基础上改几行。

5.3 我的一点心得

这道题刷新了我对“逆向思维”的认知。正面看,你每一步都在做选择,像是在玩一个动态规划游戏;反面看,你只关心哪些牌没被拿走,而没被拿走的牌必定是中间连续一段。这一正一反之间,复杂度和思维量都大幅降低。

我个人在实际操作中还有一个习惯:拿到数组类题目,第一件事永远是在草稿纸上画一条横线,用|把左段、中段、右段分隔开。比如cardPoints = [1,2,3,4,5,6,1],我先画:

[1,2,3,4] | [5,6,1]

中间[1,2,3,4]是“没拿走的”,两段是“要拿走的”。只要把中间段标出来,窗口长度、需要求什么值,全都一目了然。这个方法几乎可以消灭所有下标错误。

最后再分享一个小技巧:如果你的滑动窗口代码跑出了错误答案,不要急着打印整个数组,先用长度为 3 到 5 的简单样例手算一遍,走三四个循环体,错误基本就暴露了。我这道题第一次写反向滑动窗口时,把min_win_sum初始化和窗口更新顺序写反了,结果总是输出总和,就是靠手算[8,100,1,9]这个例子抓出来的。练到最后你会发现,这种“手算小样例”的能力,比记住任何模板都管用。

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

NTFS备用数据流(ADS)深度解析:原理、检测与清理实战

你有没有遇到过这样的遭遇&#xff1a;在Windows里看某个文件&#xff0c;右键属性一切正常&#xff0c;大小、修改时间都对得上&#xff0c;但磁盘空间却莫名其妙少了几个GB&#xff1b;又或者你在某个目录下用dir命令翻来翻去&#xff0c;明明看到一堆文件&#xff0c;可某个…

作者头像 李华
网站建设 2026/10/2 3:51:33

AI NAS本地推理实战:外接GPU与LLM部署指南

1. 从一台"带脑子"的NAS说起&#xff1a;iDX6011 Pro到底想解决什么问题第一次看到"AI NAS"这个词&#xff0c;很多人的反应是&#xff1a;NAS加个AI标签&#xff0c;是不是又一个营销概念&#xff1f;我一开始也这么想。直到把绿联这台 iDX6011 Pro 的架构…

作者头像 李华
网站建设 2026/10/2 3:51:21

Hindsight机制:将失败轨迹转化为零样本强化学习训练信号

2. 核心细节解析与实操要点2.1 零样本泛化&#xff1a;为什么不需要预训练传统上&#xff0c;类似 hindsight 的能力往往依赖大规模预训练数据或循环学习&#xff0c;但在很多实际项目里&#xff0c;我们根本没有足够的标注语料。hindsight 的核心思路是通过对已有失败或低质量…

作者头像 李华
网站建设 2026/10/2 3:51:00

PyTorch图像分类实战:用迁移学习训练12类垃圾识别模型

简介&#xff1a;这套12种生活垃圾分类图像数据集面向图像分类与目标检测场景&#xff0c;覆盖衣服、塑料、废纸等12个常见生活垃圾类别&#xff0c;适合需要标准分类数据进行算法练习、课程设计或项目实验的学习者与开发者。资源压缩包文件条目共2000个&#xff0c;绝大多数为…

作者头像 李华
网站建设 2026/10/2 3:50:58

Agent Skills与MCP的区别与协同:从原理到数据库巡检实战

先说一个我最近经常被问到的问题&#xff1a;“Agent Skills是不是要取代MCP&#xff1f;”问的人多了&#xff0c;我发现大家其实是被两个概念的命名带偏了——都带“Agent/模型”的影子&#xff0c;都像是给AI加能力&#xff0c;很容易被当成同一类东西。实际上在真实项目里&…

作者头像 李华
网站建设 2026/10/2 3:50:26

信息化主管的职责拆解与实操:从选型到数据治理的落地指南

做企业信息化主管这些年&#xff0c;我有一个很深的感触&#xff1a;这个岗位在公司里的位置相当微妙。老板觉得你是“搞IT的”&#xff0c;业务部门觉得你是“管系统的”&#xff0c;下属觉得你是“审批流程的”&#xff0c;只有你自己知道&#xff0c;你其实是在一座由老系统…

作者头像 李华