Shortest string containing the most occurrence of strings in a set

Viewed 203

Define the degree of a string M be the number of times it appears in another string S. For example M = "aba" and S="ababa", the degree of M is 2. Given a set of strings and an integer N, find the string of the minimum length so that the sum of degrees of all strings in the set is at least N.

For example a set {"ab", "bd", "abd" "babd", "abc"}, N = 4, the answer will be "babd". It contains "ab", "abd", "babd" and "bd" one time.

N <= 100, M <= 100, length of every string in the set <= 100. Strings in the set only consist of uppercase and lowercase letters.

How to solve this problem? This looks similar to the shortest superstring problems which has a dynamic programming solution that has exponential complexity. However, the constraint in this problem is much larger and the same idea also won't work here. Is there some string data structure that can be applied here?

2 Answers

I have a polynomial time algorithm, which I'm too lazy to code. But I'll describe it for you.

First, make each string in the set plus the empty string be the nodes of a graph. The empty string is connected to each other string, and vice versa. If the end of one string overlaps with the start of another, they also connect. If two can overlap by different amounts, they get multiple edges. (So it is not exactly a graph...)

Each edge gets a cost and a value. The cost is how many characters you have to extend the string you are building by to move from the old end to the new end. (In other words the length of the second string minus the length of the overlap.) to having this one. The value is how many new strings you completed that cross the barrier between the former and the latter string.

Your example was {"ab", "bd", "abd" "babd", "abc"}. Here are the (cost, value) pairs for each transition.

 from  ->   to  : (value, cost)
 ""    ->   "ab": (    1,    2)
 ""    ->   "bd": (    1,    2)
 ""    ->  "abd": (    3,    3) # we added "ab", "bd" and "abd"
 ""    -> "babd": (    4,    4) # we get "ab", "bd", "abd" and "babd"
 ""    ->  "abc": (    2,    3) # we get "ab" and "abc"
 "ab"  ->     "": (    0,    0)
 "ab"  ->   "bd": (    2,    1) # we added "abd" and "bd" for 1 character
 "ab"  ->  "abd": (    2,    1) # ditto
 "ab"  ->  "abc": (    1,    1) # we only added "abc"
 "bd"  ->     "": (    0,    0) # only empty, nothing else starts "bd"
"abd"  ->     "": (    0,    0) 

"babd" -> "": ( 0, 0) "babd" -> "abd": ( 0, 0) # overlapped, but added nothing. "abc" -> "": ( 0, 0)

OK, all of that is setup. Why did we want this graph?

Well note that if we start at "" with a cost of 0 and a value of 0, then take a path through the graph, that constructs a string. It correctly states the cost, and provides a lower bound on the value. The value can be higher. For example if your set were {"ab", "bc", "cd", "abcd"} then the path "" -> "ab" -> "bc" -> "cd" would lead to the string "abcd" with a cost of 4 and a predicted value of 3. But that value estimate missed the fact that we matched "abcd".

However for any given string made up only of substrings from the set, there is a path through the graph that has the correct cost and the correct value. (At each choice you want to pick the earliest starting matching string that you have not yet counted, and of those pick the longest of them. Then you never miss any matches.)

So we've turned our problem from constructing strings to constructing paths through a graph. What we want to do is build up the following data structure:

for each (value, node) combination:
    (best cost, previous node, previous value)

Filling in that data structure is a dynamic programming problem. Once filled in we can just trace back through it to find what path in the graph got us to that value with that cost. Given that path, we can figure out the string that did it.

How fast is it? If our set has K strings then we only need to fill in K * N values, each of which we can give a maximum of K candidates for new values. Which makes the path finding a O(K^2 * N) problem.

So here is my approach. At first iteration we construct a pool out of the initial strings. After that:

  1. We select out of the pool a string having minimal length and sum of degrees=N. If we found such a string we just return it.
  2. We filter out of the pool all strings with degree less than maximal. We work only with the best possible string combinations.
  3. We construct all variants out of the current pool and the initial strings. Here we need to take into consideration that strings can overlap. Say a string "aba" and "ab"(from initial strings) could produce: ababa, abab, abaab (we do not include "aba" because we already had it in our pool and we need to move further).
  4. We filter out duplicates and this is our next pool.
  5. Repeat everything from the point 1.

The FindTarget() method accepts the target sum as a parameter. FindTarget(4) will solve the sample task.

public class Solution
{
    /// <summary>
    /// The initial strings.
    /// </summary>
    string[] stringsSet;
    Tuple<string, string>[][] splits;
    public Solution(string[] strings)
    {
        stringsSet = strings;
        splits = stringsSet.Select(s => ProduceItemSplits(s)).ToArray();
    }

    /// <summary>
    /// Find the optimal string.
    /// </summary>
    /// <param name="N">Target degree.</param>
    /// <returns></returns>
    public string FindTarget(int N)
    {
        var pool = stringsSet;
        while (true)
        {
            var poolWithDegree = pool.Select(s => new { str = s, degree = GetN(s) })
                .ToArray();
            var maxDegree = poolWithDegree.Max(m => m.degree);
            var optimalString = poolWithDegree
                .Where(w => w.degree >= N)
                .OrderBy(od => od.str.Length)
                .FirstOrDefault();
            if (optimalString != null) return optimalString.str; // We found it
            var nextPool = poolWithDegree.Where(w => w.degree == maxDegree)
                .SelectMany(sm => ExpandString(sm.str))
                .Distinct()
                .ToArray();
            pool = nextPool;
        }
    }
    /// <summary>
    /// Get degree.
    /// </summary>
    /// <param name="candidate"></param>
    /// <returns></returns>
    public int GetN(string candidate)
    {

        var N = stringsSet.Select(s =>
        {
            var c = Regex.Matches(candidate, s).Count();
            return c;
        }).Sum();
        return N;
    }

    public Tuple<string, string>[] ProduceItemSplits(string item)
    {
        var substings = Enumerable.Range(0, item.Length + 1)
            .Select((i) => new Tuple<string, string>(item.Substring(0, i), item.Substring(i, item.Length - i))).ToArray();
        return substings;
    }

    private IEnumerable<string> ExpandStringWithOneItem(string str, int index)
    {
        var item = stringsSet[index];
        var itemSplits = splits[index];
        var startAttachments = itemSplits.Where(w => str.StartsWith(w.Item2) && w.Item1.Length > 0)
            .Select(s => s.Item1 + str);
        var endAttachments = itemSplits.Where(w => str.EndsWith(w.Item1) && w.Item2.Length > 0)
            .Select(s => str + s.Item2);
        return startAttachments.Union(endAttachments);
    }

    public IEnumerable<string> ExpandString(string str)
    {
        var r = Enumerable.Range(0, splits.Length - 1)
                .Select(s => ExpandStringWithOneItem(str, s))
                .SelectMany(s => s);
        return r;
    }
}

static void Main(string[] args)
{
    var solution = new Solution(new string[] { "ab", "bd", "abd", "babd", "abc" });
    var s = solution.FindTarget(150);
    Console.WriteLine(s);
}
Related