Ответ 1
Это действительно интересная проблема. Во-первых, я покажу, как бы я решил эту проблему. Мы увидим, что это не так сложно при использовании рекурсии и что проблема может быть решена с помощью динамического программирования. Мы создадим общее решение, которое не требует жесткого кодирования верхнего предела 26
для каждой кодовой точки.
Заметка о терминологии . Я буду использовать термин "кодовая точка" (CP) не в смысле Unicode, а для обозначения одного из кодовых номеров 1
хотя 26
. Каждая кодовая точка представляется как переменное количество символов. Я также буду использовать термины закодированный текст (ET) и чистый текст (CT) в их очевидных значениях. Когда речь идет о последовательности или массиве, первый элемент называется головой. Остальные элементы - это хвост.
Теоретическая прелюдия
- EC
""
имеет одно декодирование: CT""
. - EC
"3"
может быть деструктурирован в'3' + ""
и имеет одно декодирование. - EC
"23"
может быть деструктурирован как'2' + "3"
или'23' + ""
. Каждый из хвостов имеет одно декодирование, поэтому весь EC имеет два декодирования. - EC
"123"
может быть деструктурирован как'1' + "23"
или'12' + "3"
. У хвостов есть два и один декодирования соответственно. Вся EC имеет три декодирования. Деструктурирование'123' + ""
неверно, потому что123 > 26
, наш верхний предел. - ... и т.д. для EC любой длины.
Поэтому, учитывая строку типа "123"
, мы можем получить количество декодирования, найдя все действующие CP в начале и суммируя количество декодирования каждого хвоста.
Самая сложная часть этого - найти действительные главы. Мы можем получить максимальную длину головы, посмотрев строковое представление верхнего предела. В нашем случае голова может содержать до двух символов. Но не все главы соответствующих длин действительны, потому что они также должны быть ≤ 26
.
Наивная рекурсивная реализация
Теперь мы выполнили всю необходимую работу для простой (но работающей) рекурсивной реализации:
static final int upperLimit = 26;
static final int maxHeadSize = ("" + upperLimit).length();
static int numDecodings(String encodedText) {
// check base case for the recursion
if (encodedText.length() == 0) {
return 1;
}
// sum all tails
int sum = 0;
for (int headSize = 1; headSize <= maxHeadSize && headSize <= encodedText.length(); headSize++) {
String head = encodedText.substring(0, headSize);
String tail = encodedText.substring(headSize);
if (Integer.parseInt(head) > upperLimit) {
break;
}
sum += numDecodings(tail);
}
return sum;
}
Резервированная реализация с кэшем
Очевидно, что это не очень эффективно, потому что (для более длинных ET) один и тот же хвост будет анализироваться несколько раз. Кроме того, мы создаем много временных строк, но мы это пока дадим. Одна вещь, которую мы можем легко сделать, - это мемуаровать количество декодирования определенного хвоста. Для этого мы используем массив той же длины, что и входная строка:
static final int upperLimit = 26;
static final int maxHeadSize = ("" + upperLimit).length();
static int numDecodings(String encodedText) {
return numDecodings(encodedText, new Integer[1 + encodedText.length()]);
}
static int numDecodings(String encodedText, Integer[] cache) {
// check base case for the recursion
if (encodedText.length() == 0) {
return 1;
}
// check if this tail is already known in the cache
if (cache[encodedText.length()] != null) {
return cache[encodedText.length()];
}
// cache miss -- sum all tails
int sum = 0;
for (int headSize = 1; headSize <= maxHeadSize && headSize <= encodedText.length(); headSize++) {
String head = encodedText.substring(0, headSize);
String tail = encodedText.substring(headSize);
if (Integer.parseInt(head) > upperLimit) {
break;
}
sum += numDecodings(tail, cache); // pass the cache through
}
// update the cache
cache[encodedText.length()] = sum;
return sum;
}
Обратите внимание, что мы используем Integer[]
, а не int[]
. Таким образом, мы можем проверить несуществующие записи, используя тест для null
. Это решение не только корректно, но и быстро-наивная рекурсия выполняется в O (количество декодирования) времени, а memoized версия работает в O (длина строки).
К DP-решению
Когда вы запускаете над кодом в своей голове, вы заметите, что первый вызов со всей строкой будет иметь промаху в кеше, а затем вычислить количество декодирования для первого хвоста, который также пропускает кеш каждый раз. Мы можем избежать этого, сначала оценив хвосты, начиная с конца ввода. Поскольку все хвосты будут оцениваться до начала всей строки, мы можем удалить проверки на пропуски кеша. Теперь у нас также нет причин для рекурсии, потому что все предыдущие результаты уже находятся в кеше.
static final int upperLimit = 26;
static final int maxHeadSize = ("" + upperLimit).length();
static int numDecodings(String encodedText) {
int[] cache = new int[encodedText.length() + 1];
// base case: the empty string at encodedText.length() is 1:
cache[encodedText.length()] = 1;
for (int position = encodedText.length() - 1; position >= 0; position--) {
// sum directly into the cache
for (int headSize = 1; headSize <= maxHeadSize && headSize + position <= encodedText.length(); headSize++) {
String head = encodedText.substring(position, position + headSize);
if (Integer.parseInt(head) > upperLimit) {
break;
}
cache[position] += cache[position + headSize];
}
}
return cache[0];
}
Этот алгоритм можно было бы оптимизировать, заметив, что мы только запрашиваем последние maxHeadSize
элементы в кеше. Поэтому вместо массива мы могли бы использовать очередь фиксированного размера. В этот момент у нас будет динамическое программирующее решение, которое работает в * O (длина входной длины) и O (maxHeadSize).
Специализация для upperLimit = 26
Вышеуказанные алгоритмы были сохранены как можно более общие, но мы можем вручную и вручную их специфицировать для конкретного upperLimit
. Это может быть полезно, потому что это позволяет нам делать различные оптимизации. Тем не менее, это вводит "магические числа", которые делают код более сложным для поддержания. Поэтому такие ручные специализации следует избегать в некритичном программном обеспечении (и вышеупомянутый алгоритм уже так же быстро, как и он).
static int numDecodings(String encodedText) {
// initialize the cache
int[] cache = {1, 0, 0};
for (int position = encodedText.length() - 1; position >= 0; position--) {
// rotate the cache
cache[2] = cache[1];
cache[1] = cache[0];
cache[0] = 0;
// headSize == 1
if (position + 0 < encodedText.length()) {
char c = encodedText.charAt(position + 0);
// 1 .. 9
if ('1' <= c && c <= '9') {
cache[0] += cache[1];
}
}
// headSize == 2
if (position + 1 < encodedText.length()) {
char c1 = encodedText.charAt(position + 0);
char c2 = encodedText.charAt(position + 1);
// 10 .. 19
if ('1' == c1) {
cache[0] += cache[2];
}
// 20 .. 26
else if ('2' == c1 && '0' <= c2 && c2 <= '6') {
cache[0] += cache[2];
}
}
}
return cache[0];
}
Сравнение с вашим кодом
Код внешне похож. Тем не менее, ваш синтаксический анализ вокруг символов более запутан. Вы ввели переменную used
, которая, если она установлена, уменьшит счетчик декодирования, чтобы учесть двухсимвольные СР. Это неправильно, но я не знаю, почему. Основная проблема заключается в том, что вы удваиваете счет почти на каждом шагу. Как мы видели, предыдущие подсчеты добавляются и могут очень отличаться.
Это означает, что вы написали код без надлежащей подготовки. Вы можете написать много видов программного обеспечения, не думая слишком много, но вы не можете обойтись без тщательного анализа при разработке алгоритма. Для меня часто полезно разрабатывать алгоритм на бумаге и рисовать диаграммы каждого шага (в соответствии с "теоретической прелюдией" этого ответа). Это особенно полезно, когда вы слишком много думаете о языке, который собираетесь внедрить, и слишком мало о возможных ошибках.
Я предлагаю вам прочитать "доказательства по индукции" , чтобы понять, как написать правильный рекурсивный алгоритм. Когда у вас есть рекурсивное решение, вы всегда можете перевести его в итеративную версию.