【问题标题】:Recursive String Transformations递归字符串转换
【发布时间】:2014-08-10 10:15:52
【问题描述】:

编辑:我对使用迭代器进行了主要更改,以跟踪位和字符串中的连续位置,并通过 const ref 传递后者。现在,当我多次将样本输入复制到自己身上以测试时钟时,对于非常长的位和字符串,甚至多达 50 行样本输入,一切都在 10 秒内完成。但是,当我提交时,CodeEval 说该过程在 10 秒后中止。正如我所提到的,他们不分享他们的输入,所以现在样本输入的“扩展”工作,我不知道如何继续。任何关于额外改进以提高我的递归性能的想法将不胜感激。

注意:记忆化是一个很好的建议,但我不知道在这种情况下如何实现它,因为我不确定如何将位到字符的相关性存储在静态查找表中。我唯一想到的是将位值转换为相应的整数,但这可能会导致长位字符串的整数溢出,并且似乎计算时间太长。也非常感谢您在此处提供有关记忆的进一步建议。

这实际上是适度的 CodeEval 挑战之一。他们不共享示例输入或输出以应对中等挑战,但输出“失败错误”只是说“10 秒后中止”,所以我的代码被挂在某个地方。

任务很简单。您将文件路径作为单个命令行参数。文件的每一行将包含一系列 0 和 1 以及一系列 As 和 B,由空格分隔。你要根据以下两条规则来判断二进制序列是否可以转化为字母序列:

1) 每个 0 都可以转换为任意非空的 As 序列(例如 'A'、'AA'、'AAA' 等)

2) 每个 1 都可以转换为 As OR B 的任何非空序列(例如,'A'、'AA' 等,或 'B'、'BB' 等)(但不能混合字母)

限制是从文件中处理最多 50 行,并且二进制序列的长度在 [1,150] 中,字母序列的长度在 [1,1000] 中。

最明显的起始算法是递归地执行此操作。我想出的是对于每一位,首先折叠整个下一个允许的字符组,测试缩短的位和字符串。如果失败,一次从被杀死的角色组中添加一个角色,然后再次调用。

这是我的完整代码。为简洁起见,我删除了命令行参数错误检查。

#include <iostream>
#include <fstream>
#include <string>
#include <iterator>

using namespace std;

//typedefs
typedef string::const_iterator str_it;

//declarations
//use const ref and iterators to save time on copying and erasing
bool TransformLine(const string & bits, str_it bits_front, const string & chars, str_it chars_front);

int main(int argc, char* argv[])
{
    //check there are at least two command line arguments: binary executable and file name
    //ignore additional arguments
    if(argc < 2)
    {
        cout << "Invalid command line argument. No input file name provided." << "\n"
             << "Goodybe...";
        return -1;
    }

    //create input stream and open file
    ifstream in;
    in.open(argv[1], ios::in);
    while(!in.is_open())
    {
        char* name;
        cout << "Invalid file name. Please enter file name: ";
        cin >> name;
        in.open(name, ios::in);
    }

    //variables
    string line_bits, line_chars;

    //reserve space up to constraints to reduce resizing time later
    line_bits.reserve(150);
    line_chars.reserve(1000);

    int line = 0;

    //loop over lines (<=50 by constraint, ignore the rest)
    while((in >> line_bits >> line_chars) && (line < 50))
    {
        line++;     
        //impose bit and char constraints
        if(line_bits.length() > 150 ||
           line_chars.length() > 1000)
            continue; //skip this line

        (TransformLine(line_bits, line_bits.begin(), line_chars, line_chars.begin()) == true) ? (cout << "Yes\n") : (cout << "No\n");
    }

    //close file
    in.close();

    return 0;
}

bool TransformLine(const string & bits, str_it bits_front, const string & chars, str_it chars_front)
{
    //using iterators so store current length as local const
    //can make these const because they're not altered here
    int bits_length = distance(bits_front, bits.end());
    int chars_length = distance(chars_front, chars.end());

    //check success rule
    if(bits_length == 0 && chars_length == 0)
        return true;

    //Check fail rules:
    //1. next bit is 0 but next char is B
    //2. bits length is zero (but char is not, by previous if)
    //3. char length is zero (but bits length is not, by previous if)
    if((*bits_front == '0' && *chars_front == 'B') ||
        bits_length == 0 ||
        chars_length == 0)
        return false;

    //we now know that chars_length != 0 => chars_front != chars.end()

    //kill a bit and then call recursively with each possible reduction of front char group
    bits_length = distance(++bits_front, bits.end());

    //current char group tracker
    const char curr_char_type = *chars_front; //use const so compiler can optimize
    int curr_pos = distance(chars.begin(), chars_front); //position of current front in char string

    //since chars are 0-indexed, the following is also length of current char group
    //start searching from curr_pos and length is relative to curr_pos so subtract it!!!    
    int curr_group_length = chars.find_first_not_of(curr_char_type, curr_pos)-curr_pos;

    //make sure this isn't the last group!
    if(curr_group_length < 0 || curr_group_length > chars_length)
        curr_group_length = chars_length; //distance to end is precisely distance(chars_front, chars.end()) = chars_length

    //kill the curr_char_group
    //if curr_group_length = char_length then this will make chars_front = chars.end()
    //and this will mean that chars_length will be 0 on next recurssive call.
    chars_front += curr_group_length;
    curr_pos = distance(chars.begin(), chars_front);

    //call recursively, adding back a char from the current group until 1 less than starting point
    int added_back = 0;
    while(added_back < curr_group_length) 
    {
        if(TransformLine(bits, bits_front, chars, chars_front))
            return true;

        //insert back one char from the current group
        else
        {
            added_back++;
            chars_front--; //represents adding back one character from the group
        }

    }
    //if here then all recursive checks failed so initial must fail
    return false;
}

他们给出了以下测试用例,我的代码可以正确解决:

示例输入:

1|第1010章

2| 00 啊啊啊

3| 01001110 AAAABAAABBBBBBAAAAAAA

4| 1100110 BBAABABBA

正确的输出:

1|是的

2|是的

3|是的

4|没有

由于当且仅当它的副本存在时才能进行转换,所以我尝试将每个二进制和字母序列多次复制到自身上,并查看时钟如何运行。即使对于非常长的位和字符串以及许多行,它也可以在 10 秒内完成。

我的问题是:由于 CodeEval 仍然说它的运行时间超过 10 秒,但他们没有分享他们的输入,有没有人有任何进一步的建议来提高这种递归的性能?或者可能是完全不同的方法?

提前感谢您的帮助!

【问题讨论】:

  • 通过const 引用传递字符串以减少复制数据结构所花费的时间。
  • 您正在检查bits.length() 两次。函数调用需要时间来执行。如果您需要状态,请在第一次调用时将其复制到变量中。

标签: c++ string recursion


【解决方案1】:

这是我发现的:

通过常量引用传递
字符串和其他大型数据结构应通过常量引用传递。
这允许编译器将指针传递给原始对象,而不是制作数据结构的副本。

调用一次函数,保存结果
你打电话给bits.length() 两次。您应该调用一次并将结果保存在一个常量变量中。这允许您在不调用该函数的情况下再次检查状态。

函数调用对于时间紧迫的程序来说是昂贵的。

使用常量变量
如果您不打算在赋值后修改变量,请在声明中使用const

const char curr_char_type = chars[0];

const 允许编译器执行更高阶的优化并提供安全检查。

更改数据结构
由于您可能在字符串中间执行插入,因此您应该为字符使用不同的数据结构。 std::string 数据类型可能需要在插入后重新分配并将字母进一步向下移动。 std::list&lt;char&gt; 的插入速度更快,因为链表只交换指针。可能需要权衡,因为链表需要为每个字符动态分配内存。

在字符串中保留空间
创建目标字符串时,应使用为最大大小的字符串预分配或保留空间的构造函数。这将阻止 std::string 重新分配。重新分配的成本很高。

不要删除
你真的需要删除字符串中的字符吗? 通过使用开始和结束索引,您可以覆盖现有字母,而不必擦除整个字符串。 部分擦除是昂贵的。完全擦除不是。

如需更多帮助,请在 StackExchange 上发布代码审查。

【讨论】:

【解决方案2】:

这是一个经典的递归问题。然而,递归的幼稚实现将导致对先前计算的函数值的重新评估呈指数级。使用一个更简单的示例进行说明,比较以下两个函数在相当大的 N 下的运行时间。不用担心 int 溢出。

int RecursiveFib(int N)
{
if(N<=1)
return 1;
return RecursiveFib(N-1) + RecursiveFib(N-2);
}

int IterativeFib(int N)
{
if(N<=1)
return 1;
int a_0 = 1, a_1 = 1;
for(int i=2;i<=N;i++)
{
int temp = a_1;
a_1 += a_0;
a_0 = temp;
}
return a_1;
}

您需要在此处遵循类似的方法。有两种常见的方法来解决这个问题——动态编程和记忆。记忆是修改方法的最简单方法。下面是一个记忆化的斐波那契实现,用于说明如何加速您的实现。

int MemoFib(int N)
{
static vector<int> memo(N, -1);
if(N<=1)
return 1;
int& res = memo[N];
if(res!=-1)
return res;
return res = MemoFib(N-1) + MemoFib(N-2);
}

【讨论】:

    【解决方案3】:

    您的失败消息是“10 秒后中止”——这意味着程序运行良好,但耗时过长。这是可以理解的,因为您的递归程序对于较长的输入字符串需要成倍增加的时间——它适用于短(2-8 位)字符串,但对于 100+ 位字符串(测试允许)。要查看您的运行时间是如何增加的,您应该为自己构建一些更长的测试输入,并查看它们需要多长时间才能运行。尝试类似

    0000000011111111   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBAAAAAAAA
    00000000111111110000000011111111  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBAAAAAAAA
    

    以及更长的时间。您需要能够处理多达 150 个数字和 1000 个字母。

    【讨论】:

      【解决方案4】:

      在 CodeEval,您可以提交仅输出输入内容的“解决方案”,然后收集他们的测试集。它们可能有变化,因此您可能希望提交几次以收集更多样本。虽然其中一些太难手动解决......您可以手动解决的那些也将在 CodeEval 上运行得非常快,即使解决方案效率低下,所以要考虑这一点。

      不管怎样,我在 CodeEval 上做了同样的问题(使用 VB),我的解决方案递归地寻找 A 和 B 的“下一个索引”,这取决于我所在的“当前”索引是什么翻译(在递归方法中首先检查停止条件之后)。我没有使用记忆,但这可能有助于加快速度。

      PS,我没有运行你的代码,但是递归方法包含一个调用递归方法的while循环似乎很奇怪......因为它已经是递归的,因此应该包含所有场景,是while( ) 循环必要吗?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-27
        • 1970-01-01
        • 2015-11-27
        • 1970-01-01
        • 2011-09-27
        • 2011-03-26
        相关资源
        最近更新 更多