Before and After Puzzle
Concatenate phrases sharing a boundary word — group by first word and last word.
Problem#
Given a list of phrases, return all distinct “puzzle” phrases formed by joining two phrases such that the last word of the first equals the first word of the second. The shared word should appear once in the result. Return the answer sorted, deduplicated.
Examples#
phrases = ["mission statement","a quick bite to eat","a chip off the old block","chocolate bar","mission impossible","a man on a mission","block party","eat my words","bar of soap"]→ contains things like"a chip off the old block party","chocolate bar of soap".
Constraints#
1 <= phrases.length <= 100,1 <= phrases[i].length <= 100.
Hints#
Hint
(i, j) with i != j, check if phrases[i]’s last word equals phrases[j]’s first word. Pre-extract first/last to avoid repeated parsing. Approach#
Extract firstWord and lastWord for each phrase. For every ordered pair (i, j) with i != j, if lastWord[i] == firstWord[j], concat with the boundary word appearing once. Use a set for dedupe and final sort.
Solution#
class Solution { pair<string,string> firstLast(const string& p) { int sp1 = p.find(' '); int sp2 = p.rfind(' '); string first = sp1 == (int)string::npos ? p : p.substr(0, sp1); string last = sp2 == (int)string::npos ? p : p.substr(sp2 + 1); return {first, last}; }public: vector<string> beforeAndAfterPuzzles(vector<string>& phrases) { int n = phrases.size(); vector<pair<string,string>> fl(n); for (int i = 0; i < n; ++i) fl[i] = firstLast(phrases[i]); set<string> uniq; for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (i == j) continue; if (fl[i].second == fl[j].first) { // concat dropping the duplicate boundary word int sp = phrases[j].find(' '); string tail = sp == (int)string::npos ? "" : phrases[j].substr(sp); uniq.insert(phrases[i] + tail); } } } return {uniq.begin(), uniq.end()}; }};func beforeAndAfterPuzzles(phrases []string) []string { firstLast := func(p string) (string, string) { sp1 := strings.Index(p, " ") sp2 := strings.LastIndex(p, " ") first := p last := p if sp1 != -1 { first = p[:sp1] } if sp2 != -1 { last = p[sp2+1:] } return first, last } n := len(phrases) fl := make([][2]string, n) for i := 0; i < n; i++ { f, l := firstLast(phrases[i]) fl[i] = [2]string{f, l} } uniq := make(map[string]bool) for i := 0; i < n; i++ { for j := 0; j < n; j++ { if i == j { continue } if fl[i][1] == fl[j][0] { sp := strings.Index(phrases[j], " ") tail := "" if sp != -1 { tail = phrases[j][sp:] } uniq[phrases[i]+tail] = true } } } out := make([]string, 0, len(uniq)) for s := range uniq { out = append(out, s) } sort.Strings(out) return out}from typing import List, Tuple
class Solution: def beforeAndAfterPuzzles(self, phrases: List[str]) -> List[str]: def first_last(p: str) -> Tuple[str, str]: sp1 = p.find(' ') sp2 = p.rfind(' ') first = p if sp1 == -1 else p[:sp1] last = p if sp2 == -1 else p[sp2 + 1:] return first, last
n = len(phrases) fl = [first_last(p) for p in phrases] uniq: set[str] = set() for i in range(n): for j in range(n): if i == j: continue if fl[i][1] == fl[j][0]: sp = phrases[j].find(' ') tail = '' if sp == -1 else phrases[j][sp:] uniq.add(phrases[i] + tail) return sorted(uniq)var beforeAndAfterPuzzles = function(phrases) { const firstLast = (p) => { const sp1 = p.indexOf(' '); const sp2 = p.lastIndexOf(' '); const first = sp1 === -1 ? p : p.slice(0, sp1); const last = sp2 === -1 ? p : p.slice(sp2 + 1); return [first, last]; }; const n = phrases.length; const fl = phrases.map(firstLast); const uniq = new Set(); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j) continue; if (fl[i][1] === fl[j][0]) { const sp = phrases[j].indexOf(' '); const tail = sp === -1 ? '' : phrases[j].slice(sp); uniq.add(phrases[i] + tail); } } } return [...uniq].sort();};class Solution { private String[] firstLast(String p) { int sp1 = p.indexOf(' '); int sp2 = p.lastIndexOf(' '); String first = sp1 == -1 ? p : p.substring(0, sp1); String last = sp2 == -1 ? p : p.substring(sp2 + 1); return new String[]{first, last}; }
public List<String> beforeAndAfterPuzzles(String[] phrases) { int n = phrases.length; String[][] fl = new String[n][]; for (int i = 0; i < n; i++) fl[i] = firstLast(phrases[i]); TreeSet<String> uniq = new TreeSet<>(); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (i == j) continue; if (fl[i][1].equals(fl[j][0])) { int sp = phrases[j].indexOf(' '); String tail = sp == -1 ? "" : phrases[j].substring(sp); uniq.add(phrases[i] + tail); } } } return new ArrayList<>(uniq); }}function beforeAndAfterPuzzles(phrases: string[]): string[] { const firstLast = (p: string): [string, string] => { const sp1 = p.indexOf(' '); const sp2 = p.lastIndexOf(' '); const first = sp1 === -1 ? p : p.slice(0, sp1); const last = sp2 === -1 ? p : p.slice(sp2 + 1); return [first, last]; }; const n = phrases.length; const fl = phrases.map(firstLast); const uniq = new Set<string>(); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j) continue; if (fl[i][1] === fl[j][0]) { const sp = phrases[j].indexOf(' '); const tail = sp === -1 ? '' : phrases[j].slice(sp); uniq.add(phrases[i] + tail); } } } return [...uniq].sort();}Editorial#
Pre-extracting first/last avoids re-parsing inside the O(N^2) loop. The dedupe set simultaneously sorts the output — important because two different pairs can produce the same merged string. The boundary word appears once because we keep phrases[i] whole and strip the first word from phrases[j].
Complexity#
- Time: O(N^2 * L).
- Space: O(N * L) for the result.
Concept revision#
Related#