I had to solve this problem recently. Here's the Python implementation I came up with, which includes a (rough) proof using a mixture of Python and mathematical notation:
def is_prefix_free(values: Iterable[str]) -> bool:
'''determine if a list of strings is prefix-free
(i.e., no element of 'values' is a prefix of any other)
Args:
values: any iterable of strings
Returns:
True iff there is no solution (i, j) to the following
statements:
values[i].startswith(values[j])
i >= 0
j >= 0
i != j
Approach:
We sort 'values' and examine adjacent pairs: once sorted, iff
'svalues[i]' is a prefix of 'svalues[j]' (i != j), then there
must exist an adjecent pair of 'svalues[k], svalues[k+1]' s.t.
'svalues[k]' is a prefix of 'svalues[k+1]'.
Rough proof of validity for this approach:
Let s := svalues (as defined in 'Approach'; for brevity).
The approach assumes that the list is prefix-free IFF the sorted
list is pairwise prefix-free. We can validate this by showing
that each condition implies the other.
It's trivial to demonstrate that presence of an adjecent prefix
pair in the sorted list implies the presence of an
(unrestricted) prefix pair in the unsorted list, as these lists
contain the same elements.
The rest of this proof focuses on proving the converse: that
the presence of an (unrestricted) prefix pair in the unsorted
list implies presence of an adjecent prefix pair in the sorted
list.
Per the problem, we assume:
i != j [0]
Suppose the assumptions given in 'Approach' are untrue,
i.e.,:
s[j].startswith(s[i]), but [1]
∄k | s[k+1].startswith(s[k]) [2]
We show that this leads to contradictions in all cases.
∀ a,b: s[a] < s[b] => a < b [3]
If s[i] = s[j]: contradiction! [4]
i != j (per [0])
i < j [5]
We may assume this is without loss of generality:
we may swap i and j if necessary to ensure this
condition without violating any earlier
assumption.
j != i+1 [6]
Suppose j = i+1
s[i] = s[j] (per [4])
=> s[i] = s[i+1]
=> contradiction! (k=i, per[2])
i < i+1 < j [7]
i < i+1 (trivial algebraic proof)
i < j (per [5])
j != i+1 (per[6])
(no value exists between i, i+1)
s[i] = s[i+1] = s[j] (per [3], [4], [7]) [8]
contradiction! (k=i, per[2])
s[i+1].startswith(s[i])
s[i] = s[i+1] (per [8])
∀ v: v.startswith(v) (trivial proof)
Otherwise: contradiction! [9]
i < i+1 < j, and [10]
s[i] < s[i+1] < s[j] [11]
i < j [12]
i != j (per [0])
Suppose: j < i [13]
s[j] ≤ s[i] (per [3]) [14]
s[i] ≤ s[j] (per [1], [A1]) [15]
s[i] = s[j] (per [14], [15])
=> contradiction! (per [9])
s[i] != s[i+1] [16]
otherwise: contradiction! (with k=i, per[2])
i+1 != j [17]
s[i+1] != s[j] [18]
s[j].startswith(s[i]) (per [1]) [19]
not s[i+1].startswith(s[i]) (per [2]) [20]
=> s[i+1], s[j] are distinct values [21]
(per [19], [20])
=> i+1 != j (per [21], [A2])
i < i+1 [22]
(simple algebraic proof)
i+1 < j [23]
i+1 != j (per [17])
i < j (per [12])
(no value exists between i, i+1)
s[i] < s[i+1] (per [3], [16], [22]) [24]
s[i+1] < s[j] [25]
i+1 < j (per [23])
s[i+1] != s[j] (per [18])
(per [3])
∃ p ≥ 0 s.t. s[i][:p] = s[i+1][:p], and [26]
s[i][p] != s[i+1][p] [27]
otherwise, one of the following would be true:
s[i+1].startswith(s[i]) (contradicts [2])
s[i].startswith(s[i+1])
(contradicts [3] given [A1])
p < len(s[i]) [28]
(otherwise s[i][p] would be undefined in [26])
s[i][p] < s[i+1][p] [29]
s[i][:p] = s[i+1][:p] (per [26])
s[i][p] != s[i+1][p] (per [27])
(per [3])
s[i][:p] = s[j][:p], and [30]
s[i][p] = s[j][p] [31]
p < len(s[i]) (per [28])
s[i] = s[j][:len(s[i])]
s[j].startswith(s[i]) (per [1])
s[i][:p] = s[i+1][:p] = s[j][:p] [32]
s[i][:p] ≤ s[i+1][:p] ≤ s[j][:p] (per [3], [A3])
s[i][:p] = s[j][:p] (per [30])
s[i+1][p] < s[j][p] (per [32], [36], [A4]) [33]
s[i][p] != s[i+1][p] (per [27]) [34]
s[i][p] = s[j][p] (per [31]) [35]
s[i+1][p] != s[j][p] (per [34], [35]) [36]
s[i][p] < s[i+1][p] < s[j][p] [37]
s[i][p] < s[i+1][p] (per [29])
s[i+1][p] < s[j][p] (per [33])
Contradiction!
s[i][p] < s[i+1][p] < s[j][p] (per [37])
=> s[i][p] < s[j][p]
=> s[i][p] != s[j][p] [38]
s[i][p] = s[j][p] (per [31])
([38] conflicts with [31])
Annex (proofs left as an exercise):
s[b].startswith(s[a]) => a ≤ b (assuming [3]) [A1]
s[a] != s[b] => a != b [A2]
∀ q: s[a] ≤ s[b] => s[a][:q] ≤ s[b][:q] [A3]
Given: [A4]
0 ≤ a < b
q ≥ 0
s[a][:q] = s[b][:q]
s[a][q] != s[b][q]
=> s[a][q] < s[b][q] (per above, [3])
'''
values = sorted(values)
for left, right in zip(values[:-1], values[1:]):
if right.startswith(left):
return False
return True
For those who aren't Pythonistas: in Python, sequences and strings are zero-indexed. The slice notation for a string "some_str[:p]
" means "the first p
characters of some_str
", while "some_str[p]
" means "the p
-th character, where p=0
is the first character".
There are probably errors and omissions in the proof, so take it with a grain of salt.
There must be a much more elegant proof, but it's not obvious to me what approach one would take.