Calcule el número máximo de ejecuciones posibles para una cadena lo más grande posible

24

[Esta pregunta es un seguimiento para calcular las ejecuciones de una cadena ]

Un período pde una cadena wes cualquier número entero positivo de pmodo que w[i]=w[i+p] siempre que se definan ambos lados de esta ecuación. Deje per(w)denotar el tamaño del período más pequeño de w. Decimos que una cadena wes periódica iff per(w) <= |w|/2.

Entonces, informalmente, una cadena periódica es solo una cadena que se compone de otra cadena repetida al menos una vez. La única complicación es que al final de la cadena no necesitamos una copia completa de la cadena repetida siempre que se repita en su totalidad al menos una vez.

Por ejemplo, considere la cadena x = abcab. per(abcab) = 3como x[1] = x[1+3] = a, x[2]=x[2+3] = by no hay un período más pequeño. La cadena, abcabpor lo tanto, no es periódica. Sin embargo, la cadena ababaes periódica como per(ababa) = 2.

Como más ejemplos, abcabca, ababababay abcabcabctambién son periódicas.

Para aquellos a quienes les gustan las expresiones regulares, este detecta si una cadena es periódica o no:

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

La tarea es encontrar todas las subcadenas máximas periódicas en una cadena más larga. A veces se les llama carreras en la literatura.

Una subcadena wes una subcadena periódica máxima (ejecución) si es periódica y ni w[i-1] = w[i-1+p]tampoco w[j+1] = w[j+1-p]. Informalmente, la "ejecución" no puede estar contenida en una "ejecución" más grande con el mismo período.

Debido a que dos ejecuciones pueden representar la misma cadena de caracteres que ocurren en diferentes lugares en la cadena general, representaremos las ejecuciones por intervalos. Aquí está la definición anterior repetida en términos de intervalos.

Una ejecución (o subcadena periódica máxima) en una cadena Tes un intervalo [i...j]con j>=ital que

  • T[i...j] es una palabra periódica con el punto p = per(T[i...j])
  • Es maximo. Formalmente, ni T[i-1] = T[i-1+p]tampoco T[j+1] = T[j+1-p]. Informalmente, la ejecución no puede estar contenida en una ejecución más grande con el mismo período.

Denote por RUNS(T)el conjunto de ejecuciones en cadena T.

Ejemplos de carreras

  • Los cuatro subseries periódicas máximas (carreras) en la cadena T = atattattson T[4,5] = tt, T[7,8] = tt, T[1,4] = atat, T[2,8] = tattatt.

  • La cadena T = aabaabaaaacaacaccontiene los siguientes 7 subcadenas periódicas máximas (carreras): 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.

  • La cadena T = atatbatatbcontiene las siguientes tres ejecuciones. Ellos son: T[1, 4] = atat, T[6, 9] = ataty T[1, 10] = atatbatatb.

Aquí estoy usando la indexación 1.

La tarea

Escriba el código de manera que para cada número entero n que comience en 2, genere la mayor cantidad de ejecuciones contenidas en cualquier cadena binaria de longitud n.

Puntuación

Su puntaje es el más alto nque alcanza en 120 segundos, de modo que, para todos k <= n, nadie más ha publicado una respuesta correcta más alta que usted. Claramente, si tiene todas las respuestas óptimas, obtendrá la puntuación más alta nque publique. Sin embargo, incluso si su respuesta no es la óptima, aún puede obtener el puntaje si nadie más puede superarlo.

Idiomas y bibliotecas

Puede usar cualquier idioma y bibliotecas disponibles que desee. Siempre que sea posible, sería bueno poder ejecutar su código, por lo tanto, si es posible, incluya una explicación completa de cómo ejecutar / compilar su código en Linux.

Ejemplo optima

En lo que sigue: 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

¿Qué debería exactamente mostrar mi código?

Para cada uno, nsu código debe generar una sola cadena y el número de ejecuciones que contiene.

Mi máquina Los tiempos se ejecutarán en mi máquina. Esta es una instalación estándar de ubuntu en un procesador AMD FX-8350 de ocho núcleos. Esto también significa que necesito poder ejecutar su código.

Respuestas principales

  • 49 por Anders Kaseorg en C . Rosca simple y ejecución con L = 12 (2 GB de RAM).
  • 27 por cdlane en C .
Comunidad
fuente
1
Si solo desea que consideremos solo las {0,1}cadenas, indíquelo explícitamente. De lo contrario, el alfabeto podría ser infinito, y no veo por qué sus casos de prueba deberían ser óptimos, porque parece que también buscó {0,1}cadenas también.
flawr
3
@flawr, busqué cadenas sobre un alfabeto ternario nhasta 12y nunca superó el alfabeto binario. Heurísticamente, esperaría que una cadena binaria sea óptima, porque agregar más caracteres aumenta la longitud mínima de una ejecución.
Peter Taylor
1
En los resultados óptimos anteriores tiene "12 7 001001010010" pero mi código emite "12 8 110110011011" donde las ejecuciones del período 1 son (11, 11, 00, 11, 11), las ejecuciones del período 3 son (110110, 011011) y hay un período de 4 carreras (01100110): ¿dónde me equivoco al contar mi carrera?
cdlane
1
@cdlane 0000 tiene una carrera. Considere el período de 000 ... siempre es 1, no importa cuántos ceros.

Respuestas:

9

do

Esto hace una búsqueda recursiva de soluciones óptimas, muy podadas utilizando un límite superior en el número de ejecuciones que podría terminar el resto desconocido de la cadena. El cálculo del límite superior utiliza una tabla de búsqueda gigantesca cuyo tamaño está controlado por la constante L( L=11: 0.5 GiB L=12,: 2 GiB L=13,: 8 GiB).

En mi computadora portátil, esto sube a través de n = 50 en 100 segundos; la siguiente línea llega a 142 segundos.

#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;
}

Salida:

$ 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

Aquí están todas las secuencias óptimas para n ≤ 64 (no solo la lexicografía primero), generadas por una versión modificada de este programa y muchas horas de cálculo.

Una notable secuencia casi óptima

Los prefijos de la secuencia fractal infinita

1010010110100101001011010010110100101001011010010100101…

eso es invariante bajo la transformación 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 …

parece tener un número de corridas casi óptimo, siempre dentro de 2 del óptimo para n ≤ 64. El número de corridas en los primeros n caracteres dividido por n se aproxima (13 - 5√5) / 2 ≈ 0.90983. Pero resulta que esta no es la proporción óptima; vea los comentarios.

Anders Kaseorg
fuente
Gracias la respuesta y tus correcciones. ¿Cuáles cree que son las perspectivas para una solución de fuerza no bruta?
1
@Lembik no lo sé. Creo que mi solución actual es algo más rápida que o (2 ^ N) con suficiente memoria, pero sigue siendo exponencial. No he encontrado una fórmula directa que omita completamente el proceso de búsqueda, pero podría existir. Supongo que la secuencia de Thue – Morse es asintóticamente óptima con N⋅5 / 6 - O (log N), pero parece quedarse un puñado de carreras detrás del óptimo real.
Anders Kaseorg
Curiosamente, 42/50> 5/6.
1
@Lembik Uno siempre debe esperar que las predicciones asintóticas a veces sean superadas por pequeñas cantidades. Pero en realidad estaba completamente equivocado: encontré una secuencia mucho mejor que parece acercarse a N⋅ (13 - 5√5) / 2 ≈ N⋅0.90983 carreras.
Anders Kaseorg
Muy impresionante. Sin embargo, creo que la conjetura 0.90983 no es correcta. Echa un vistazo a bpaste.net/show/287821dc7214 . Tiene una longitud de 1558 y tiene 1445 carreras.
2

Como no es una carrera si solo hay un caballo, presento mi solución, aunque es solo una fracción de la velocidad de Anders Kaseorg y solo un tercio como críptico. Compilar con:

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

El corazón de mi algoritmo es un cambio simple y un esquema XOR:

ingrese la descripción de la imagen aquí

Una ejecución de ceros en el resultado XOR que sea mayor o igual que el período / cambio actual indica una ejecución en la cadena original para este período. A partir de esto, puede saber cuánto duró la ejecución y dónde comienza y termina. El resto del código está sobrecargado, configurando la situación y decodificando los resultados.

Espero que haga al menos 28 después de dos minutos en la máquina de Lembik. (Escribí una versión de pthread, pero solo logré que funcionara aún más lentamente).

#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;
}

Salida:

> 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
fuente
Bienvenido segundo caballo! Pequeña consulta, ¿por qué usted y la otra respuesta sugieren -O2 en lugar de -O3?
@Lembik, con la optimización de -O2, pude medir una diferencia de tiempo ejecutando el código pero no pude medir más con -O3. Dado que supuestamente estamos cambiando de forma segura por la velocidad, supuse que el nivel más alto que realmente marcó la diferencia fue el mejor. Si crees que mi código se clasificará más alto con -O3, ¡adelante!
cdlane
-O3no está destinado a ser "inseguro". Permite la vectorización automática, pero probablemente no haya nada que vectorizar aquí. A veces puede hacer que el código sea más lento, por ejemplo, si usa un cmov sin ramificaciones para algo donde una ramificación hubiera predicho muy bien. Pero generalmente debería ayudar. Por lo general, también vale la pena probar clang, para ver cuál de gcc o clang hace un mejor código para un bucle específico. Además, casi siempre ayuda usar -march=native, o al menos -mtune=nativesi todavía quieres un binario que se ejecute en cualquier lugar.
Peter Cordes