这个回文分割算法的时间复杂度是多少?

8

回文分割

给定一个字符串s,将s分割成若干个回文子串。
返回所有可能的回文分割结果。

个人认为时间复杂度为O(n^n),其中n是给定字符串的长度。

感谢Dan Roche,紧密时间复杂度= O(n* (2^n)),具体细节请参见下文。

#include <vector>
using namespace std;

class Solution {
public:
vector<vector<string>> partition(string s) {
    vector<vector<string>> list;
    vector<string> subList;

    // Input validation.
    if (s.length() <= 1) {
        subList.push_back(s);
        list.push_back(subList);
        return list;
    }

    int len = s.length();
    vector<vector<bool>> memo(len, vector<bool>(len));
    for (int i = 0; i < len; i ++) {
        for (int j = 0; j < len; j ++) {
            if (i >= j) memo[i][j] = true;
            else memo[i][j] = false;
        }
    }

    int start = 0;
    helper(s, start, list, subList, memo);

    return list;
}

void helper(string s, int start, 
            vector<vector<string>> &list, vector<string> &subList,
            vector<vector<bool>> &memo) {

    // Base case.
    if (start > s.length() - 1) {
        vector<string> one_rest(subList);
        list.push_back(one_rest);
        return;
    }

    for (int len = 1; start + len <= s.length(); len ++) {
        int end = start + len - 1;

        memo[start][end] = (len == 1) ||
                           (memo[start + 1][end - 1] && s[start] == s[end]);

        if (memo[start][end] == true) {
            // Have a try.
            subList.push_back(s.substr(start, len));

            // Do recursion.
            helper(s, end + 1, list, subList, memo);

            // Roll back.
            subList.pop_back();
        }
    }
}
};

partition函数中的算法看起来像是O(n^2),但我不确定你的helper函数是什么,因为它是递归的,我还没有在脑海中模拟它。 - Dai
谢谢你,Dai。因为有一个递归中有一个循环,这让我在脑海中运行它也很困难。 - Zhaonan
我有一个O(n^2)的解决方案,如果你想的话,我可以发布解决方案。 - akashchandrakar
2个回答

21
应该是 O(n*2^n)。你基本上要尝试每一个可能的分割方式。对于一个长度为 n 的字符串,你将有 2^(n-1) 种分割方法。这是因为,一个分割相当于在两个字符之间放置一个“|”。有 n - 1 个这样的插槽可以放置“|”。每个插槽只有两种选择——放置“|”或不放置“|”。因此,有 2^(n-1) 种方法可以放置“|”。
然后对于每个唯一的分割,你必须遍历整个字符串(在最坏的情况下,当你有重复的字符时)以确保每个分割都是回文的。所以 n * 2 ^ (n-1) = O(n*2^n)。

我认为指数偏移了1。在像aaa这样的字符串中,您可以将|(分隔符)放置在3个位置而不是2个位置,即a | aaaa | aaaa | - heretoinfinity

5
最坏情况下的运行时间是O(n * 2^n)。这当然是指数级别的,正如您所猜测的那样,但不像O(n^n)那么糟糕。
我得出O(n * 2^n)的方法是:您的顶层函数有一个O(n^2)循环来初始化备忘录,然后在整个字符串上调用helper。因此,如果我们将H(n)表示为使用(s.length()-start)等于n调用helper的成本,则您的算法的总成本将为
cost = H(n) + O(n^2)
s.length() - start等于1时,H(n)的基本情况就是复制列表的成本:
H(1) = O(n)
对于递归情况,如果if条件memo[start][end]每次都为true,则会在大小(n-1)、(n-2)、(n-3)...、2、1上进行(n-1)个递归调用。除了这些递归调用helper之外,您还必须对相同大小调用substr函数,它的成本总计为O(n^2)。因此,对于n>1,H(n)的成本总体上是
H(n) = H(n-1) + H(n-2) + ... + H(1) + O(n^2)
(我将其写成求和形式,但SO没有LaTeX支持。)
现在您可以为H(n-1)编写相同的表达式,然后替换回来简化:
H(n) = 2 H(n-1) + O(n)
这解决了
H(n) = O(n * 2^n)
由于它大于O(n^2),整个成本也是O(n * 2^n)。
注意:您可以通过在单个O(n^3)循环中预先计算所有子字符串以及memo数组来稍微改进此操作。但是,这不会改变渐近的大O界限。
实际上,O(n * 2^n)是最优的,因为在最坏的情况下,字符串是相同字符n次的重复,例如“aaaaaa”,在这种情况下,有2^n个可能的分区,每个分区大小为n,在总输出大小为Ω(n * 2^n)。

1
你是如何得出 H(n) = 2*H(n-1) + O(n) 的?是因为 (a) H(n-1) = H(n-2) + H(n-3) + ... + H(1) + O((n-1)^2);(b) O(n^2) = O(n) + O((n-1)^2),并且因此 H(n) = H(n-1) + [ H(n-2) + ... + H(1) + O((n-1)^2) ] + O(n) = 2*H(n-1) + O(n) 吗?这是正确的吗?我不确定 (b) 是否合法。 - Adama
@Adama,你是正确的,我的推理在(b)方面有些“宽松”。关键是在那里被减去的两个大O实际上是相同的函数。更加严谨地说,我们应该定义一个函数g(n)=调用大小为(n-1),(n-2)......1的substr所需的时间。然后我们有H(n)= H(n-1)+... + H(1)+ g(n),现在你可以简化为H(n)= 2 * H(n-1)+ g(n)-g(n-1)。最后,你说差异g(n)-g(n-1)是调用大小为(n-1)的substr所需的时间,这是所需的O(n) - Dan R

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接