A clear explanation of solving DI String Match using a greedy two-pointer construction.
Problem Restatement
We are given a string s of length n.
Each character is either:
"I"or:
"D"We need to return any permutation perm of the integers from 0 to n such that:
| Character | Required relation |
|---|---|
s[i] == "I" | perm[i] < perm[i + 1] |
s[i] == "D" | perm[i] > perm[i + 1] |
If multiple answers exist, any valid answer is accepted. The official statement defines the same reconstruction task for a permutation of [0, n].
Input and Output
| Item | Meaning |
|---|---|
| Input | A string s containing only "I" and "D" |
| Output | A permutation of integers from 0 to len(s) |
| Length | Output length is len(s) + 1 |
| Valid answer | Any permutation satisfying all relations |
Function shape:
class Solution:
def diStringMatch(self, s: str) -> list[int]:
...Examples
Example 1:
s = "IDID"One valid answer is:
[0, 4, 1, 3, 2]Check each relation:
| Index | Character | Check |
|---|---|---|
| 0 | I | 0 < 4 |
| 1 | D | 4 > 1 |
| 2 | I | 1 < 3 |
| 3 | D | 3 > 2 |
Example 2:
s = "III"A valid answer is:
[0, 1, 2, 3]Every adjacent pair increases.
Example 3:
s = "DDI"A valid answer is:
[3, 2, 0, 1]The first two relations decrease, and the last relation increases.
These examples match the official examples.
First Thought: Try Permutations
A direct method is to generate every permutation of:
[0, 1, 2, ..., n]Then test whether it satisfies the pattern.
This is not practical.
There are (n + 1)! permutations, while n can be up to 10^5. We need to construct a valid answer directly.
Key Insight
Use the smallest and largest remaining numbers.
Keep two pointers:
| Pointer | Meaning |
|---|---|
low | Smallest unused number |
high | Largest unused number |
When we see "I", we need the current value to be smaller than the next value.
So we can safely place the smallest remaining number.
When we see "D", we need the current value to be larger than the next value.
So we can safely place the largest remaining number.
This works because choosing an extreme value guarantees that the next value can satisfy the relation.
Algorithm
Initialize:
low = 0
high = len(s)
ans = []For each character c in s:
If c == "I":
ans.append(low)
low += 1If c == "D":
ans.append(high)
high -= 1After processing all characters, exactly one number remains.
Append it:
ans.append(low)Return ans.
Walkthrough
Use:
s = "IDID"Start:
low = 0
high = 4
ans = []| Character | Action | ans | Remaining range |
|---|---|---|---|
I | append low = 0 | [0] | 1..4 |
D | append high = 4 | [0, 4] | 1..3 |
I | append low = 1 | [0, 4, 1] | 2..3 |
D | append high = 3 | [0, 4, 1, 3] | 2..2 |
Append the last remaining value:
[0, 4, 1, 3, 2]This satisfies "IDID".
Correctness
The algorithm always uses each number at most once because it only takes from the current unused range [low, high], then moves one boundary inward.
When the current character is "I", the algorithm appends low, the smallest unused number. The next number must come from the remaining range, where every value is greater than low. Therefore, the required relation perm[i] < perm[i + 1] can be satisfied.
When the current character is "D", the algorithm appends high, the largest unused number. The next number must come from the remaining range, where every value is smaller than high. Therefore, the required relation perm[i] > perm[i + 1] can be satisfied.
After processing all n characters, one unused number remains. Appending it completes a permutation of all integers from 0 to n.
Each relation was made valid at the moment its left value was chosen, so the final array satisfies every character in s.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We process each character once |
| Space | O(n) | The output array has length n + 1 |
Implementation
class Solution:
def diStringMatch(self, s: str) -> list[int]:
low = 0
high = 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 ansCode Explanation
low starts as the smallest available number:
low = 0high starts as the largest available number:
high = len(s)For an increasing relation, use the smallest available value:
if c == "I":
ans.append(low)
low += 1For a decreasing relation, use the largest available value:
else:
ans.append(high)
high -= 1At the end, low == high, so either value is the last remaining number:
ans.append(low)Testing
def is_valid(s, perm):
n = len(s)
if sorted(perm) != list(range(n + 1)):
return False
for i, c in enumerate(s):
if c == "I" and not (perm[i] < perm[i + 1]):
return False
if c == "D" and not (perm[i] > perm[i + 1]):
return False
return True
def run_tests():
sol = Solution()
ans = sol.diStringMatch("IDID")
assert is_valid("IDID", ans)
ans = sol.diStringMatch("III")
assert is_valid("III", ans)
ans = sol.diStringMatch("DDI")
assert is_valid("DDI", ans)
ans = sol.diStringMatch("I")
assert is_valid("I", ans)
ans = sol.diStringMatch("D")
assert is_valid("D", ans)
ans = sol.diStringMatch("DIDDI")
assert is_valid("DIDDI", ans)
print("all tests passed")
run_tests()| Test | Why |
|---|---|
"IDID" | Alternating pattern |
"III" | Strictly increasing |
"DDI" | Standard decreasing then increasing |
"I" | Smallest increasing case |
"D" | Smallest decreasing case |
"DIDDI" | Mixed pattern |