计算尽可能大的字符串可能的最大运行次数


24

[此问题是计算字符串的运行的后续步骤]

p字符串的周期w是任何正整数p,因此w[i]=w[i+p] 只要定义了该方程式的两边。让per(w)表示最小周期的大小w。我们说一个字符串w是周期iff per(w) <= |w|/2

因此,非正式地,定期字符串只是由重复至少一次的另一个字符串组成的字符串。唯一的麻烦是,在字符串的末尾,我们不需要重复的字符串的完整副本,只要整个字符串至少重复一次即可。

例如,考虑字符串x = abcabper(abcab) = 3作为x[1] = x[1+3] = ax[2]=x[2+3] = b并且没有更短的期限。abcab因此,该字符串不是周期性的。但是,字符串ababa是周期性的per(ababa) = 2

随着越来越多的例子abcabcaababababa并且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 = atattattT[4,5] = ttT[7,8] = ttT[1,4] = atatT[2,8] = tattatt

  • 该字符串T = aabaabaaaacaacac包含以下7分最大的周期性子(运行): , T[1,2] = aaT[4,5] = aaT[7,10] = aaaaT[12,13] = aaT[13,16] = acac,。T[1,8] = aabaabaaT[9,15] = aacaaca

  • 该字符串T = atatbatatb包含以下三个运行。它们是: T[1, 4] = atatT[6, 9] = atatT[1, 10] = atatbatatb

在这里,我正在使用1-索引。

任务

编写代码,以便对于以2开头的每个整数n,输出最大长度的任何二进制字符串中包含的运行次数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代码,您的代码应输出一个字符串及其包含的运行次数。

我的机器时间将在我的机器上运行。这是在AMD FX-8350八核处理器上的标准ubuntu安装。这也意味着我需要能够运行您的代码。

领先的答案

  • 49由Anders Kaseorg在C中撰写。单线程并以L = 12(2GB RAM)运行。
  • 27由cdlane在C中编写。


1
如果只希望我们仅考虑{0,1}-strings,请明确说明。否则字母可能是无限的,我不明白为什么您的测试用例应该是最佳的,因为似乎您也只搜索了{0,1}字符串。
瑕疵的

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。

Answers:


9

C

这将对最佳解决方案进行递归搜索,并使用字符串的未知余数可以完成的运行次数上限来对它们进行严重修剪。上限计算使用一个巨大的查找表,该表的大小由常数LL=11:0.5 GiB,L=12:2 GiB,L=13:8 GiB)控制。

在我的笔记本电脑上,这会在100秒内上升到n = 50;下一行是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

以下是所有最佳的序列Ñ ≤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-总是Ñ ≤64运行在第一数Ñ字符除以Ñ接近(13 - 5√5)/ 2≈0.90983。但是事实证明,这不是最佳比率—请参见注释。


谢谢您的回答和更正。您认为非暴力解决方案的前景如何?

1
@Lembik我不知道。我认为,如果有足够的内存,我当前的解决方案要比o(2 ^ N)快一些,但是它仍然是指数级的。我还没有找到完全跳过搜索过程的直接公式,但是可能存在。我猜想,Thue-Morse序列在N⋅5/ 6 − O(log N)游程下是渐近最优的,但似乎在实际最优值之后还有少数游程。
Anders Kaseorg '16

有趣的是42/50> 5/6。

1
@Lembik人们应该总是期望渐近预测有时会被少量击败。但是实际上我完全错了—我发现一个更好的序列似乎接近N⋅(13 −5√5)/ 2≈N⋅0.90983运行。
Anders Kaseorg '16

非常令人印象深刻。我认为0.90983的猜想是不正确的。查看bpaste.net/show/287821dc7214。它的长度为1558,行程为1445。

2

因为只有一匹马不是一场比赛,所以我提交我的解决方案,尽管它只是安德斯·卡塞格(Anders Kaseorg)速度的一小部分,而神秘主义者的速度仅为三分之一。编译:

gcc -O2运行计数.c -o运行计数

我算法的核心是一个简单的移位和XOR方案:

在此处输入图片说明

XOR结果中的零游程大于或等于当前时间段/班次表示该时间段原始字符串中的游程。从中您可以知道运行了多长时间以及运行的开始和结束位置。剩下的代码是开销,设置情况并解码结果。

我希望它在Lembik的机器上运行两分钟后至少可以达到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

欢迎第二匹马!小查询,为什么您和其他答案建议使用-O2而不是-O3?

@Lembik,通过-O2优化,我可以测量运行代码的时间差,但无法使用-O3进行任何其他测量。因为我们应该安全地权衡速度,所以我认为实际上有所作为的最高水平是最好的。如果您相信我的代码在使用-O3时排名更高,那就去吧!
cdlane

-O3并非旨在“不安全”。它支持自动矢量化,但是这里可能没有任何矢量化。有时它会使代码变慢,例如,如果它使用无分支cmov进行分支预测会很好的事情。但通常它应该有所帮助。通常也应该尝试使用clang,以查看gcc或clang哪个可以为特定循环提供更好的代码。另外,它几乎总是可以使用-march=native,或者至少-mtune=native在您仍然希望二进制文件可以在任何地方运行的情况下。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.