Вычислить максимально возможное количество прогонов для максимально возможной строки

24

[Этот вопрос является продолжением для вычисления прогонов строки ]

Период pстроки w- это любое положительное целое число, pтакое, что w[i]=w[i+p] когда бы ни были определены обе стороны этого уравнения. Позвольте per(w)обозначить размер наименьшего периода w. Мы говорим, что строка wпериодическая тогда и только тогда per(w) <= |w|/2.

Так что неофициально периодическая строка - это просто строка, которая состоит из другой строки, повторенной хотя бы один раз. Единственное осложнение состоит в том, что в конце строки нам не требуется полная копия повторяемой строки, если она повторяется полностью хотя бы один раз.

Для примера рассмотрим строку x = abcab. per(abcab) = 3как x[1] = x[1+3] = a, x[2]=x[2+3] = bи нет меньшего периода. Поэтому строка не abcabявляется периодической. Тем не менее, строка ababaявляется периодической как per(ababa) = 2.

Чем больше примеров, abcabca, ababababaи abcabcabcтакже являются периодическими.

Для тех, кто любит регулярные выражения, этот определяет, является ли строка периодической или нет:

\b(\w*)(\w+\1)\2+\b

Задача состоит в том, чтобы найти все максимальные периодические подстроки в более длинной строке. Это иногда называют пробегами в литературе.

Подстрока w- это максимальная периодическая подстрока (прогон), если она периодическая, и ни, w[i-1] = w[i-1+p]ни w[j+1] = w[j+1-p]. Неформально «прогон» не может содержаться в более крупном «прогоне» с тем же периодом.

Поскольку два прогона могут представлять одну и ту же строку символов, встречающихся в разных местах общей строки, мы будем представлять прогоны по интервалам. Здесь приведенное выше определение повторяется в терминах интервалов.

В рабочем цикле (или максимальная периодическая подстроки) в строке Tпредставляет собой интервал [i...j]с j>=i, таким образом, что

  • T[i...j] является периодическим словом с точкой p = per(T[i...j])
  • Это максимально. Формально, ни, T[i-1] = T[i-1+p]ни T[j+1] = T[j+1-p]. Неофициально прогон не может содержаться в большем прогоне с тем же периодом.

Обозначим через RUNS(T)множество прогонов в строке T.

Примеры прогонов

  • Четыре максимальные периодические подстроки (бежит) в строке T = atattattявляются T[4,5] = tt, T[7,8] = tt, T[1,4] = atat, T[2,8] = tattatt.

  • Строка T = aabaabaaaacaacacсодержит следующие 7 максимальных периодических подстроки (запускается): T[1,2] = aa, T[4,5] = aa, T[7,10] = aaaa, T[12,13] = aa, T[13,16] = acac, T[1,8] = aabaabaa, T[9,15] = aacaaca.

  • Строка T = atatbatatbсодержит следующие три прогона. Они являются T[1, 4] = atat, T[6, 9] = atatи T[1, 10] = atatbatatb.

Здесь я использую 1-индексирование.

Задание

Напишите код, чтобы для каждого целого числа n, начиная с 2, вы выводили наибольшее количество прогонов, содержащихся в любой двоичной строке длины n.

Гол

Ваш результат является самым высоким, которого nвы достигли за 120 секунд, так что k <= nни для кого больше никто не опубликовал более правильный ответ, чем вы. Очевидно, что если у вас есть все оптимальные ответы, вы получите балл за самый высокий nпост. Тем не менее, даже если ваш ответ не является оптимальным, вы все равно можете получить счет, если никто другой не сможет его побить.

Языки и библиотеки

Вы можете использовать любой доступный язык и библиотеки, которые вам нравятся. Там, где это возможно, было бы хорошо иметь возможность запускать ваш код, поэтому, пожалуйста, включите полное объяснение того, как запускать / компилировать ваш код в Linux, если это вообще возможно.

Пример оптима

В следующем: n, optimum number of runs, example string.

2 1 00
3 1 000
4 2 0011
5 2 00011
6 3 001001
7 4 0010011
8 5 00110011
9 5 000110011
10 6 0010011001
11 7 00100110011
12 8 001001100100
13 8 0001001100100
14 10 00100110010011
15 10 000100110010011
16 11 0010011001001100
17 12 00100101101001011
18 13 001001100100110011
19 14 0010011001001100100
20 15 00101001011010010100
21 15 000101001011010010100
22 16 0010010100101101001011

Что именно должен выводить мой код?

Для каждого nвашего кода необходимо вывести одну строку и количество прогонов, которые он содержит.

Моя машина Время будет запущено на моей машине. Это стандартная установка Ubuntu на восьмиъядерный процессор AMD FX-8350. Это также означает, что мне нужно иметь возможность запускать ваш код.

Ведущие ответы

  • 49 Андерс Kaseorg в C . Однопоточный и работает с L = 12 (2 ГБ ОЗУ).
  • 27 по cdlane в C .
Сообщество
источник
1
Если вы хотите, чтобы мы учитывали только {0,1}-строки, укажите это явно. В противном случае алфавит мог бы быть бесконечным, и я не понимаю, почему ваши тесты должны быть оптимальными, потому что кажется, что вы искали только {0,1}строки.
flawr
3
@flawr, я искал строки в троичном алфавите nдо, 12и он никогда не побивал двоичный алфавит. Эвристически я ожидал бы, что двоичная строка должна быть оптимальной, потому что добавление большего количества символов увеличивает минимальную длину цикла.
Питер Тейлор
1
В приведенных выше оптимальных результатах у вас есть "12 7 001001010010", но мой код выкачивает "12 8 110110011011", где запуски периода 1 (11, 11, 00, 11, 11), запуски периода 3 (110110, 011011) и есть прогон периода 4 (01100110) - где я ошибаюсь при подсчете прогонов?
CDlane
1
У @cdlane 0000 один прогон. Рассмотрим период 000 ... это всегда 1 независимо от того, сколько нулей.

Ответы:

9

С

Это делает рекурсивный поиск оптимальных решений, сильно сокращенных с использованием верхней границы числа прогонов, которые могут быть завершены неизвестным остатком строки. При вычислении верхней границы используется гигантская справочная таблица, размер которой контролируется константой L( L=11: 0,5 ГиБ,: L=122 ГиБ,: L=138 ГиБ).

На моем ноутбуке это увеличивается до n = 50 за 100 секунд; следующая строка идет через 142 секунды.

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#define N (8*sizeof(unsigned long long))
#define L 13
static bool a[N], best_a[N];
static int start[N/2 + 1], best_runs;
static uint8_t end_runs[2 << 2*L][2], small[N + 1][2 << 2*L];

static inline unsigned next_state(unsigned state, int b)
{
    state *= 2;
    state += b;
    if (state >= 2 << 2*L) {
        state &= ~(2 << 2*L);
        state |= 1 << 2*L;
    }
    return state;
}

static void search(int n, int i, int runs, unsigned state)
{
    if (i == n) {
        int r = runs;
        unsigned long long m = 0;
        for (int p = n / 2; p > 0; p--) {
            if (i - start[p] >= 2*p && !(m & 1ULL << start[p])) {
                m |= 1ULL << start[p];
                r++;
            }
        }
        if (r > best_runs) {
            best_runs = r;
            memcpy(best_a, a, n*sizeof(a[0]));
        }
    } else {
        a[i] = false;
        do {
            int r = runs, bound = 0, saved = 0, save_p[N/2], save_start[N/2], p, s = next_state(state, a[i]);
            unsigned long long m = 0;
            for (p = n/2; p > i; p--)
                if (p > L)
                    bound += (n - p + 1)/(p + 1);
            for (; p > 0; p--) {
                if (a[i] != a[i - p]) {
                    if (i - start[p] >= 2*p && !(m & 1ULL << start[p])) {
                        m |= 1ULL << start[p];
                        r++;
                    }
                    save_p[saved] = p;
                    save_start[saved] = start[p];
                    saved++;
                    start[p] = i + 1 - p;
                    if (p > L)
                        bound += (n - i)/(p + 1);
                } else {
                    if (p > L)
                        bound += (n - (start[p] + p - 1 > i - p ? start[p] + p - 1 : i - p))/(p + 1);
                }
            }
            bound += small[n - i - 1][s];

            if (r + bound > best_runs)
                search(n, i + 1, r, s);
            while (saved--)
                start[save_p[saved]] = save_start[saved];
        } while ((a[i] = !a[i]));
    }
}

int main()
{
    for (int n = 0; n <= N; n++) {
        if (n <= 2*L) {
            for (unsigned state = 1U << n; state < 2U << n; state++) {
                for (int b = 0; b < 2; b++) {
                    int r = 0;
                    unsigned long long m = 0;
                    for (int p = n / 2; p > 0; p--) {
                        if ((b ^ state >> (p - 1)) & 1) {
                            unsigned k = state ^ state >> p;
                            k &= -k;
                            k <<= p;
                            if (!(k & ~(~0U << n)))
                                k = 1U << n;
                            if (!((m | ~(~0U << 2*p)) & k)) {
                                m |= k;
                                r++;
                            }
                        }
                    }
                    end_runs[state][b] = r;
                }
                small[0][state] = end_runs[state][0] + end_runs[state][1];
            }
        }

        for (int l = 2*L < n - 1 ? 2*L : n - 1; l >= 0; l--) {
            for (unsigned state = 1U << l; state < 2U << l; state++) {
                int r0 = small[n - l - 1][next_state(state, 0)] + end_runs[state][0],
                    r1 = small[n - l - 1][next_state(state, 1)] + end_runs[state][1];
                small[n - l][state] = r0 > r1 ? r0 : r1;
            }
        }

        if (n >= 2) {
            search(n, 1, 0, 2U);
            printf("%d %d ", n, best_runs);
            for (int i = 0; i < n; i++)
                printf("%d", best_a[i]);
            printf("\n");
            fflush(stdout);
            best_runs--;
        }
    }
    return 0;
}

Выход:

$ gcc -mcmodel=medium -O2 runs.c -o runs
$ ./runs
2 1 00
3 1 000
4 2 0011
5 2 00011
6 3 001001
7 4 0010011
8 5 00110011
9 5 000110011
10 6 0010011001
11 7 00100110011
12 8 001001100100
13 8 0001001100100
14 10 00100110010011
15 10 000100110010011
16 11 0010011001001100
17 12 00100101101001011
18 13 001001100100110011
19 14 0010011001001100100
20 15 00101001011010010100
21 15 000101001011010010100
22 16 0010010100101101001011
23 17 00100101001011010010100
24 18 001001100100110110011011
25 19 0010011001000100110010011
26 20 00101001011010010100101101
27 21 001001010010110100101001011
28 22 0010100101101001010010110100
29 23 00101001011010010100101101011
30 24 001011010010110101101001011010
31 25 0010100101101001010010110100101
32 26 00101001011010010100101101001011
33 27 001010010110100101001011010010100
34 27 0001010010110100101001011010010100
35 28 00100101001011010010100101101001011
36 29 001001010010110100101001011010010100
37 30 0010011001001100100010011001001100100
38 30 00010011001001100100010011001001100100
39 31 001001010010110100101001011010010100100
40 32 0010010100101101001010010110100101001011
41 33 00100110010001001100100110010001001100100
42 35 001010010110100101001011010110100101101011
43 35 0001010010110100101001011010110100101101011
44 36 00101001011001010010110100101001011010010100
45 37 001001010010110100101001011010110100101101011
46 38 0010100101101001010010110100101001011010010100
47 39 00101101001010010110100101101001010010110100101
48 40 001010010110100101001011010010110101101001011010
49 41 0010100101101001010010110100101101001010010110100
50 42 00101001011010010100101101001011010110100101101011
51 43 001010010110100101001011010110100101001011010010100

Вот все оптимальные последовательности для n ≤ 64 (не только сначала лексикографически), сгенерированные модифицированной версией этой программы и многими часами вычислений.

Замечательная почти оптимальная последовательность

Префиксы бесконечной фрактальной последовательности

1010010110100101001011010010110100101001011010010100101…

которая инвариантна относительно преобразования 101 ↦ 10100, 00 ↦ 101:

  101   00  101   101   00  101   00  101   101   00  101   101   00  …
= 10100 101 10100 10100 101 10100 101 10100 10100 101 10100 10100 101 …

Похоже, что число прогонов почти оптимально - всегда в пределах 2 от оптимального для n ≤ 64. Количество прогонов в первых n символах, разделенное на n подходов (13 - 5√5) / 2 ≈ 0,90983. Но оказывается, что это не оптимальное соотношение - см. Комментарии.

Андерс Касеорг
источник
Спасибо за ответ и ваши исправления. Как вы думаете, каковы перспективы решения без грубой силы?
1
@Lembik Я не знаю. Я думаю, что мое текущее решение несколько быстрее, чем o (2 ^ N), учитывая достаточно памяти, но оно все еще экспоненциально. Я не нашел прямой формулы, которая полностью пропускает процесс поиска, но можно было бы существовать. Я предполагаю , что последовательность Туэ-Морса является асимптотически оптимальным с N⋅5 / 6 - O (журнал N) работает, но это , кажется, остается несколько прогонов за фактической оптимума.
Андерс Касеорг
Интересно, что 42/50> 5/6.
1
@Lembik Всегда следует ожидать, что асимптотические предсказания иногда будут побеждены небольшими партиями. Но на самом деле я был совершенно неправ - я нашел гораздо лучшую последовательность, которая, кажется, приближается к N⋅ (13 - 5√5) / 2 ≈ N⋅0,90983.
Андерс Касеорг
Очень впечатляюще. Я думаю, что гипотеза 0,90983 неверна. Проверьте bpaste.net/show/287821dc7214 . Он имеет длину 1558 и 1445 трасс.
2

Поскольку это не гонка, если есть только одна лошадь, я отправляю свое решение, хотя это лишь небольшая часть скорости Андерса Касерга и всего лишь третья часть, как загадочная. Компилировать с:

gcc -O2 run-count.c -o run-count

Сердцем моего алгоритма является простая схема shift и XOR:

введите описание изображения здесь

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

Я надеюсь, что это сделает по крайней мере 28 после двух минут на машине Лембика. (Я написал pthread-версию, но только смог заставить ее работать еще медленнее.)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>

enum { START = 0, WIDTH } ;

// Compare and shuffle just one thing while storing two
typedef union {
    uint16_t token;
    uint8_t data[sizeof(uint16_t)];
} overlay_t;

#define SENTINAL (0)  // marks the end of an array of overlay_t

#define NUMBER_OF_BITS (8 * sizeof(uint64_t))

void period_runs(uint64_t xor_bits, uint8_t nbits, uint8_t period, overlay_t *results) {

    overlay_t *results_ptr = results;
    uint8_t count = 0;

    for (uint8_t position = 0; position < nbits; position++) {

        if (xor_bits & 1ULL) {

            if ((nbits - position) < period) {
                break;  // no room left to succeed further
            }

            if (count >= period) {  // we found a run

                results_ptr->data[START] = position - (count - 1);
                results_ptr->data[WIDTH] = period + count;
                results_ptr++;
            }

            count = 0;
        } else {

            count++;
        }

        xor_bits >>= 1;
    }

    if (count >= period) {  // process the final run, if any

        results_ptr->data[START] = 0;
        results_ptr->data[WIDTH] = period + count;
        results_ptr++;
    }

    results_ptr->token = SENTINAL;
}

void number_runs(uint64_t number, uint8_t bit_length, overlay_t *results) {

    overlay_t sub_results[bit_length];
    uint8_t limit = bit_length / 2 + 1;
    uint64_t mask = (1ULL << (bit_length - 1)) - 1;

    overlay_t *results_ptr = results;
    results_ptr->token = SENTINAL;

    for (uint8_t period = 1; period < limit; period++) {

        uint64_t xor_bits = mask & (number ^ (number >> period));  // heart of the code
        period_runs(xor_bits, bit_length - period, period, sub_results);

        for (size_t i = 0; sub_results[i].token != SENTINAL; i++) {

            bool stop = false;  // combine previous and current results

            for (size_t j = 0; !stop && results[j].token != SENTINAL; j++) {

                // lower period result disqualifies higher period result over the same span 
                stop = (sub_results[i].token == results[j].token);
            }

            if (!stop) {

                (results_ptr++)->token = sub_results[i].token;
                results_ptr->token = SENTINAL;
            }
        }

        mask >>= 1;
    }
}

int main() {

    overlay_t results[NUMBER_OF_BITS];

    for (uint8_t bit_length = 2; bit_length < 25; bit_length++) {

        int best_results = -1;
        uint64_t best_number = 0;

        for (uint64_t number = 1ULL << (bit_length - 1); number < (1ULL << bit_length); number++) {

            // from the discussion comments, I should be able to solve this
            // with just bit strings that begin "11...", so toss the rest
            if ((number & (1ULL << (bit_length - 2))) == 0ULL) {

                continue;
            }

            uint64_t reversed = 0;

            for (uint8_t i = 0; i < bit_length; i++) {

                if (number & (1ULL << i)) {

                    reversed |= (1ULL << ((bit_length - 1) - i));
                }
            }

            if (reversed > number) {

                continue;  // ~ 1/4 of bit_strings are simply reversals, toss 'em
            }

            number_runs(number, bit_length, results);
            overlay_t *results_ptr = results;
            int count = 0;

            while ((results_ptr++)->token != SENTINAL) {

                count++;
            }

            if (count > best_results) {

                best_results = count;
                best_number = number;
            }
        }

        char *best_string = malloc(bit_length + 1);
        uint64_t number = best_number;
        char *string_ptr = best_string;

        for (int i = bit_length - 1; i >= 0; i--) {

            *(string_ptr++) = (number & (1ULL << i)) ? '1' : '0';
        }

        *string_ptr = '\0';

        printf("%u %d %s\n", bit_length, best_results, best_string);

        free(best_string);
    }

    return 0;
}

Выход:

> gcc -O2 run-count.c -o run-count
> ./run-count
2 1 11
3 1 110
4 2 1100
5 2 11000
6 3 110011
7 4 1100100
8 5 11001100
9 5 110010011
10 6 1100110011
11 7 11001100100
12 8 110110011011
13 8 1100100010011
14 10 11001001100100
15 10 110010011001000
16 11 1100100110010011
17 12 11001100100110011
18 13 110011001001100100
19 14 1101001011010010100
20 15 11010110100101101011
21 15 110010011001001100100
22 16 1100101001011010010100
23 17 11010010110100101001011
24 18 110100101001011010010100
25 19 1100100110010001001100100
26 20 11010010100101101001010010
27 21 110010011001000100110010011
28 22 1101001010010110100101001011
cdlane
источник
Добро пожаловать второй конь! Небольшой вопрос, почему вы и другой ответ предлагаете -O2 вместо -O3?
@Lembik, с оптимизацией -O2 я мог измерить разницу во времени выполнения кода, но я не смог измерить ничего с помощью -O3. Так как мы, якобы, безостановочно торгуем на скорость, я решил, что самый высокий уровень, который на самом деле имел значение, был лучшим. Если вы считаете, что мой код будет иметь более высокий рейтинг с -O3, сделайте это!
CDlane
-O3не предназначен быть «небезопасным». Это позволяет автоматически векторизовать, но, вероятно, здесь нечего векторизовать. Иногда он может замедлить выполнение кода, например, если он использует cmov без веток для чего-то, что ветвление предсказывало бы очень хорошо. Но обычно это должно помочь. Также стоит попробовать clang, чтобы увидеть, какой из gcc или clang делает лучший код для конкретного цикла. Кроме того, это почти всегда помогает использовать -march=native, или, по крайней мере, -mtune=nativeесли вы все еще хотите двоичный файл, который работает где угодно.
Питер Кордес