增减字符串匹配 图解题解
这道题到底在问什么
- 输入
- s = "IDID"
- 输出
- [0,4,1,3,2]
- 输入
- s = "III"
- 输出
- [0,1,2,3](一路递增)
- 输入
- s = "DDI"
- 输出
- [3,2,0,1]
最优解:为什么这么做
一句话答案:LeetCode 942 增减字符串匹配:遇 I 填当前最小、遇 D 填当前最大,两个指针夹着没用过的数一路收缩、末尾再补一个,就是合法排列,时间 O(n)、空间 O(1)。
只含 I 和 D 的串,要还原成怎样的排列
给一个只含 'I' 和 'D'、长度为 n 的字符串 s,要造一个由 0 到 n 这 n+1 个不同整数组成的排列 perm:s 第 i 位是 'I' 就得 perm[i]<perm[i+1],是 'D' 就得 perm[i]>perm[i+1]。合法答案可能不止一个,返回任意一个即可。题面例子 s="IDID",一个合法输出是 [0,4,1,3,2]。
把全排列都排出来逐个验,要试多少个
一个直接的办法,是把 0 到 n 的全排列一个个列出来,逐个检查每一位的增减关系对不对。可 n+1 个数的排列有 (n+1)! 种,阶乘级往上蹿,n 才十几就是上亿个,光是列完都跑不动。O((n+1)!·n) 这条路一开始就走不通。
'I' 该先给最小、'D' 该先给最大
手里攥着一段还没用过的连续整数,最小的记 low、最大的记 high。轮到 'I',它要求后一位更大,那就把当前最小的 low 填进去——剩下没用的数全都比 low 大,后面无论接什么都拉得起这个『更大』。轮到 'D',它要求后一位更小,就填当前最大的 high——剩下的数全比 high 小,后面那位怎么填都够小。每填一个,用掉的正好是区间的一端,剩下的仍是一段连续整数,下一步照样有最小、最大可取,不会填到中途没数可用。
两个指针怎么走,末尾为什么要单独补一格
low 从 0 起、high 从 n 起,从左到右扫 s:遇 'I' 把 low 填进答案、low 加一;遇 'D' 把 high 填进答案、high 减一。扫完 s 的 n 个字符,答案里只有 n 个数,可位置有 n+1 个——循环只填了前 n 位。这时 low 恰好等于 high,正是区间收到最后剩下的那一个数,把它补到末位,n+1 个位置才填满。
s="IDID" 四个字符,逐位填出 [0,4,1,3,2]
初始 low=0、high=4。第 0 位 'I':填 low=0,low 变 1,答案 [0]。第 1 位 'D':填 high=4,high 变 3,答案 [0,4]。第 2 位 'I':填 low=1,low 变 2,答案 [0,4,1]。第 3 位 'D':填 high=3,high 变 2,答案 [0,4,1,3]。四个字符扫完,此时 low 和 high 都等于 2,补到末位,得 [0,4,1,3,2]。回头核一眼:0<4、4>1、1<3、3>2,四段增减和 "IDID" 一位不差。
全 I、全 D 的串会填成什么样
全是 'I' 时每步都取 low,从 0 一路递增到 n,s="III" 填出 [0,1,2,3];全是 'D' 则每步取 high,从大到小排成倒序。两种极端都自然落进同一套规则,用不着写特判。
循环一结束就返回,会少补那最后的 low,答案就短一位,根本凑不齐 0 到 n 的排列;把 'I' 填 high、'D' 填 low 放反,增减关系全体翻转,"IDID" 头一段就对不上。时间上从左到右扫一遍是 O(n),只用 low、high 两个指针、不算输出数组是 O(1)。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3一句话口诀:I 给最小、D 给最大,贪心填。下面每一帧都在套它。
- 4开局:手里有 0 到 8 这 9 个数还没用,最小是 low=0,最大是 high=8。下面从左到右逐位决定放谁。
- 5轮到第 0 位(紫色空位),字符是 'I'。绿色是已经填好的位置。当前还没用的数是 0 到 8 这一段。
- 6'I' 表示后一个要比它大,所以放最小的 0 最稳,剩下的数全都比它大。放完 low 前进到 1。
- 7轮到第 1 位(紫色空位),字符是 'I'。绿色是已经填好的位置。当前还没用的数是 1 到 8 这一段。
- 8'I' 表示后一个要比它大,所以放最小的 1 最稳,剩下的数全都比它大。放完 low 前进到 2。
- 9轮到第 2 位(紫色空位),字符是 'D'。绿色是已经填好的位置。当前还没用的数是 2 到 8 这一段。
- 10'D' 表示后一个要比它小,所以放最大的 8 最稳,剩下的数全都比它小。放完 high 后退到 7。
- 11轮到第 3 位(紫色空位),字符是 'D'。绿色是已经填好的位置。当前还没用的数是 2 到 7 这一段。
- 12'D' 表示后一个要比它小,所以放最大的 7 最稳,剩下的数全都比它小。放完 high 后退到 6。
- 13轮到第 4 位(紫色空位),字符是 'I'。绿色是已经填好的位置。当前还没用的数是 2 到 6 这一段。
- 14'I' 表示后一个要比它大,所以放最小的 2 最稳,剩下的数全都比它大。放完 low 前进到 3。
- 15轮到第 5 位(紫色空位),字符是 'D'。绿色是已经填好的位置。当前还没用的数是 3 到 6 这一段。
- 16'D' 表示后一个要比它小,所以放最大的 6 最稳,剩下的数全都比它小。放完 high 后退到 5。
- 17轮到第 6 位(紫色空位),字符是 'I'。绿色是已经填好的位置。当前还没用的数是 3 到 5 这一段。
- 18'I' 表示后一个要比它大,所以放最小的 3 最稳,剩下的数全都比它大。放完 low 前进到 4。
- 19轮到第 7 位(紫色空位),字符是 'D'。绿色是已经填好的位置。当前还没用的数是 4 到 5 这一段。
- 20'D' 表示后一个要比它小,所以放最大的 5 最稳,剩下的数全都比它小。放完 high 后退到 4。
- 21前 8 位填完后,区间只剩一个数 4(low 和 high 撞在一起)。把它放到最后一格,排列就完整了。
- 22这就是构造出来的排列。整段绿色表示全部就位。下面随手验两位,确认增减关系都对得上。
- 23第 0 位是 'I',要求 perm[0] 比 perm[1] 小:0 确实小于 1,对。
- 24第 2 位是 'D',要求 perm[2] 比 perm[3] 大:8 确实大于 7,对。其余各位同理,全部满足。
⚠️ 容易写错的地方
✗ 错:想枚举全排列去试哪个合法
✓ 对:贪心一遍直接构造,O(n)
排列数是阶乘级,枚举会超时;贪心保证每步都不会卡住
✗ 错:循环完忘了补最后一个数
✓ 对:位置有 n+1 个,循环只填了 n 个,末位再放 low
此时 low 正好等于 high,是区间剩下的唯一一个数
✗ 错:把 I 和 D 放反
✓ 对:I 放最小、D 放最大
I 要后面更大,先放最小才给后面留足空间;D 反过来
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def diStringMatch(self, s: str) -> List[int]:
low, high = 0, len(s)
ans = []
for c in s:
if c == "I":
ans.append(low)
low += 1
else:
ans.append(high)
high -= 1
ans.append(low)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> diStringMatch(string s) {
int n = s.size();
int low = 0, high = n;
vector<int> ans(n + 1);
for (int i = 0; i < n; ++i) {
if (s[i] == 'I') {
ans[i] = low++;
} else {
ans[i] = high--;
}
}
ans[n] = low;
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] diStringMatch(String s) {
int n = s.length();
int low = 0, high = n;
int[] ans = new int[n + 1];
for (int i = 0; i < n; i++) {
if (s.charAt(i) == 'I') {
ans[i] = low++;
} else {
ans[i] = high--;
}
}
ans[n] = low;
return ans;
}
}复杂度
时间
O(n)
从左到右扫一遍字符串
空间
O(1)
只用 low、high 两个指针,不计输出数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 增减字符串匹配 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
贪心一路填下去,凭什么保证不会中途卡住、填不出合法排列?+
每一步填的数都取自『当前还没用过的连续区间 low 到 high』的某一端。填 low,剩下的数全比它大,后面接多少个 'I' 都拉得起;填 high,剩下的全比它小,后面接 'D' 也压得下。区间每步缩一格,n+1 个数恰好填满 n+1 个位置,从头顺到尾,不会冒出『需要更大却没有更大』的死局。
这道题的合法排列唯一吗,为什么可以返回任意一个?+
不唯一。同一个 s 往往对应好几个满足增减关系的排列,题目只要求给出其中一个。这套贪心给的是最规整的一种,比如 s="IDID" 得 [0,4,1,3,2],你换别的填法凑出另一个合法排列同样算对,判题只看每一位的增减关系对不对,不认死某个答案。
填出来的数会不会重复,怎么保证结果正好是 0 到 n 的排列?+
low 每用一次就加一、high 每用一次就减一,两个指针从两头往中间走、走过的值再不回头,所以填进去的数彼此不同。low 和 high 相遇时,中间每个整数都被取过恰好一次,合起来正好是 0 到 n 这 n+1 个数,天然就是一个排列。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 增减字符串匹配 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。