计算给定长度字符串

时间:2016-07-17 20:11:22

标签: string algorithm math

几周前Lembik asked the following question:

  

字符串p的句点w是任意正整数pw[i]=w[i+p]   每当定义这个等式的两边时。让per(w)表示   最小句点w的大小。我们说字符串w是   周期性iff per(w) <= |w|/2

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

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

作为更多示例,abcabcaababababaabcabcabc也是定期的。

对于那些喜欢正则表达式的人,这个会检测字符串是否是周期性的:

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

任务是在较长的字符串中查找所有最大周期性子字符串。这些在文献中有时被称为 run

  

w[i,j]的子字符串w是最大周期性子字符串(运行),如果它是周期性的,既不是w[i-1] = w[i-1+p]也不是w[j+1] = w[j+1-p]。非正式地,&#34;运行&#34;不能包含在更大的&#34; run&#34;   在同一时期。

因为两次运行可以表示在整个字符串中的不同位置出现的相同字符串,所以我们将按时间间隔表示运行。以下是以间隔重复的上述定义。

  

字符串T中的运行(或最大周期性子字符串)是一个间隔   [i...j] j>=i,{<1}},

     
      
  • 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] = 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] = acacT[1,8] = aabaabaaT[9,15] = aacaaca

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

我在这里使用1索引。

目标

编写代码,以便对于从2开始的每个整数n,输出包含在任何长度为n的二进制字符串中的最大运行次数。

示例最佳

以下内容: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
  

是否有更快的方法来查找增加n值的最佳运行次数而不是天真的O(n ^ 2 2 ^ n)时间方法?

2 个答案:

答案 0 :(得分:2)

查找所有解决方案的分代算法

理念

在每个字符串中,最后一个字符只能用于有限数量的运行。

最后0只能添加一个跑步

10 + 0 => 100

因为在

00 + 0 => 000

这只是重复。如果它添加了最小运行,则下一个可能的最小运行添加是

110010 + 0 => 1100100

再次注意

010010 + 0 => 0100100

不是额外的运行,而是重复。下一个可能的补充是

111001001100100
1111001001100100111001001100100
...

数字可能会有所不同,但最小长度为

3, 7, 15, 31

4^1 - 1, 4^2 - 1, ..., 4^n - 1

在字符串开始时,不需要不同的字符,因此

maxaddlast = 4^n - 2

产生可以通过添加最后一个字符添加的最大运行次数。

算法

  • 在搜索长度n时,所有变体都以[maxNumberOfRuns - maxaddlast(n + 1),maxNumberOfRuns]中的运行计数记录。
  • 要找到maxNumberOfRuns为n + 1的解决方案,只需将所有记录的变体扩展为0和1并进行检查。

种子

剩下的问题是调整堆栈大小以收集未来种子所需的所有变种。

由于没有足够的数据来猜测有效公式,因此选择了自适应算法:

  1. n的初始堆栈大小是从n - 1
  2. 猜测的
  3. 对于每个解决方案,检查使用的堆栈大小,堆栈中始终有1个空间。
  4. 如果堆栈在某些n处被完全使用,则堆栈大小会增加,计算将在n处重新开始。
  5. 结果

    length 104 with 91 runs
    

    在600秒内达到。然后内存用完默认设置。使用-Xmx16G或更多。对于较大的数字,必须修改代码以将种子保存在磁盘上而不是内存中。

    它比蛮力方法快得多。

    **守则**

    这是我在Java中的示例代码:

    import java.io.BufferedReader;
    import java.io.FileReader;
    import java.io.FileWriter;
    import java.util.ArrayList;
    
    import de.bb.util.Pair;
    
    /**
     * A search algorithm to find all runs for increasing lengths of strings of 0s
     * and 1s.
     * 
     * This algorithm uses a seed to generate the candidates for the next search.
     * The seed contains the solutions for rho(n), rho(n) - 1, ..., minstart(n).
     * Since the seed size is unknown, it starts with a minimal seed: minstart(n) =
     * rho(n) - 1; After the solutions are calculated the all seeds are checked. If
     * a seed with minstart(n) was used, that minstart(n) gets decremented and the
     * search is restarted at position n + 1. This guarantees that the seed is
     * always large enough.
     * 
     * Optional TODO: Since the seed can occupy large amounts of memory, the seed is
     * maintained on disk.
     * 
     * @author Stefan "Bebbo" Franke (c) 2016
     */
    public class MaxNumberOfRunsAdaptive {
        private static long start;
    
        private ArrayList<Pair<byte[], ArrayList<Integer>>> seed = new ArrayList<>();
        private int max;
        private ArrayList<ArrayList<Pair<byte[], ArrayList<Integer>>>> nextSeedStack;
    
        private ArrayList<Integer> maxs = new ArrayList<>();
        private ArrayList<Integer> diffs = new ArrayList<>();
        private ArrayList<Integer> totals = new ArrayList<>();
        private int total;
    
        private byte[] buffer;
    
        public static void main(String[] args) {
            int limit = 9999;
            if (args.length == 1) {
                try {
                    limit = Integer.parseInt(args[0]);
                } catch (Exception e) {
                }
            }
            start = System.currentTimeMillis();
            new MaxNumberOfRunsAdaptive().run(limit);
            long took = (System.currentTimeMillis() - start) / 100;
            System.out.println("took " + (took / 10.) + "s");
        }
    
        /**
         * Find a string with the max number of runs for all lengths from 2 to
         * limit;
         * 
         * @param limit
         *            the limit to stop calculation.
         */
        private void run(int limit) {
            maxs.add(0);
            maxs.add(0);
            diffs.add(0);
            diffs.add(1);
            totals.add(0);
            totals.add(0);
    
            ArrayList<Integer> n0 = new ArrayList<Integer>();
            n0.add(0);
            seed.add(Pair.makePair(new byte[] { '0' }, n0));
            saveSeed(2);
    
            for (int i = 2; i <= limit;) {
                int restart = compose(i);
                if (restart < i) {
                    System.out.println("*** restarting at: " + restart + " ***");
                    i = restart;
                    loadSeed(i);
                    total = totals.get(i - 1);
                } else {
                    saveSeed(i + 1);
                    ++i;
                }
            }
        }
    
        /**
         * Load the seed for the length from disk.
         * 
         * @param length
         */
        private void loadSeed(int length) {
            try {
                seed.clear();
                final FileReader fr = new FileReader("seed-" + length + ".txt");
                final BufferedReader br = new BufferedReader(fr);
                for (String line = br.readLine(); line != null; line = br.readLine()) {
                    final int space = line.indexOf(' ');
                    final byte[] b = line.substring(0, space).getBytes();
                    final String sends = line.substring(space + 2, line.length() - 1);
                    final ArrayList<Integer> ends = new ArrayList<>();
                    for (final String s : sends.split(",")) {
                        ends.add(Integer.parseInt(s.trim()));
                    }
                    seed.add(Pair.makePair(b, ends));
                }
                fr.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * Save the seed for the given length to the disk.
         * 
         * @param length
         *            the length
         */
        private void saveSeed(int length) {
            try {
                final FileWriter fos = new FileWriter("seed-" + length + ".txt");
                for (final Pair<byte[], ArrayList<Integer>> p : seed) {
                    fos.write(new String(p.getFirst()) + " " + p.getSecond().toString() + "\n");
                }
                fos.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * Compose new strings from all available bases. Also collect the candidates
         * for the next base.
         */
        private int compose(int length) {
            max = 0;
    
            int nextStackSize;
            if (diffs.size() > length)
                nextStackSize = diffs.get(length) + 1;
            else
                nextStackSize = diffs.get(length - 1) - 1;
            if (nextStackSize < 2)
                nextStackSize = 2;
    
            // setup collector for next bases
            nextSeedStack = new ArrayList<>();
            for (int i = 0; i < nextStackSize; ++i) {
                nextSeedStack.add(new ArrayList<Pair<byte[], ArrayList<Integer>>>());
            }
    
            buffer = new byte[length];
            // extend the bases
            for (Pair<byte[], ArrayList<Integer>> e : seed) {
                final byte[] s = e.getFirst();
                System.arraycopy(s, 0, buffer, 0, length - 1);
                if (s.length < 3 || s[s.length - 1] == '1' || s[s.length - 2] == '1' || s[s.length - 3] == '1') {
                    buffer[length - 1] = '0';
                    test(length, e.getSecond());
                }
                if (s.length < 3 || s[s.length - 1] == '0' || s[s.length - 2] == '0' || s[s.length - 3] == '0') {
                    buffer[length - 1] = '1';
                    test(length, e.getSecond());
                }
            }
            long took = (System.currentTimeMillis() - start) / 100;
    
            final ArrayList<String> solutions = new ArrayList<String>();
            for (Pair<byte[], ArrayList<Integer>> p : nextSeedStack.get(nextSeedStack.size() - 1)) {
                solutions.add(new String(p.getFirst()));
            }
            total += solutions.size();
            if (totals.size() <= length)
                totals.add(0);
            totals.set(length, total);
    
            if (maxs.size() <= length) {
                maxs.add(0);
            }
            maxs.set(length, max);
    
            System.out.println(length + " " + max + " " + (took / 10.) + " " + total + " " + solutions);
    
            seed.clear();
            // setup base for next level
            for (ArrayList<Pair<byte[], ArrayList<Integer>>> t : nextSeedStack) {
                seed.addAll(t);
            }
    
            if (diffs.size() <= length) {
                diffs.add(1);
            }
            int restart = length;
            // check for restart
            for (final String b : solutions) {
                for (int i = 2; i < b.length(); ++i) {
                    int diff = maxs.get(i) - countRuns(b.substring(0, i));
                    if (diff >= diffs.get(i)) {
                        if (i < restart)
                            restart = i;
                        diffs.set(i, diff + 1);
                    }
                }
            }
            System.out.println(diffs);
    
            return restart;
        }
    
        /**
         * Test the current buffer and at it to the next seed stack,
         * 
         * @param l
         *            the current length
         * @param endRuns
         *            the end runs to store
         */
        void test(final int l, final ArrayList<Integer> endRuns) {
            final ArrayList<Integer> r = incrementalCountRuns(l, endRuns);
            final int n = r.get(r.size() - 1);
    
            // shift the nextBaseStack
            while (max < n) {
                nextSeedStack.remove(0);
                nextSeedStack.add(new ArrayList<Pair<byte[], ArrayList<Integer>>>());
                ++max;
            }
    
            // add to set in stack, if in stack
            final int index = nextSeedStack.size() - 1 - max + n;
            if (index >= 0)
                nextSeedStack.get(index).add(Pair.makePair(buffer.clone(), r));
        }
    
        /**
         * Find incremental the runs incremental.
         * 
         * @param l
         *            the lengths
         * @param endRuns
         *            the runs of length-1 ending at length -1
         * @return a new array containing the end runs plus the length
         */
        private ArrayList<Integer> incrementalCountRuns(final int l, final ArrayList<Integer> endRuns) {
            final ArrayList<Integer> res = new ArrayList<Integer>();
            int sz = endRuns.size();
            // last end run dummy - contains the run count
            int n = endRuns.get(--sz);
            int pos = 0;
    
            for (int i = l - 2; i >= 0; i -= 2) {
                int p = (l - i) / 2;
                // found something ?
                if (equals(buffer, i, buffer, i + p, p)) {
                    while (i > 0 && buffer[i - 1] == buffer[i - 1 + p]) {
                        --i;
                    }
                    int lasti = -1;
    
                    while (pos < sz) {
                        lasti = endRuns.get(pos);
                        if (lasti <= i)
                            break;
                        lasti = -1;
                        ++pos;
                    }
                    if (lasti != i)
                        ++n;
    
                    res.add(i);
                }
            }
    
            res.add(n);
            return res;
    
        }
    
        /**
         * Compares one segment of a byte array with a segment of a 2nd byte array.
         * 
         * @param a
         *            first byte array
         * @param aOff
         *            offset into first byte array
         * @param b
         *            second byte array
         * @param bOff
         *            offset into second byte array
         * @param len
         *            length of the compared segments
         * @return true if the segments are equal, otherwise false
         */
        public final static boolean equals(byte a[], int aOff, byte b[], int bOff, int len) {
            if (a == null || b == null)
                return a == b;
            while (len-- > 0)
                if (a[aOff + len] != b[bOff + len])
                    return false;
            return true;
        }
    
        /**
         * Simple slow stupid method to count the runs in a String.
         * 
         * @param s
         *            the string
         * @return the count of runs.
         */
        static int countRuns(String s) {
            int n = 0;
            int l = s.length();
            for (int i = 0; i < l - 1; ++i) {
                for (int k = i + 1; k < l; ++k) {
                    int p = 0;
                    while (i + p < k && k + p < l) {
                        if (s.charAt(i + p) != s.charAt(k + p))
                            break;
                        ++p;
                    }
                    if (i + p == k) {
                        int jj = k + p - 1;
                        if (i > 0 && s.charAt(i - 1) == s.charAt(i - 1 + p)) {
                            continue;
                        }
                        while (jj + 1 < l && s.charAt(jj + 1) == s.charAt(jj + 1 - p)) {
                            ++jj;
                            ++k;
                        }
                        ++n;
                    }
                }
            }
            return n;
        }
    }
    

答案 1 :(得分:0)

部分答案。我们的想法是从Boyer-Moore字符串搜索算法中获取一个页面,进行适当修改,以便匹配的字符串来自源字符串。

请考虑长度为n的字符串的问题,以查找句点k,其中2k < n。如果存在针对该问题的多项式时间算法,则存在针对一般问题的算法。只需为每个2 <= k <= n/2运行一次这样的算法。如果特定问题在O(p(n))p具有度d的情况下运行,则一般问题将使用度d+1的多项式运行。因此,检查具体问题就足够了。

让输入字符串为T[0 ... n-1]。这里的关键是要意识到,如果T[i] != T[i+k],则索引对(i, i+k)会对运行的存在造成障碍。当我们看到障碍物时,我们可以将问题细分为较短的输入字符串上的两个问题:T[0 ... i+k-1]T[i+1 ... n-1]。如果这些字符串中的任何一个太短,则算法不会发出任何内容并终止;这是当运行不存在时递归终止的方式。现在查找i+1i+2,......,最多i+k-1的障碍物。如果存在,则切割。另一方面,如果序列[i ... i+k-1]没有障碍物,那么我们有一个长度为2k的游程。如果我们找到一个运行,我们发现我们最大限度地扩展它(这很容易),然后将问题分成三部分:前缀,运行和后缀。我们发出了运行,现在我们有两个问题,前缀和后缀,每个都更短。要以递归方式运行此选项,请选择值为i的初始(n+k)/2

这是部分答案的原因是我遗漏了这是多项式时间算法的分析。证明不是微不足道的原因是,当你有障碍物时,长度i+kn-i-1加起来n+k-1,大于n,所以可以想象递归堆栈上的总输入长度可能呈指数增长。需要进一步的论证来证明这实际上并没有发生。