Самая длинная подпоследовательность со всеми вхождениями символа на 1 месте

в последовательности S из n символов; каждый символ может возникать много раз в последовательности. Вы хотите найти самую длинную подпоследовательность S, где все вхождения одного и того же символа находятся вместе в одном месте;

для ex. если S = aaaccaaaccbccbbbbab, то самая длинная такая подпоследовательность (ответ) - aaaaaaccccbbbb i.e= aaa_ _ aaacc _ ccbbb_b.

другими словами, любой символ алфавита, который появляется в S, может отображаться только в одном непрерывный блок в подпоследовательности. Если возможно, дайте полиномиальное время алгоритм определения решения.

3 ответов


конструкция

ниже я даю реализацию c++ алгоритма динамического программирования, который решает эту проблему. Верхняя граница времени выполнения(которая, вероятно, не является жесткой) задается O(g*(n^2 + log (g))), где n-длина строки, а g-число различных подпоследовательностей на входе. Я не знаю хорошего способа охарактеризовать это число, но это может быть так же плохо, как O(2^n) для строки, состоящей из n различных символов, что делает этот алгоритм экспоненциального времени в худшем случае. Он также использует пространство O(ng) для хранения таблицы memoisation DP. (Подпоследовательность, в отличие от подстроки, может состоять из несмежного символа из исходной строки.) На практике алгоритм будет быстрым, когда количество различных символов мало.

две ключевые идеи, использованные при разработке этого алгоритма, были:

  • каждая подпоследовательность строки length-n является либо (a) пустой строкой, либо (b) подпоследовательностью, первый элемент находится в некоторой позиции 1
  • если мы добавляем символы (или, более конкретно, позиции символов) по одному в подпоследовательность, то для того, чтобы построить все и только подпоследовательности, удовлетворяющие критериям достоверности,всякий раз, когда мы добавляем символ c, если предыдущий добавленный символ, p, отличался от c, то больше невозможно добавить любые символы p позже on.

есть по крайней мере 2 способа управления второй точкой выше. Один из способов-поддерживать набор запрещенных символов (например, с помощью 256-битного массива), который мы добавляем по мере добавления символов в текущую подпоследовательность. Каждый раз, когда мы хотим добавить символ к текущей подпоследовательности, мы сначала проверяем, разрешено ли это.

другой способ-понять, что всякий раз, когда мы должны запретить символ появляться позже в подпоследовательности, мы можем достигните этого, просто удалив все копии символа из оставшегося суффикса и используя эту (возможно, более короткую) строку в качестве подзадачи для рекурсивного решения. Эта стратегия имеет то преимущество, что делает более вероятным, что функция решателя будет вызываться несколько раз с одним и тем же строковым аргументом, что означает, что больше вычислений можно избежать, когда рекурсия преобразуется в DP. Вот как работает приведенный ниже код.

рекурсивная функция должна принимать 2 параметра: строка для работы и символ, недавно добавленный к подпоследовательности, к которой будет добавлен вывод функции. Второй параметр должен иметь специальное значение, указывающее, что символы еще не добавлены (что происходит в рекурсивном случае верхнего уровня). Один из способов сделать это-выбрать символ, который не отображается во входной строке, но это вводит требование не использовать этот символ. Очевидное решение-пройти 3-й параметр, логическое значение, указывающее, были ли добавлены какие-либо символы. Но немного удобнее использовать только 2 параметра: логическое значение, указывающее, были ли добавлены какие-либо символы, и строка. Если логическое значение равно false, то строка-это просто строка, которая будет работать. Если это правда, то первый символ строки принимается за последний добавленный символ, а остальная часть-строка для работы. Принятие этого подхода означает, что функция принимает только 2 параметра, что упрощает запоминание.

как я сказал в верхней части, этот алгоритм экспоненциальный-время в худшем случае. Я не могу придумать способ, чтобы полностью избежать этого, но некоторые оптимизации могут помочь в определенных случаях. Один из них, который я реализовал, - всегда добавлять максимальные непрерывные блоки одного и того же символа за один шаг, так как если вы добавляете хотя бы один символ из такого блока, никогда не может быть оптимальным добавить меньше, чем весь блок. Другой ветвей и границ-оптимизация стиля возможна, например, отслеживание лучшей в глобальном масштабе строки до сих пор и сокращение рекурсии, когда мы можем быть уверены, что текущая подзадача не может произвести более длинную-например, когда количество символов, добавленных к подпоследовательности до сих пор, плюс общее количество оставшихся символов, меньше длины лучшей подпоследовательности до сих пор.

код

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <functional>
#include <map>

using namespace std;

class RunFinder {
    string s;
    map<string, string> memo[2];    // DP matrix

    // If skip == false, compute the longest valid subsequence of t.
    // Otherwise, compute the longest valid subsequence of the string
    // consisting of t without its first character, taking that first character
    // to be the last character of a preceding subsequence that we will be
    // adding to.
    string calc(string const& t, bool skip) {
        map<string, string>::iterator m(memo[skip].find(t));

        // Only calculate if we haven't already solved this case.
        if (m == memo[skip].end()) {
            // Try the empty subsequence.  This is always valid.
            string best;

            // Try starting a subsequence whose leftmost position is one of
            // the remaining characters.  Instead of trying each character
            // position separately, consider only contiguous blocks of identical
            // characters, since if we choose one character from this block there
            // is never any harm in choosing all of them.
            for (string::const_iterator i = t.begin() + skip; i != t.end();) {
            if (t.end() - i < best.size()) {
                // We can't possibly find a longer string now.
                break;
            }

                string::const_iterator next = find_if(i + 1, t.end(), bind1st(not_equal_to<char>(), *i));
                // Just use next - 1 to cheaply give us an extra char at the start; this is safe
                string u(next - 1, t.end());
                u[0] = *i;      // Record the previous char for the recursive call
                if (skip && *i != t[0]) {
                    // We have added a new segment that is different from the
                    // previous segment.  This means we can no longer use the
                    // character from the previous segment.
                    u.erase(remove(u.begin() + 1, u.end(), t[0]), u.end());
                }
                string v(i, next);
                v += calc(u, true);

                if (v.size() > best.size()) {
                    best = v;
                }

                i = next;
            }

            m = memo[skip].insert(make_pair(t, best)).first;
        }

        return (*m).second;
    }

public:
    RunFinder(string s) : s(s) {}

    string calc() {
        return calc(s, false);
    }
};

int main(int argc, char **argv) {
    RunFinder rf(argv[1]);
    cout << rf.calc() << '\n';
    return 0;
}

пример результаты

C:\runfinder>stopwatch runfinder aaaccaaaccbccbbbab
aaaaaaccccbbbb
stopwatch: Terminated. Elapsed time: 0ms
stopwatch: Process completed with exit code 0.

C:\runfinder>stopwatch runfinder abbaaasdbasdnfa,mnbmansdbfsbdnamsdnbfabbaaasdbasdnfa,mnbmansdbfsbdnamsdnbfabbaaasdbasdnfa,mnbmansdbfsbdnamsdnbfabbaaasdbasdnfa,mnbmansdbfsbdnamsdnbf
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,mnnsdbbbf
stopwatch: Terminated. Elapsed time: 609ms
stopwatch: Process completed with exit code 0.

C:\runfinder>stopwatch -v runfinder abcdefghijklmnopqrstuvwxyz123456abcdefghijklmnop
stopwatch: Command to be run: <runfinder abcdefghijklmnopqrstuvwxyz123456abcdefghijklmnop>.
stopwatch: Global memory situation before commencing: Used 2055507968 (49%) of 4128813056 virtual bytes, 1722564608 (80%) of 2145353728 physical bytes.
stopwatch: Process start time: 21/11/2012 02:53:14
abcdefghijklmnopqrstuvwxyz123456
stopwatch: Terminated. Elapsed time: 8062ms, CPU time: 7437ms, User time: 7328ms, Kernel time: 109ms, CPU usage: 92.25%, Page faults: 35473 (+35473), Peak working set size: 145440768, Peak VM usage: 145010688, Quota peak paged pool usage: 11596, Quota peak non paged pool usage: 1256
stopwatch: Process completed with exit code 0.
stopwatch: Process completion time: 21/11/2012 02:53:22

последний запуск, который занял 8s и использовал 145Mb, показывает, как у него могут быть проблемы со строками, содержащими много разных символов.

EDIT: добавлено в другой оптимизации: теперь мы выходим из цикла, который ищет место для начала подпоследовательности, если мы можем доказать, что он не может быть лучше, чем лучший, обнаруженный до сих пор. Это уменьшает время, необходимое для последнего примера, с 32 до 8!


EDIT: это решение неверно для проблемы OP. Я не удаляю его, потому что это может быть правильным для кого-то другого. :)

рассмотрим связанную задачу: найти самую длинную подпоследовательность s последовательных вхождений данного символа. Это можно решить в линейное время:

char c = . . .; // the given character
int start = -1;
int bestStart = -1;
int bestLength = 0;
int currentLength = 0;
for (int i = 0; i < S.length; ++i) {
    if (S.charAt(i) == c) {
        if (start == -1) {
            start = i;
        }
        ++currentLength;
    } else {
        if (currentLength > bestLength) {
            bestStart = start;
            bestLength = currentLength;
        }
        start = -1;
        currentLength = 0;
    }
}
if (bestStart >= 0) {
    // longest sequence of c starts at bestStart
} else {
    // character c does not occur in S
}

если количество различных символов (назовем его m) достаточно мал, просто примените этот алгоритм параллельно каждому символу. Это можно легко сделать путем преобразования start, bestStart, currentLength, bestLength массивы m долго. В конце сканируйте bestLength массив для индекса наибольшей записи и использовать соответствующую запись в bestStart массив в качестве ответа. Общая сложность составляет O (mn).


import java.util.*;

public class LongestSubsequence {

    /**
     * @param args
     */
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        String str = sc.next();

        execute(str);

    }


    static void execute(String str) {

        int[] hash = new int[256];
        String ans = "";

        for (int i = 0; i < str.length(); i++) {

            char temp = str.charAt(i);

            hash[temp]++;
        }

        for (int i = 0; i < hash.length; i++) {
            if (hash[i] != 0) {
                for (int j = 0; j < hash[i]; j++)
                    ans += (char) i;
            }
        }

        System.out.println(ans);
    }
}

Space: 256 -> O(256), я не знаю, правильно ли это сказать..., потому что O (256) я думаю, что это O (1) Время: O (n)