Finding acronyms with >=1 letters at the start of each consecutive word Problem
I'm seeking a clean and efficient way to determine whether one string is an initialism/acronym of another.
Example: IBM =&gt; International Business Machines
However, a key condition for my use case is that an initialism might be constructed using more than one letter from each consecutive word.
Example: ANEXAIST = An Example Is This
Working solution
My solution works, though only returns a single answer (even when multiple might exist - this is fine for my use case). I wonder if there's a cleaner and more efficient implementation?
s1 = 'anexithi'
s2 = 'an example is this'

def recursive_split(s1, s2, mapping=[]):
    for s1_sub in [s1[:i] for i in range(4)]:
        if s1_sub!='' and s1_sub == s2[:len(s1_sub)]:
            mapping_ = mapping + [(s1_sub, s2.split()[0])]
            if s1_sub == s1:
                return mapping_
            rec = recursive_split(s1[len(s1_sub):], ' '.join(s2.split()[1:]), mapping_)
            if rec is not None:
                return rec

recursive_split(s1, s2)

Output
[('an', 'an'), ('ex', 'example'), ('i', 'is'), ('thi', 'this')]
Correctness

Your code allows partial acronyms which doesn't seem right. &quot;I'm seeking a clean and efficient way to determine whether one string is an initialism/acronym of another.&quot;
recursive_split('aei', 'an example is this')
# [('a', 'an'), ('e', 'example'), ('i', 'is')]



Performance 1

A list comprehension being fed into a for loop is not idiomatic. It would be better if you move the expression into the for's expression.
for i in range(4):
    s1_sub = s1[:i]


It should be clear if len(s1_sub) != i then s1_sub == s2[:len(s1_sub)] is going to call the body multiple times with the same values. To address this wastage we can bound the for loop.
for i in range(min(4, 1 + len(s2))):


Rather than s1_sub!='' we can start the range at 1.
for i in range(1, min(4, 1 + len(s2))):


If len(s1) &lt; len(s2) then we are wasting iterations of the for loop when we could just adjust the range.
for i in range(1, min(4, 1 + len(s1), 1 + len(s2))):


If s1[:i] != s2[:i] then i+1 will always be false too. You should instead break if this is ever false.
for i in range(1, min(4, 1 + len(s1), 1 + len(s2))):
    s1_sub = s1[:i]
    if s1_sub != s2[:i]:
        break
    # do stuff


We don't need to slice s1 and s2 to compare the values. We can just index. This makes the range a little simpler.
for i in range(min(3, len(s1), len(s2))):
    if s1[i] != s2[i]:
        break
    s1_sub = s1[:1 + i]



def recursive_split(s1, s2, mapping=[]):
    for i in range(min(3, len(s1), len(s2))):
        if s1[i] != s2[i]:
            break
        s1_sub = s1[:1 + i]
        mapping_ = mapping + [(s1_sub, s2.split()[0])]
        if s1_sub == s1:
            return mapping_
        rec = inner(s1[len(s1_sub):], ' '.join(s2.split()[1:]), mapping_)
        if rec is not None:
            return rec

Clean code
Many of the changes above have increased the readability of your code.

The names s1 and s2 aren't very descriptive.

Performance 2
These are some more advanced performance changes. Depending on the person the decreased readability may not outweigh the performance gains.

Rather than using mapping=[] you can use a closure and pass a fresh mapping.
def recursive_split(s1, s2):
    def inner(s1, s2, mapping):
        for i in range(min(3, len(s1), len(s2))):
            if s1[i] != s2[i]:
                break
            s1_sub = s1[:1 + i]
            mapping_ = mapping + [(s1_sub, s2.split()[0])]
            if s1_sub == s1:
                return mapping_
            rec = inner(s1[len(s1_sub):], ' '.join(s2.split()[1:]), mapping_)
            if rec is not None:
                return rec
    return inner(s1, s2, [])


Constantly calling s2.split() and ' '.join(s2.split()[1:]) are very wasteful. Instead you could call str.split once and just work with the split list.
def recursive_split(s1, s2):
    def inner(s1, s2_index, mapping):
        try:
            s2 = s2s[s2_index]
        except IndexError:
            return None
        for i in range(1, min(4, 1 + len(s1), 1 + len(s2))):
            s1_sub = s1[:i]
            if s1_sub != s2[:i]:
                break
            mapping_ = mapping + [(s1_sub, s2s[s2_index])]
            if s1_sub == s1:
                return mapping_
            rec = inner(s1[len(s1_sub):], s2_index + 1, mapping_)
            if rec is not None:
                return rec
    s2s = s2.split()
    return inner(s1, 0, [])


We don't need to add s2s[s2_index] to mapping_ because s2_index only ever increases by 1 each time so we can do that outside of inner.
mapping_ = mapping + [s1_sub]

ret = inner(s1, 0, [])
if ret is None:
    return None
return list(zip(ret, s2s))


With mapping + [s1_sub] will make near duplicate copy each time you recurse. We can improve the performance by mutating the mapping.
To do this we just append to the stack where you did mapping + [s1_sub] and then pop after any recursion.
def recursive_split(s1, s2):
    def inner(s1, s2_index, mapping):
        try:
            s2 = s2s[s2_index]
        except IndexError:
            return None
        for i in range(min(3, len(s1), len(s2))):
            if s1[i] != s2[i]:
                break
            s1_sub = s1[:1 + i]
            mapping.append(s1_sub)
            if s1_sub == s1:
                return mapping
            else:
                rec = inner(s1[1 + i:], s2_index + 1, mapping)
                if rec is not None:
                    return rec
            mapping.pop()
    s2s = s2.split()
    ret = inner(s1, 0, [])
    if ret is None:
        return None
    return list(zip(ret, s2s))


We can do something similar to s1. However instead of appending and popping we can pop in the for loop and then extend after the for loop. As such we just need to reverse s1.
def recursive_split(s1, s2):
    def inner(s1, s2_index, mapping):
        try:
            s2 = s2s[s2_index]
        except IndexError:
            return None
        s1_sub = []
        for i in range(min(3, len(s1), len(s2))):
            s1_sub.append(s1.pop())
            if s1_sub[-1] != s2[i]:
                break
            mapping.append(s1_sub)
            if not s1:
                return mapping
            else:
                rec = inner(s1, s2_index + 1, mapping)
                if rec is not None:
                    return rec
            mapping.pop()
        s1.extend(s1_sub[::-1])
    s2s = s2.split()
    ret = inner(list(reversed(s1)), 0, [])
    if ret is None:
        return None
    return [
        (''.join(s1_sub), s2)
        for s1_sub, s2 in zip(ret, s2s)
    ]


We can use generators and a couple of tricks to reduce the line count.
def recursive_split(s1, s2):
    def inner(s1, s2_index, mapping):
        s2 = s2s[s2_index]
        s1_sub = []
        for i in range(min(3, len(s1), len(s2))):
            s1_sub.append(s1.pop())
            if s1_sub[-1] != s2[i]:
                break
            mapping.append(s1_sub)
            if not s1:
                yield mapping
            elif s2_index + 1 &lt; len(s2s):
                yield from inner(s1, s2_index + 1, mapping)
            mapping.pop()
        s1.extend(s1_sub[::-1])
    s2s = s2.split() or ['']
    rets = (
        [(''.join(s1_sub), s2) for s1_sub, s2 in zip(ret, s2s)]
        for ret in inner(list(reversed(s1)), 0, [])
    )
    return next(rets, None)


Given your examples it would be better to use a greedy search. For each letter you recurse straight away however if you went from the point we break in the for loop then inner would never return None on your example data.
def recursive_split(s1, s2):
    def inner(s1, s2_index, mapping):
        s2 = s2s[s2_index]
        s1_sub = []
        for i in range(min(3, len(s1), len(s2))):
            s1_sub.append(s1.pop())
            if s1_sub[-1] != s2[i]:
                break
        while s1_sub:
            mapping.append(s1_sub)
            if not s1:
                yield mapping
            elif s2_index + 1 &lt; len(s2s):
                yield from inner(s1, s2_index + 1, mapping)
            mapping.pop()
            s1.append(s1_sub.pop())
        # nonlocal counter
        # counter += 1
    # counter = 0
    s2s = s2.split() or ['']
    rets = (
        [(''.join(s1_sub), s2) for s1_sub, s2 in zip(ret, s2s)]
        for ret in inner(list(reversed(s1)), 0, [])
    )
    ret = next(rets, None)
    # print(counter)
    return ret

This prints 0, if you uncomment the code, where as the previous code printed 2 because it recursed on a from an and e from ex.

We can remove the recursion and so we can remove the stack frames.
In the previous example it should be clear that mapping is acting like a stack and we can actually get s2_index from it.
Additionally we can see that the code is quite reliant now on the fact that we're mutating s1.
So we can think of the code another way.

Build the rest of the stack based on the state of s1.

At first this is just the same as the for i in range(...) loop.
If there are no matches, s1_sub is empty, then we have to start the popping process.
Otherwise we add the value to the stack.
Then if the length of the stack is the same as s2s then we have a value to return.


Pop from the stack.

If we make a value in the stack empty, then we need to pop again from the stack, otherwise we'll have an infinite loop. As the builder will just build the exact same way.
If we pop from the entire stack, we're done.



def recursive_split(s1, s2):
    s1_ = list(reversed(s1))
    s2s = s2.split()
    stack = []
    while True:
        while len(stack) &lt; len(s2s):
            s1_sub = []
            for value in s2s[len(stack)]:
                if not s1_ or value != s1_[-1]:
                    break
                s1_sub.append(s1_.pop())
            if not s1_sub:
                break
            else:
                stack.append(s1_sub)
                if len(stack) == len(s2s):
                    return [(''.join(values), s2) for values, s2 in zip(stack, s2s)]
        for _ in range(len(stack)):
            s1_.append(stack[-1].pop())
            if stack[-1]:
                break
            stack.pop()
        else:
            return