【问题标题】:What's the time complexity of this algorithm for Palindrome Partitioning?这个回文分区算法的时间复杂度是多少?
【发布时间】:2014-08-26 19:34:09
【问题描述】:

回文分区

给定一个字符串 s,分区 s 使得 partition 是回文。
返回所有可能的回文 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 是什么,因为它是递归的,我还没有试运行它在我的脑海里。
  • 谢谢戴。由于一次递归中有一个循环,我也很难在脑海中运行它。
  • 我有一个 O(n^2) 的解决方案如果你想要我可以发布解决方案。

标签: c++ algorithm big-o dynamic-programming palindrome


【解决方案1】:

应该是 O(n*2^n)。您基本上是在尝试所有可能的分区。对于长度为 n 的字符串,您将有 2^(n - 1) 种方法对其进行分区。这是因为,一个分区相当于放一个“|” b/t 两个字符。有 n - 1 个这样的插槽可以放置“|”。每个插槽只有两个选择 - 放置一个“|”或不放置“|”。因此有 2^(n - 1) 种方式来放置“|”。

然后对于每个唯一的分区,您必须遍历整个字符串(在最坏的情况下,当您有重复的字符时)以确保每个分区都是回文。所以 n * 2 ^ (n - 1) = O(n*2^n)。

【讨论】:

  • 我认为指数减1。在aaa这样的字符串中,可以将|(分隔符)放在3个地方而不是2处,即a|aa,@ 987654324@, aaa|
【解决方案2】:

最坏情况下的运行时间为 O(n * 2^n)。这当然是你所怀疑的指数,但没有 O(n^n) 那么糟糕。

这是我得到 O(n * 2^n) 的方法:您的顶级函数有一个 O(n^2) 循环来初始化备忘录,然后调用整个字符串的帮助程序。因此,如果我们将 H(n) 用于调用助手的成本,(s.length()-start) 等于 n,那么您的算法的总成本将是

成本 = H(n) + O(n^2)

H(n) 的基本情况是s.length() - start 等于 1,然后它只是复制列表的成本:

H(1) = O(n)

对于递归的情况,如果if条件memo[start][end]每次都是true,就会有(n-1)次递归调用size(n-1)、(n-2)、( n-3), ..., 2, 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)。

【讨论】:

  • 你是怎么到达H(n) = 2*H(n-1) + O(n)的?是不是(一)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 你说得对,我对 (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)
猜你喜欢
  • 2015-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多