Здравствуйте, Chriso, Вы писали:
C>2. Дан список последовательностей чисел C>Найти самую часто встречающуюся непрерывную подпоследовательность и сколько раз она встречается. C>Облегчённый вариант, с которым я также не справился: длина подпоследовательности задана, рассматривать только подпоследовательности этой длины. C>Очевидное решение с Dictionary, где ключом является сама подпоследовательность не подходит по требуемой памяти, даже в "облегчённом" варианте. Предполагается, что список читается из файла и в память не влезет.
Весь список списков(т.е. целый файл в память не влезает), а один список из входного списка влезает в память?
Здравствуйте, MaximVK, Вы писали:
MVK>Да, судя по всему так оно и есть. Но если мы решаем задачу для K, то потом можно решить для K+1, MVK>сузив поиск для подпоследовательностей содержащих подпоследовательности найденные для K.
Как мы запомним подпоследовательности из предыдущего К? Ведь память должна быть O(1)
MVK>Я исхожу из того, что длинна строки (== последовательности) ограничена и влазит в нашу константную память. MVK>В этом случае предложенное решение будет N^2, где N количество строк, так как обработка одной строки в этом случае становится константой.
Что-то я запутался.
Пусть P — длина строки, N — количество строк. Превратить длину строки P в константу мы всегда сможем, а сейчас предлагаю ее учитывать.
Как работает алгоритм и какова его сложность?
Здравствуйте, Буравчик, Вы писали:
Б>Как мы запомним подпоследовательности из предыдущего К? Ведь память должна быть O(1)
В этом случае надо будет несколько проходов.
Ты можешь отсортировать подпоследовательности и запихать столько в память, сколько влезет. Это позволит уменьшить кол-во чтений файла.
MVK>>Я исхожу из того, что длинна строки (== последовательности) ограничена и влазит в нашу константную память. MVK>>В этом случае предложенное решение будет N^2, где N количество строк, так как обработка одной строки в этом случае становится константой.
Б>Что-то я запутался. Б>Пусть P — длина строки, N — количество строк. Превратить длину строки P в константу мы всегда сможем, а сейчас предлагаю ее учитывать. Б>Как работает алгоритм и какова его сложность?
Сходу только в брутфорсном варианте когда мы для для каждой подстроки считаем количество вхождений сканируя весь файл (считаем, что поиск подстроки стоит нам O(P)):
для i in 2..P (i = длина подпоследовательности)
N * (N-1) * (P-i) * P
получаем
O(N^2*P^3)
В умном варианте надо подумать, сходу не скажу. Постараюсь вечером прикинуть.
C>2. Дан список последовательностей чисел C>Найти самую часто встречающуюся непрерывную подпоследовательность и сколько раз она встречается. C>Облегчённый вариант, с которым я также не справился: длина подпоследовательности задана, рассматривать только подпоследовательности этой длины. C>Очевидное решение с Dictionary, где ключом является сама подпоследовательность не подходит по требуемой памяти, даже в "облегчённом" варианте. Предполагается, что список читается из файла и в память не влезет.
Что-то здесь не так. Даже упрощенная задача "найти самое часто встречающееся число в потоке" требует хранения всех чисел со счетчиками в памяти
Здравствуйте, scf, Вы писали:
scf>Что-то здесь не так.
определенно, не хватает ограничений. или не сказали или предполагалось, что топикстартер должен их был спросить.
scf>Даже упрощенная задача "найти самое часто встречающееся число в потоке" требует хранения всех чисел со счетчиками в памяти
ну файл можно несколько раз зачитать
Здравствуйте, MaximVK, Вы писали:
scf>>Даже упрощенная задача "найти самое часто встречающееся число в потоке" требует хранения всех чисел со счетчиками в памяти MVK>ну файл можно несколько раз зачитать
Мне что-то не приходит в голову, как чтение файла несколько раз может снизить потребность в памяти.
Здравствуйте, scf, Вы писали:
scf>Мне что-то не приходит в голову, как чтение файла несколько раз может снизить потребность в памяти.
Ну в твоем примере, для каждого числа перечитываешь файл и считаешь количество вхождений.
В этом случае нужно помнить только число и кол.-во вхождений.
Здравствуйте, Chriso, Вы писали: C>2. Дан список последовательностей чисел C>Найти самую часто встречающуюся непрерывную подпоследовательность и сколько раз она встречается. C>Облегчённый вариант, с которым я также не справился: длина подпоследовательности задана, рассматривать только подпоследовательности этой длины. C>Очевидное решение с Dictionary, где ключом является сама подпоследовательность не подходит по требуемой памяти, даже в "облегчённом" варианте. Предполагается, что список читается из файла и в память не влезет.
Во-первых, нужно дополнительное условие вроде "среди всех самых часто встречающихся последовательностей найти самую длинную". Иначе допустимым ответом будет последовательность длины 1 из самого часто встречающегося числа.
Во-вторых, было бы неплохо указать остальные ограничения по памяти — влезает ли в память один список, влезает ли в память Dictionary со всеми числами из всех последовательностей, влезает ли в память ответ, и т.д.
Я пока приведу решение для одной последовательности и случая, когда в память влезает всё.
Пусть у нас есть ответ — подпоследовательность, встречающаяся C раз. Тогда для неё выполняются следующие утверждения:
1) Любое число, входящее в эту подпоследовательность, встречается в ней ровно один раз. (Если оно встречается несколько раз, то последовательность длины 1, состоящая из этого числа, встретится k*C раз, т.е. чаще, чем ответ).
2) Любое число, входящее в эту подпоследовательность, встречается в исходной последовательности только во вхождениях ответа. (Если оно встречается где-то ещё, то последовательность длины 1 из этого числа, встретится C+k раз — чаще чем ответ).
Следствием этих утверждений является то, что все последовательности-кандидаты на ответ состоят из непересекающихся множеств чисел.
Решение следующее. На первом проходе считаем вхождения всех чисел, и составляем набор чисел, имеющих максимальное количество вхождений. Ответ будет состоять только из них.
На втором проходе жадным алгоритмом строим последовательности, удовлетворяющие приведенным выше условиям, и выбираем самую длинную из них. Сложность решения — O(N*Log(N)), где N — длина входной последовательности. Хотя нет, нашёл ошибку и теперь там O(N*N) вылезает.
Код решения
#include <iostream>
#include <vector>
#include <deque>
#include <map>
using namespace std;
class SequenceSolver
{
public:
SequenceSolver(vector<int>& sequence);
int getCount() { return maxCount_; }
vector<int> getSequence();
private:
// находит все числа-кандидаты для построения последовательностейvoid buildNumbers(vector<int>& sequence);
// прекращает построение/проверку активной последовательностиvoid terminateCurrentSequence();
// делит последовательность на две по элементу с индексом position
size_t splitSequence(deque<int>& sequence, size_t position);
// начинает построение новой последовательностиvoid beginSequence(size_t& index, int number);
// начинает проверку существующей последовательностиvoid beginSequenceCheck(size_t index, size_t position);
private:
static const size_t NONE = (size_t)(-1);
// числа-кандидаты для построения последовательностей
// значение - (индекс последовательности, позиция в последовательности)
map<int, pair<size_t, size_t>> numbers_;
// последовательности-кандидаты
vector<deque<int>> sequences_;
// индекс и текущая позиция активной последовательности
// (которая строится или проверяется в данный момент)
size_t activeIndex_;
size_t activePosition_;
// true, если активная последовательность встретилась впервыеbool activeNew_;
// сколько раз встречается искомая последовательностьint maxCount_;
};
SequenceSolver::SequenceSolver(vector<int>& sequence) : maxCount_(0), activeIndex_(NONE)
{
buildNumbers(sequence);
for (size_t i = 0; i < sequence.size(); i++)
{
auto it = numbers_.find(sequence[i]);
if (it == numbers_.end())
{
terminateCurrentSequence();
}
else
{
if (activeIndex_ == NONE) // нет активной последовательности
{
if (it->second.first == NONE)
{
beginSequence(it->second.first, it->first);
}
else
{
beginSequenceCheck(it->second.first, it->second.second);
}
}
else if (activeNew_) // есть новая активная последовательность
{
if (it->second.first == NONE)
{
// продолжаем новую активную последовательность
it->second.first = activeIndex_;
it->second.second = activePosition_;
sequences_[activeIndex_].push_back(it->first);
activePosition_ += 1;
}
else
{
// прерываем новую активную последовательность
// и начинаем проверку старой активной последовательности
terminateCurrentSequence();
beginSequenceCheck(it->second.first, it->second.second);
}
}
else// есть старая активная последовательность
{
if (it->second.first == NONE)
{
// прерываем старую активную последовательность
// и начинаем новую активную последовательность
terminateCurrentSequence();
beginSequence(it->second.first, it->first);
}
else if (it->second.first == activeIndex_ && it->second.second == activePosition_)
{
// продолжаем проверку старой активной последовательности
activePosition_ += 1;
if (activePosition_ == sequences_[activeIndex_].size())
{
activeIndex_ = NONE;
}
}
else
{
// прерываем старую активную последовательность
// и начинаем проверку другой старой активной последовательности
terminateCurrentSequence();
// обработка случая, когда число находится в этой же последовательности но на более
// далёкой позиции, например: [ 1 2 3 4 5 ] и [ 1 2 5 ]if (numbers_.find(sequence[i]) == numbers_.end())
{
activeIndex_ = NONE;
}
else
{
beginSequenceCheck(it->second.first, it->second.second);
}
}
}
}
}
}
vector<int> SequenceSolver::getSequence()
{
size_t max = 0;
for (size_t i = 1; i < sequences_.size(); i++)
{
if (sequences_[i].size() > sequences_[max].size())
{
max = i;
}
}
return vector<int>(sequences_[max].begin(), sequences_[max].end());
}
void SequenceSolver::buildNumbers(vector<int>& sequence)
{
map<int, int> counts;
for (size_t i = 0; i < sequence.size(); i++)
{
counts[sequence[i]]++;
}
maxCount_ = 0;
for (auto it = counts.begin(); it != counts.end(); ++it)
{
maxCount_ = it->second > maxCount_ ? it->second : maxCount_;
}
for (auto it = counts.begin(); it != counts.end(); ++it)
{
if (it->second == maxCount_)
{
numbers_[it->first] = make_pair(NONE, 0);
}
}
}
void SequenceSolver::terminateCurrentSequence()
{
if (activeIndex_ != NONE && !activeNew_)
{
splitSequence(sequences_[activeIndex_], activePosition_);
}
activeIndex_ = NONE;
}
size_t SequenceSolver::splitSequence(deque<int>& sequence, size_t position)
{
deque<int> temp(sequence.begin() + position, sequence.end());
sequence.erase(sequence.begin() + position, sequence.end());
size_t index = NONE;
if (temp.size() > 0)
{
index = sequences_.size();
for (size_t i = 0; i < temp.size(); i++)
{
numbers_[temp[i]] = make_pair(index, i);
}
sequences_.push_back(temp);
}
return index;
}
void SequenceSolver::beginSequence(size_t& index, int number)
{
index = sequences_.size();
sequences_.push_back(deque<int>());
sequences_.back().push_back(number);
activeIndex_ = index;
activePosition_ = 1;
activeNew_ = true;
}
void SequenceSolver::beginSequenceCheck(size_t index, size_t position)
{
activeIndex_ = splitSequence(sequences_[index], position);
activePosition_ = 1;
activeNew_ = false;
if (activePosition_ == sequences_[activeIndex_].size())
{
activeIndex_ = NONE;
}
}
int main()
{
int arr[] = { 1, 2, 3, 4, 0, 4, 2, 3, 2, 3 };
SequenceSolver sol(vector<int>(arr, arr + sizeof(arr) / sizeof(int)));
vector<int> ans = sol.getSequence();
cout << "N = " << sol.getCount() << endl;
cout << ans[0];
for (size_t i = 1; i < ans.size(); i++)
{
cout << " " << ans[i];
}
cout << endl;
return 0;
}
Если в ответе за числом X идёт число Y, то в исходной последовательности будет то же самое — за числом X всегда будет идти число Y.
Поэтому для решения задачи нам нужно посчитать количество вхождений каждого числа, и выяснить, идёт ли за ним всегда одно и то же число, или могут идти разные (конец последовательности трактуется как "идут разные числа").
Далее рассматриваем только числа, имеющие максимальное количество вхождений. По сути у нас есть ориентированный граф, вершины которого — числа, а дуги строятся по правилу: если за числом X всегда идёт число Y, то есть дуга X->Y. Ответом исходной задачи является самый длинный путь в этом графе. Поскольку исходящих дуг у каждой вершины не более одной, и отсутствуют циклы, то поиск такого пути тривиален.
И сложность решения теперь O(N*Log(N)).
Код решения
#include <iostream>
#include <vector>
#include <stack>
#include <map>
using namespace std;
void solve(vector<int>& sequence)
{
// для каждого числа считаем, сколько раз оно встречается
// и одинаковые ли числа идут после него
// если одинаковые - запоминаем их во втором поле, если разные - записываем -1
map<int, pair<int, int>> numbers;
for (size_t i = 0; i < sequence.size(); i++)
{
int nextNumber = i + 1 < sequence.size() ? sequence[i + 1] : -1;
auto it = numbers.find(sequence[i]);
if (it == numbers.end())
{
it = numbers.insert(make_pair(sequence[i], make_pair(1, nextNumber))).first;
}
else
{
it->second.first++;
if (it->second.second != nextNumber)
{
it->second.second = -1;
}
}
}
int maxCount = 0;
for (auto it = numbers.begin(); it != numbers.end(); ++it)
{
maxCount = it->second.first > maxCount ? it->second.first : maxCount;
}
// удаляем все числа, встречающиеся менее максимального числа раз
// одновременно инициализируем поле счетчика для запоминания длины последовательностиauto it = numbers.begin();
while (it != numbers.end())
{
auto tmp = it++;
if (tmp->second.first != maxCount)
{
numbers.erase(tmp);
}
else
{
tmp->second.first = -1;
}
}
// поиск в глубину из каждого числа с кэшированием результата
stack<int> stk;
for (auto it = numbers.begin(); it != numbers.end(); ++it)
{
if (it->second.first == -1)
{
auto tmp = it;
while (tmp->second.first == -1 && tmp->second.second != -1)
{
stk.push(tmp->first);
tmp = numbers.find(tmp->second.second);
}
if (tmp->second.second == -1)
{
tmp->second.first = 0;
}
while (!stk.empty())
{
tmp = numbers.find(stk.top());
tmp->second.first = numbers.find(tmp->second.second)->second.first + 1;
stk.pop();
}
}
}
// ищем число, из которого начинается путь максимальной длиныint maxLength = 0;
int maxNumber = 0;
for (auto it = numbers.begin(); it != numbers.end(); ++it)
{
if (it->second.first > maxLength)
{
maxLength = it->second.first;
maxNumber = it->first;
}
}
cout << "N = " << maxCount << endl;
cout << maxNumber;
it = numbers.find(maxNumber);
while (it->second.second != -1)
{
it = numbers.find(it->second.second);
cout << " " << it->first;
}
cout << endl;
}
int main()
{
int arr[] = { 1, 2, 3, 4, 0, 4, 2, 3, 2, 3 };
solve(vector<int>(arr, arr + sizeof(arr) / sizeof(int)));
return 0;
}
Здравствуйте, Sergei MO, Вы писали:
SM>Придумал, как сделать ещё проще. SM>И сложность решения теперь O(N*Log(N)). SM>int arr[] = { 1, 2, 3, 4, 0, 4, 2, 3, 2, 3 };
Решение ломается, если в конец последовательности добавить 2, 4.
Б>Т.е. в качестве ответа предложена последовательность {2}, а правильный ответ {2, 3}
С чего-то ответ [2, 3] является правильным? Невооруженным взглядом видно, что в этом массиве эта непрерывная подпоследовательность встречается три раза. Хотя другая подпоследовательность ([2]), которая встречается четыре раза. Так что это сразу свидетельствует том, что [2, 3] не может быть самой частвовстречаемой.
Здравствуйте, watchmaker, Вы писали:
W>С чего-то ответ [2, 3] является правильным? Невооруженным взглядом видно, что в этом массиве эта непрерывная подпоследовательность встречается три раза. Хотя другая подпоследовательность ([2]), которая встречается четыре раза. Так что это сразу свидетельствует том, что [2, 3] не может быть самой частвовстречаемой.
Ну, ок. Я пытался привести пример, когда за двойкой будет следовать другой элемент (четверка вместо тройки), и алгоритм ошибется.
Вот для такого массива { 1, 2, 3, 4, 0, 4, 2, 3, 2, 3, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4 }
N = 9
2
Правильный ответ {2, 4} и N=6
ДОБАВЛЕНО:
Если уж что-то и реализовывать из достаточно простых и эффективных алгоритмов, то это алгоритм на суффиксных массивах (не дереве).
— формируем массив суффиксов (можно просто хранить начальный индекс суффикса)
— сортируем все суффиксы по алфавиту (лексикографически)
— пробегаем по списку и находим общие префиксы у суффиксов (сравнивая соседние элементы)
Выводим ответ — максимальный найденный префикс
Но почему он правильный?
Ведь эта подпоследовательность встречается только 6 раз. Хотя в этот массив входит и другая подпоследовательность (всё та же [2]), которая встречается целых 9 раз. Что явно чаще
Здравствуйте, watchmaker, Вы писали:
W>Ведь эта подпоследовательность встречается только 6 раз. Хотя в этот массив входит и другая подпоследовательность (всё та же [2]), которая встречается целых 9 раз. Что явно чаще
Здравствуйте, watchmaker, Вы писали:
W>Но почему он правильный? W>Ведь эта подпоследовательность встречается только 6 раз. Хотя в этот массив входит и другая подпоследовательность (всё та же [2]), которая встречается целых 9 раз. Что явно чаще
Имхо, задача с длиной последовательности равной единице — это совсем другая задач. Это задача про поиск наиболее часто встречающегося элемента в массиве.
А условие "рассматривать только подпоследовательности этой длины" (в облегченной задаче!) намекает нам, что наша задача не про подсчет элементов, а про поиск последовательности (т.е. хотя бы из двух элементов).
Здравствуйте, Буравчик, Вы писали:
Б>А условие "рассматривать только подпоследовательности этой длины" (в облегченной задаче!) намекает нам, что наша задача не про подсчет элементов, а про поиск последовательности (т.е. хотя бы из двух элементов).
В исходном сообщении в задаче опущены и другие условия, не только это, которые важны для того чтобы понять какое решение является подходящим, а какое нет. Так что многие додумывают как хотят
Гораздо печальнее, если это происходит не на форуме, а во время интервью кандидат не уточняет важные детали и решает не ту задачу, которую имел ввиду собеседующий.
То есть из исходного сообщения понятно из какой области спрашивали знания. В целом интересно. Как раз при подготовке к собеседованию ясно какие разделы нужно повторить. Но условия задач лучше искать в другом месте.
Здравствуйте, Chriso, Вы писали:
C>1. Дан список строк S[1..N], нужно вернуть список R всех возможных списков таких, что: C>R[i][j] ∈ S C>string.Join("", R[i]) ∈ S C>R[i].Count >= 2
C>Пример: C>вход [a, b, ab, cd, abcd, fgh, f, g, h] C>выход [[a, b], [a, b, cd], [ab, cd], [f, g, h]]
Перебираем все слова в input.
Отсекаем те, длина которы меньше 2-х символов, т.к. для них не будет решений, удоволетворяющих условию R[i].Count >= 2
Для каждого слова перебираем все возможные комбинации head и tail (скользящий splitIndex)
Если head найден в input, то рекурсивно ищем все возможные комбинации для tail.
Из найденных комбинаций убираем состоящие из одного элемента.
class Program
{
private static string[] input = new[] { "a", "b", "ab", "cd", "abcd", "fgh", "f", "g", "h" };
static void Main()
{
var output = input
.Where(term => term.Length >= 2) // Заранее отсекаем все слова, которые короче 2-х символов
.SelectMany(term => FindCombinations(term)) // Ищем все возможные комбинации для каждого слова
.Where(list => list.Count() >= 2); // Отсекаем комбинации, которые состоят из менее 2-х элементов
Console.WriteLine(JsonConvert.SerializeObject(output));
Console.ReadLine();
}
static IEnumerable<IEnumerable<string>> FindCombinations(string term)
{
var splitIndex = 1;
while (splitIndex <= term.Length)
{
var (head, tail) = (term.Substring(0, splitIndex), term.Substring(splitIndex++));
if (input.Contains(head))
{
if (tail.Length == 0)
yield return new[] { head };
else
foreach (var result in FindCombinations(tail))
yield return new[] { head }.Concat(result);
}
}
}
}