CF 105335L - Lulu and Friends
We have a fixed string T of length at most 20. For each query string s, we may delete any characters from T, keeping the relative order of the remaining characters. The goal is to make the resulting string contain s as a contiguous substring.
Rating: -
Tags: -
Solve time: 58s
Verified: yes
Solution
Problem Understanding
We have a fixed string T of length at most 20. For each query string s, we may delete any characters from T, keeping the relative order of the remaining characters.
The goal is to make the resulting string contain s as a contiguous substring. Among all valid ways to delete characters, we want the minimum number of deletions. If no sequence of deletions can make s appear as a substring, we print -1.
The key detail is that deletions do not reorder characters. Any copy of s that appears after deletions must come from a subsequence of T.
Although there are as many as 100,000 queries, the fixed string T is extremely short. Its length is at most 20, which completely changes the problem. Any algorithm whose complexity is quadratic in |T| is effectively constant time, because 20² = 400.
A common mistake is to think only about whether s is a subsequence of T. That is necessary, but not sufficient for computing the minimum deletions. We also need to minimize the number of characters removed between matched letters.
Consider:
T = abxc
s = abc
The only matching subsequence uses positions 0,1,3. The character x between b and c must be deleted so that abc becomes contiguous. The answer is 1, not 0.
Another easy trap is assuming that once a matching subsequence is found, every character outside it must also be deleted.
Example:
T = zabcz
s = abc
No deletions are needed. The string already contains abc as a substring. Keeping the surrounding z characters is completely allowed.
A third edge case occurs when multiple embeddings of the same query exist.
Example:
T = axbxc
s = abc
Matching positions (0,2,4) requires deleting two internal characters. A careless implementation that stops at the first match may miss a better embedding in other inputs. We must examine all possible starting positions.
Approaches
A brute-force viewpoint is to choose a subsequence of T, build the resulting string after deletions, and check whether it contains s as a substring. Since |T| ≤ 20, there are at most 2^20 ≈ 10^6 subsequences. This is small enough to think about, but doing it separately for every query would be wasteful.
A more direct approach starts from the observation that any valid occurrence of s in the final string comes from a subsequence of T.
Suppose the characters of s are matched to positions
p1 < p2 < ... < pk
inside T.
To make these matched characters form a contiguous substring after deletions, every unmatched character lying between p1 and pk must be removed. Characters before p1 and after pk may stay, because they do not interfere with the substring.
The number of required deletions for this embedding is exactly
(pk - p1 + 1) - k
because the interval length is pk - p1 + 1, while only k characters of that interval belong to the desired string.
So the problem becomes:
Find a subsequence match of s inside T whose span (last - first + 1) is as small as possible.
Since T has length at most 20, we can try every possible position that serves as the first matched character. From that starting point, a simple two-pointer scan finds the earliest possible completion of the subsequence. That gives the smallest ending position for that chosen start, hence the smallest span for that start.
Taking the best span over all starts gives the answer.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force subsequence generation | O(2^ | T | ) per query |
| Try every start, greedy subsequence matching | O( | T | ( |
Algorithm Walkthrough
- Let
n = |T|andm = |s|. - Initialize the answer as infinity.
- For every position
startinTsuch thatT[start] == s[0], attempt to build the entire query string beginning from this position. - Set a pointer in
Tatstartand a pointer insat0. - Move through
Tfrom left to right. Whenever the current character matches the current character ofs, advance the pointer ins. - If all characters of
sare matched, record the position where the last character was matched. - The span length is
last - start + 1
and the required deletions inside that span are
span - m
- Minimize this value over all valid starting positions.
- If no full match was found, output
-1. Otherwise output the minimum deletion count.
Why it works
Fix any valid occurrence of s in the final string. It corresponds to a subsequence match inside T with first matched position first and last matched position last.
Every character inside the interval [first, last] that is not part of the match must be deleted. Characters outside the interval can always be kept. Hence the number of necessary deletions is exactly
(last - first + 1) - |s|
For a fixed starting position first, the greedy left-to-right matching chooses the earliest possible position for every subsequent character, which minimizes last. Since the deletion count depends only on the span, this gives the optimal embedding for that start.
Trying all possible starts covers every feasible embedding. The minimum over them is the global optimum.
Python Solution
import sys
input = sys.stdin.readline
T = input().strip()
n = len(T)
q = int(input())
for _ in range(q):
s = input().strip()
m = len(s)
best = float('inf')
for start in range(n):
if T[start] != s[0]:
continue
j = 0
last = -1
for i in range(start, n):
if j < m and T[i] == s[j]:
j += 1
last = i
if j == m:
break
if j == m:
best = min(best, (last - start + 1) - m)
print(-1 if best == float('inf') else best)
The outer loop processes each query independently.
For each possible starting position, the code performs a standard subsequence match. The variable j stores how many characters of the query have already been matched.
When the entire query is matched, last stores the position of the final matched character. The quantity
(last - start + 1) - m
is exactly the number of unmatched characters inside the span, which are the characters that must be deleted.
A subtle detail is that we do not count characters before start or after last. They can remain in the final string without affecting the existence of the substring.
Another detail is that we must try every valid starting position. The earliest occurrence of the first character is not always part of the optimal solution.
Worked Examples
Example 1
Input:
T = leiulocuuniapnax
s = lulu
| Start | Matched Positions | Last | Span | Deletions |
|---|---|---|---|---|
| 0 | 0, 3, 4, 6 | 6 | 7 | 3 |
| 3 | 3, 4, 6, 7 | 7 | 5 | 1 |
Using the best embedding, the span has length 5 and the query length is 4.
5 - 4 = 1
internal characters must be removed from that span.
The remaining deletions mentioned in the statement are simply one particular construction. The algorithm is computing the minimum necessary deletions inside the matched interval.
Example 2
Input:
T = abxc
s = abc
| Start | Matched Positions | Last | Span | Deletions |
|---|---|---|---|---|
| 0 | 0, 1, 3 | 3 | 4 | 1 |
The character x lies inside the matched interval but is not part of the subsequence.
Removing it yields:
abc
so the answer is 1.
This example illustrates the central invariant: only unmatched characters inside the span are forced to be deleted.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O( | T |
| Space | O(1) | Only a few variables are used |
Since both |T| and |s| are at most 20, the work per query is bounded by a very small constant. This easily fits within the limits even for a large number of queries.
Test Cases
# helper: run solution on input string, return output string
import sys, io
def solve():
input = sys.stdin.readline
T = input().strip()
n = len(T)
q = int(input())
ans = []
for _ in range(q):
s = input().strip()
m = len(s)
best = float('inf')
for start in range(n):
if T[start] != s[0]:
continue
j = 0
last = -1
for i in range(start, n):
if j < m and T[i] == s[j]:
j += 1
last = i
if j == m:
break
if j == m:
best = min(best, (last - start + 1) - m)
ans.append("-1" if best == float('inf') else str(best))
sys.stdout.write("\n".join(ans))
def run(inp: str) -> str:
backup_stdin = sys.stdin
backup_stdout = sys.stdout
sys.stdin = io.StringIO(inp)
sys.stdout = io.StringIO()
solve()
out = sys.stdout.getvalue()
sys.stdin = backup_stdin
sys.stdout = backup_stdout
return out
# custom cases
assert run("abc\n1\nabc\n") == "0", "already a substring"
assert run("abxc\n1\nabc\n") == "1", "delete one internal character"
assert run("abc\n1\nd\n") == "-1", "impossible"
assert run("aaaaa\n2\naaa\naaaaa\n") == "0\n0", "all equal characters"
assert run("axbxcxd\n1\nabcd\n") == "3", "multiple internal deletions"
| Test input | Expected output | What it validates |
|---|---|---|
abc, query abc |
0 |
No deletion needed |
abxc, query abc |
1 |
Internal gap removal |
abc, query d |
-1 |
Impossible match |
aaaaa, queries aaa, aaaaa |
0, 0 |
Repeated characters |
axbxcxd, query abcd |
3 |
Large span with several gaps |
Edge Cases
Consider:
T = zabcz
s = abc
The algorithm starts at the a, matches b and c, obtains span length 3, and returns
3 - 3 = 0
The surrounding z characters are outside the span, so they do not need to be deleted.
Now consider:
T = abxc
s = abc
The match uses positions 0,1,3. The span length is 4, the query length is 3, and the answer is
4 - 3 = 1
Only the x inside the span must be removed.
Finally:
T = abc
s = acd
No starting position can match all characters of the query. The variable storing the best answer is never updated, and the algorithm correctly prints -1.