#string #algorithm #data-structures #dynamic-programming
#строка #алгоритм #структуры данных #динамическое программирование
Вопрос:
Определите степень строки M как количество раз, когда она появляется в другой строке S. Например, M = «aba» и S = «abeba», степень M равна 2. Учитывая набор строк и целое число N, найдите строку минимальной длины, чтобы сумма степеней всех строк в наборе была не менее N.
Например, набор {«ab», «bd», «abd», «babd», «abc»}, N = 4, ответом будет «babd». Она содержит «ab», «abd», «babd» и «bd» один раз.
N <= 100, M <= 100, длина каждой строки в наборе <= 100. Строки в наборе состоят только из прописных и строчных букв.
Как решить эту проблему? Это похоже на задачи о кратчайших суперструнах, которые имеют решение для динамического программирования с экспоненциальной сложностью. Однако ограничение в этой задаче намного больше, и та же идея здесь также не сработает. Существует ли какая-либо строковая структура данных, которая может быть применена здесь?
Ответ №1:
У меня есть алгоритм полиномиального времени, который я слишком ленив, чтобы кодировать. Но я опишу это для вас.
Сначала сделайте каждую строку в наборе плюс пустую строку узлами графика. Пустые строки соединяются друг с другом строками, и наоборот. Если конец одной строки перекрывается с началом другой, они также соединяются. Если две строки могут перекрываться на разную величину, они получают несколько ребер. (Так что это не совсем график …)
Каждое ребро получает стоимость и значение. Стоимость — это то, на сколько символов вам нужно расширить строку, на которую вы строите, чтобы перейти от старого конца к новому концу. (Другими словами, длина второй строки минус длина перекрытия.) к наличию этой. Значение — это количество новых строк, которые вы завершили, которые пересекают барьер между первой и последней строками.
Ваш пример был {«ab», «bd», «abd», «babd», «abc»}. Вот (cost, value)
пары для каждого перехода.
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) # перекрывается, но ничего не добавлено.
«abc» -> «»: ( 0, 0)
Хорошо, все это настройка. Зачем нам нужен этот график?
Хорошо, обратите внимание, что если мы начнем с «» со стоимостью 0 и значением 0, затем пройдем путь через график, который создает строку. В нем правильно указана стоимость и указана нижняя граница значения. Значение может быть выше. Например, если бы ваш набор был {«ab», «bc», «cd», «abcd»}, то путь «» -> «ab» -> «bc» -> «cd» привел бы к строке «abcd» со стоимостью 4 и прогнозируемым значением 3. Но в этой оценке значения не учитывался тот факт, что мы соответствовали «abcd».
Однако для любой заданной строки, состоящей только из подстрок из набора, существует путь через график, который имеет правильную стоимость и правильное значение. (При каждом выборе вы хотите выбрать самую раннюю начальную строку соответствия, которую вы еще не подсчитали, и из них выбрать самую длинную из них. Тогда вы никогда не пропустите ни одного совпадения.)
Итак, мы перевели нашу задачу с построения строк на построение путей через граф. Что мы хотим сделать, так это создать следующую структуру данных:
for each (value, node) combination:
(best cost, previous node, previous value)
Заполнение этой структуры данных является проблемой динамического программирования. После заполнения мы можем просто проследить по нему, чтобы найти, какой путь на графике привел нас к этому значению с такой стоимостью. Учитывая этот путь, мы можем вычислить строку, которая это сделала.
Насколько это быстро? Если в нашем наборе есть K
строки, то нам нужно только заполнить K * N
значения, каждому из которых мы можем предоставить максимум K
кандидатов на новые значения. Что делает поиск пути O(K^2 * N)
проблемой.
Комментарии:
1. Что означает «самая ранняя начальная строка соответствия, которую вы еще не подсчитали»?
2. @LearningMathematics У нас есть набор строк типа «abc», «bcd» и «cde». Мы смотрим на объединенную строку типа «abcdef». В «abc» мы еще не посчитали «bcd» или «cde», потому что они заканчиваются тем, что мы рассмотрели. Но из них «bcd» начинается раньше в «abc», так что это следующая, которую мы хотим. Потому что мы можем получить это сейчас и посчитать «cde» позже, в то время как если мы перейдем к «cde», мы пропустим совпадение по «bcd».
3. Почему вы хотите выбрать самую длинную из них? Означает ли это, что при «» вы всегда переходите к самой длинной строке? Или это означает что-то еще?
4. @LearningMathematics Потому что, если вы выберете более короткую строку в этот момент, вы никогда не будете считать более длинную. Например, если бы наш набор был {«abc», «bcd», «bcde» и «cde»}. Если наш путь по графику «abc» -> «bcd» -> «cde», то мы не учитываем «bcde».
5. Извините за поздний комментарий, но у меня есть несколько вопросов: Вы упомянули «(При каждом выборе вы хотите выбрать самую раннюю начальную строку соответствия, которую вы еще не подсчитали, и из них выбрать самую длинную из них. Тогда вы никогда не пропустите ни одного совпадения.)». Это не то, что я должен реализовать, а просто объяснение предыдущего утверждения?
Ответ №2:
Итак, вот мой подход. На первой итерации мы создаем пул из начальных строк. После этого:
- Мы выбираем из пула строку, имеющую минимальную длину и сумму степеней=N. Если мы нашли такую строку, мы просто возвращаем ее.
- Мы отфильтровываем из пула все строки со степенью меньше максимальной. Мы работаем только с наилучшими возможными комбинациями строк.
- Мы создаем все варианты из текущего пула и начальных строк. Здесь нам нужно принять во внимание, что строки могут перекрываться. Допустим, строка «aba» и «ab» (из начальных строк) может выдавать: ababa, abab, abaab (мы не включаем «aba», потому что она уже была у нас в пуле, и нам нужно двигаться дальше).
- Мы отфильтровываем дубликаты, и это наш следующий пул.
- Повторите все с точки 1.
Метод FindTarget() принимает целевую сумму в качестве параметра. FindTarget(4) решит примерную задачу.
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) amp;amp; w.Item1.Length > 0)
.Select(s => s.Item1 str);
var endAttachments = itemSplits.Where(w => str.EndsWith(w.Item1) amp;amp; 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);
}