Skip to main content
Back to problems
#3485
Hard Algorithms

Longest common prefix of k strings after removal

Array String Trie
23.7% acceptance
Mar 10, 2026
63
5
You are given an array of strings words and an integer k. For each index i in the range [0, words.length - 1], find the length of the longest common prefix among any k strings (selected at distinct indices) from the remaining array after removing the ith element. Return an array answer, where answer[i] is the answer for ith element. If removing the ith element leaves the array with fewer than k strings, answer[i] is 0.

Solution

Rust
Time O(n²)
Space O(n)
LeetCode
solution.rs
impl Solution {
  pub fn longest_common_prefix(words: Vec<String>, k: i32) -> Vec<i32> {
    let n = words.len();
    let k = k as usize;
    let mut ans = vec![0i32; n];

    if n <= k { return ans; }

    let total_len: usize = words.iter().map(|w| w.len()).sum();
    let max_depth = words.iter().map(|w| w.len()).max().unwrap_or(0);

    // Build a Trie. Each node stores a count of words passing through it.
    // node 0 = root (virtual, not counted).
    let cap = total_len + 1;
    let mut children = vec![[0usize; 26]; cap];
    let mut count = vec![0usize; cap];
    let mut next_id = 1usize;

    // word_nodes[i][d] = trie node ID at depth d+1 for words[i].
    // Avoids re-traversing the trie and eliminates O(L) hash cost per level.
    let mut word_nodes: Vec<Vec<usize>> = Vec::with_capacity(n);

    for w in &words {
      let mut cur = 0;
      let mut path = Vec::with_capacity(w.len());
      for b in w.bytes() {
        let c = (b - b'a') as usize;
        if children[cur][c] == 0 {
          children[cur][c] = next_id;
          next_id += 1;
        }
        cur = children[cur][c];
        count[cur] += 1;
        path.push(cur);
      }
      word_nodes.push(path);
    }

    // For each depth, track the top-2 prefix counts and the node ID of the leader.
    // Used to answer: "is there another prefix of this length with count >= k?"
    let mut top1_cnt = vec![0usize; max_depth + 1];
    let mut top1_id  = vec![0usize; max_depth + 1];
    let mut top2_cnt = vec![0usize; max_depth + 1];

    for path in &word_nodes {
      for (d, &node) in path.iter().enumerate() {
        let l = d + 1;
        let c = count[node];
        if c > top1_cnt[l] {
          top2_cnt[l] = top1_cnt[l];
          top1_cnt[l] = c;
          top1_id[l]  = node;
        } else if node != top1_id[l] && c > top2_cnt[l] {
          top2_cnt[l] = c;
        }
      }
    }

    // suffix_max_l[l] = max depth l' in [l, max_depth] where top1_cnt[l'] >= k.
    // For "Part 1": prefixes longer than wi cannot involve word i, so global counts
    // are unaffected by its removal — suffix_max_l is valid as-is.
    let mut suffix_max_l = vec![0usize; max_depth + 2];
    let mut cur_max = 0usize;
    for l in (1..=max_depth).rev() {
      if top1_cnt[l] >= k { cur_max = cur_max.max(l); }
      suffix_max_l[l] = cur_max;
    }

    for i in 0..n {
      let wi_len = word_nodes[i].len();

      // Part 1: best length from prefixes strictly longer than wi.
      // word i has no such prefix, so removing it changes nothing at those depths.
      let best_longer = if wi_len + 1 <= max_depth { suffix_max_l[wi_len + 1] } else { 0 };

      // Part 2: scan depths 1..=wi_len (backwards for early exit).
      // At each depth we remove word i's contribution and check feasibility.
      let mut best_shorter = 0usize;
      for (d, &node) in word_nodes[i].iter().enumerate().rev() {
        let l = d + 1;
        let my_cnt = count[node];

        // Condition A: this prefix still has >= k words after removing word i.
        if my_cnt >= k + 1 {
          best_shorter = l;
          break;
        }

        // Condition B: some OTHER trie node at this depth has count >= k.
        let other_best = if top1_id[l] == node { top2_cnt[l] } else { top1_cnt[l] };
        if other_best >= k {
          best_shorter = l;
          break;
        }
      }

      ans[i] = best_longer.max(best_shorter) as i32;
    }

    ans
  }
}