Berechnen Sie die maximal mögliche Anzahl von Läufen für eine möglichst große Zeichenfolge

24

[Diese Frage ist ein Follow-up, um die Läufe einer Zeichenfolge zu berechnen ]

Eine Periode peines Strings wist eine positive ganze Zahl, pso dass w[i]=w[i+p] immer dann , wenn beide Seiten dieser Gleichung definiert sind. Lassen Sie per(w)bezeichnen die Größe der kleinsten Periode von w. Wir sagen, dass ein String wein periodisches iff ist per(w) <= |w|/2.

Informell ist eine periodische Zeichenfolge also nur eine Zeichenfolge, die aus einer anderen Zeichenfolge besteht, die mindestens einmal wiederholt wird. Die einzige Schwierigkeit besteht darin, dass wir am Ende der Zeichenfolge keine vollständige Kopie der wiederholten Zeichenfolge benötigen, solange diese mindestens einmal vollständig wiederholt wird.

Betrachten Sie beispielsweise die Zeichenfolge x = abcab. per(abcab) = 3wie x[1] = x[1+3] = a, x[2]=x[2+3] = bund es gibt keine kleinere Periode. Die Zeichenfolge abcabist daher nicht periodisch. Die Zeichenfolge ababaist jedoch periodisch als per(ababa) = 2.

Als weitere Beispiele abcabca, ababababaund abcabcabcsind auch periodisch.

Für diejenigen, die reguläre Ausdrücke mögen, erkennt dieser, ob eine Zeichenfolge periodisch ist oder nicht:

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

Die Aufgabe besteht darin, alle maximal periodischen Teilzeichenfolgen in einer längeren Zeichenfolge zu finden. Diese werden in der Literatur manchmal als Läufe bezeichnet .

Ein Teilstring wist ein maximaler periodischer Teilstring (run), wenn er periodisch ist und weder w[i-1] = w[i-1+p]noch w[j+1] = w[j+1-p]. Informell kann der "Lauf" nicht in einem größeren "Lauf" mit demselben Zeitraum enthalten sein.

Da zwei Durchläufe dieselbe Zeichenfolge darstellen können, die an verschiedenen Stellen in der Gesamtzeichenfolge vorkommt, werden die Durchläufe in Intervallen dargestellt. Hier wird die obige Definition in Intervallen wiederholt.

Ein Lauf (oder maximaler periodischer Teilstring) in einem String Tist ein Intervall [i...j]mit j>=i, so dass

  • T[i...j] ist ein periodisches Wort mit der Periode p = per(T[i...j])
  • Es ist maximal. Formal weder T[i-1] = T[i-1+p]noch T[j+1] = T[j+1-p]. Informell kann der Lauf nicht in einem größeren Lauf mit demselben Zeitraum enthalten sein.

Bezeichnen Sie durch RUNS(T)die Menge der Läufe in Zeichenfolge T.

Beispiele für Läufe

  • Der vier maximal periodische Teil (läuft) in String T = atattattist T[4,5] = tt, T[7,8] = tt, T[1,4] = atat, T[2,8] = tattatt.

  • Die Zeichenfolge T = aabaabaaaacaacacenthält die folgenden 7 maximal periodischen Strings (Läufe): 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.

  • Die Zeichenfolge T = atatbatatbenthält die folgenden drei Läufe. Sie sind: T[1, 4] = atat, T[6, 9] = atatund T[1, 10] = atatbatatb.

Hier verwende ich die 1-Indizierung.

Die Aufgabe

Schreiben Sie Code so, dass Sie für jede Ganzzahl n, die bei 2 beginnt, die größte Anzahl von Läufen ausgeben, die in einer binären Zeichenfolge der Länge enthalten sind n.

Ergebnis

Ihre Punktzahl ist die höchste, die nSie in 120 Sekunden erreicht haben, sodass k <= nniemand anders eine richtigere Antwort als Sie gepostet hat. Wenn Sie alle optimalen Antworten haben, erhalten Sie die Punktzahl für den höchsten nBeitrag, den Sie verfassen. Aber auch wenn Ihre Antwort nicht optimal ist, können Sie die Punktzahl erhalten, wenn niemand anders sie schlagen kann.

Sprachen und Bibliotheken

Sie können jede verfügbare Sprache und Bibliothek verwenden, die Sie mögen. Wo immer möglich, wäre es gut, wenn Sie Ihren Code ausführen könnten. Fügen Sie daher bitte eine vollständige Erklärung dazu bei, wie Sie Ihren Code unter Linux ausführen / kompilieren, wenn dies überhaupt möglich ist.

Beispiel optima

Im Folgenden: 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

Was genau soll mein Code ausgeben?

Für jeden nCode sollte eine einzelne Zeichenfolge und die Anzahl der darin enthaltenen Läufe ausgegeben werden.

Mein Computer Die Timings werden auf meinem Computer ausgeführt. Dies ist eine Ubuntu-Standardinstallation auf einem AMD FX-8350 Eight-Core-Prozessor. Dies bedeutet auch, dass ich in der Lage sein muss, Ihren Code auszuführen.

Führende Antworten

  • 49 von Anders Kaseorg in C . Einfach eingefädelt und mit L = 12 (2 GB RAM) ausgeführt.
  • 27 durch cdlane in C .
Gemeinschaft
quelle
1
Wenn Sie nur {0,1}-strings berücksichtigen möchten, geben Sie dies bitte ausdrücklich an. Andernfalls könnte das Alphabet möglicherweise unendlich sein, und ich verstehe nicht, warum Ihre Testfälle optimal sein sollten, da Sie anscheinend auch nur nach {0,1}Zeichenfolgen gesucht haben.
Fehler
3
@flawr, ich habe Strings über einem ternären Alphabet nach nbis zu durchsucht 12und es hat das binäre Alphabet nie übertroffen . Aus heuristischer Sicht würde ich erwarten, dass eine Binärzeichenfolge optimal ist, da das Hinzufügen weiterer Zeichen die Mindestlänge eines Durchlaufs erhöht.
Peter Taylor
1
In den obigen optimalen Ergebnissen haben Sie "12 7 001001010010", aber mein Code pumpt "12 8 110110011011" aus, wobei Periode 1 Läufe sind (11, 11, 00, 11, 11), Periode 3 Läufe sind (110110, 011011) und es gibt ein Lauf der Periode 4 (01100110) - Wo gehe ich bei meiner Laufzählung falsch?
CDLane
1
@cdlane 0000 hat einen Lauf. Betrachten Sie die Periode von 000 ... es ist immer 1, egal wie viele Nullen.

Antworten:

9

C

Dies führt eine rekursive Suche nach optimalen Lösungen durch, die mithilfe einer Obergrenze für die Anzahl der Läufe, die mit dem unbekannten Rest der Zeichenfolge abgeschlossen werden könnten, stark eingeschränkt werden. Die Berechnung der oberen Schranke verwendet eine gigantische Nachschlagetabelle, deren Größe durch die Konstante gesteuert wird L( L=11: 0,5 GiB,: L=122 GiB,: L=138 GiB).

Auf meinem Laptop sind es in 100 Sekunden n = 50; Die nächste Zeile kommt bei 142 Sekunden.

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

Ausgabe:

$ 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

Hier finden Sie alle optimalen Sequenzen für n ≤ 64 (nicht nur die lexikografisch erste), die mit einer modifizierten Version dieses Programms und vielen Stunden Berechnungszeit generiert wurden.

Eine bemerkenswerte nahezu optimale Sequenz

Die Präfixe der unendlichen fraktalen Sequenz

1010010110100101001011010010110100101001011010010100101…

das ist invariant unter der Transformation 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 …

scheinen eine nahezu optimale Anzahl von Läufen zu haben - immer innerhalb von 2 von optimal für n ≤ 64. Die Anzahl der Läufe in den ersten n Zeichen geteilt durch n Ansätze (13 - 5√5) / 2 2 0.90983. Es stellt sich jedoch heraus, dass dies nicht das optimale Verhältnis ist - siehe Kommentare.

Anders Kaseorg
quelle
Vielen Dank für die Antwort und Ihre Korrekturen. Wie sehen Sie die Aussichten für eine gewaltfreie Lösung?
1
@ Lembik Ich weiß es nicht. Ich denke, meine aktuelle Lösung ist etwas schneller als o (2 ^ N), wenn genügend Speicher vorhanden ist, aber sie ist immer noch exponentiell. Ich habe keine direkte Formel gefunden, die den Suchvorgang vollständig überspringt, aber es könnte eine geben. I vermuten , dass die Morsefolge ist asymptotisch mit N⋅5 optimal / 6 - O (log N) läuft, aber es scheint eine Handvoll verläuft hinter dem tatsächlichen optimalen zu bleiben.
Anders Kaseorg
Interessanterweise 42/50> 5/6.
1
@Lembik Man sollte immer damit rechnen, dass asymptotische Vorhersagen manchmal von kleinen Beträgen übertroffen werden. Aber eigentlich habe ich mich völlig geirrt - ich habe eine viel bessere Sequenz gefunden, die sich N⋅ (13 - 5√5) / 2 ≈ N⋅0.90983-Läufen zu nähern scheint.
Anders Kaseorg
Sehr beeindruckend. Ich denke, die 0.90983-Vermutung ist jedoch nicht richtig. Schauen Sie sich bpaste.net/show/287821dc7214 an . Es hat Länge 1558 und hat 1445 Läufe.
2

Da es kein Rennen ist, wenn es nur ein Pferd gibt, reiche ich meine Lösung ein, obwohl es nur ein Bruchteil der Geschwindigkeit von Anders Kaseorg und ein Drittel nur so kryptisch ist. Kompilieren mit:

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

Das Herzstück meines Algorithmus ist ein einfaches Shift- und XOR-Schema:

Bildbeschreibung hier eingeben

Ein Durchlauf von Nullen im XOR-Ergebnis, der größer oder gleich der aktuellen Periode / Schicht ist, zeigt einen Durchlauf in der ursprünglichen Zeichenfolge für diese Periode an. Daran können Sie erkennen, wie lange der Lauf dauerte und wo er beginnt und endet. Der Rest des Codes ist Overhead, der die Situation einrichtet und die Ergebnisse dekodiert.

Ich hoffe, es wird nach zwei Minuten auf Lembiks Maschine mindestens 28 sein. (Ich habe eine pthread-Version geschrieben, aber nur geschafft, sie noch langsamer laufen zu lassen.)

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

Ausgabe:

> 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
quelle
Willkommen zweites Pferd! Kleine Frage, warum schlagen Sie und die andere Antwort -O2 statt -O3 vor?
@Lembik, mit -O2-Optimierung konnte ich einen Zeitunterschied beim Ausführen des Codes messen, aber ich konnte mit -O3 keinen zusätzlichen Unterschied messen. Da wir angeblich sicher gegen Geschwindigkeit eintauschen, stellte ich fest, dass die höchste Stufe, die tatsächlich einen Unterschied machte, die beste war. Wenn du glaubst, dass mein Code mit -O3 höher rangiert, dann mach mit!
CDLane
-O3soll nicht "unsicher" sein. Es aktiviert die automatische Vektorisierung, aber hier gibt es wahrscheinlich nichts zu vektorisieren. Es kann manchmal den Code verlangsamen, z. B. wenn ein branchless cmov für etwas verwendet wird, das von einem Branch sehr gut vorhergesagt worden wäre. Aber normalerweise sollte es helfen. In der Regel lohnt es sich, auch Clang zu testen, um herauszufinden, welcher von gcc oder Clang besseren Code für eine bestimmte Schleife liefert. Außerdem hilft es fast immer -march=native, -mtune=nativeeine Binärdatei zu verwenden , oder zumindest, wenn Sie noch eine Binärdatei benötigen, die überall ausgeführt wird.
Peter Cordes