【问题标题】:Finding smallest substring not present in string查找字符串中不存在的最小子字符串
【发布时间】:2014-08-23 17:21:31
【问题描述】:

我有一个仅由数字 0-9 组成的字符串。字符串的长度可以在 1 到 1,000,000 个字符之间。我需要在线性时间内找到字符串中不存在的最小数字。下面是一些例子:

1023456789       //Smallest number not in string is 11
1023479          //Smallest number not in string is 5
112131405678910  //Smallest number not in string is 15

大小为 1,000,000,我认为字符串中不存在的最小数字最多只能是 6 位。

我的方法是生成从 0 到 999,999 的所有数字,并将它们全部插入一个向量中(按顺序)。然后制作一张地图,标记已经看到的字符串。然后我遍历字符串,对于每个位置,我都得到从它开始的所有子字符串,大小为 1 到 6,并在地图中将所有这些子字符串标记为真。最后,我只是一个一个地检查所有键,当我在地图中找到第一个具有错误值的键时,我将其打印出来。

这里有一些代码sn-ps:

string tmp="0";
string numbers[999999];

void increase(int pos)
{
    if(pos==-1)tmp.insert(0,"1");
    else if(tmp.at(pos)!='9')tmp.at(pos)++;
    else
    {
        tmp.at(pos)='0';
        increase(pos-1);
    }
}

//And later inside main
for(int j=0;j<999999;j++)
{
    numbers[j]=tmp;
    increase(tmp.size()-1);
}

for(int j=0;j<input.size();j++)
    {
        for(int k=0;k<6;k++)
        {
            string temp="";
            if(j+k<input.size())
            {
                temp+=input.at(j+k);
                appeared[temp]=true;
            }
        }
    }

int counter=0;
while(appeared[numbers[counter]])counter++;
cout<<numbers[counter]<<endl;

关于算法第一部分的注释。我生成一次向量,然后将其用于 100 个字符串。我需要在 4 秒内解析所有 100 个字符串。

目前这个算法对我来说太慢了。我可以优化一些代码,还是应该考虑不同的方法?

【问题讨论】:

  • @A.Andevski,您是指与字符串长度相关的线性时间还是与子字符串数量(与字符串长度相关的二次方)相关的线性时间?我不确定前者是否可行。
  • 这听起来像是来自竞赛的问题——请链接到它,以便我们知道它不是最新的。我可以想到一个算法,它在输入中的字符数加上答案的值是线性的。
  • 我很想知道您是如何得出 170,000 的。
  • 我明白了。 170,000 只是 1,000,000/6(大致四舍五入)。那可能很低。例如,假设字符串 "123456123" 包含四个 6 位数字,只有 9 个数字。可能会想出一个非常接近该压缩比的安排。我怀疑你的最大值接近 500,000。
  • @JimMischel:见 de Bruijn 序列;对于任何大小为 k 的字母表和任何整数 n,您可以构造一个大小为 k^n 的循环,其中包含所有 k^n 个 n 字符序列。展开循环会产生一个长度为 k^n+n-1 的字符串,其第一个和最后一个 n-1 个字符相同。一个 1,000,000 位的序列只有 999,995 个 6 位子序列,因此至少有 5 个 6 位子序列不存在。特别是,一个 de Bruijn 序列(剪切而不是展开)将恰好具有相同数量的唯一子序列。

标签: c++ string algorithm substring


【解决方案1】:

由于您只需要知道是否已经看到了一个数字,因此使用std::vector&lt;bool&gt; 来存储该指示可能是最简单的。当您遍历输入数字时,您在数组中将数字标记为真。完成后,遍历数组,并打印出仍然为假的第一项的索引。

【讨论】:

  • 我需要一直将每个子字符串转换为整数。这不会花我更长的时间吗?
  • 在这么小的字符串中,我想说类型转换所需的时间不是一个因素,但检查它的唯一方法是实施和执行基准测试。但我会使用数组,而不是向量。
  • @user3564091:向量通常由数组支持,vector&lt;bool&gt; 有一些(臭名昭著的)优化,可以将数据打包到更小的空间中。
  • 好的,但这次不是要把它打包到更小的空间,而是快速访问它,这两个主题通常是矛盾的,但如果我在这里遗漏了什么,请纠正我。
  • @user3564091:除非数组相当小(具体来说,小到足以放入缓存中),vector&lt;bool&gt; 主要是关于交易额外的 CPU 时间以节省内存访问时间。您可以使用大量 CPU 时间来节省少量内存访问,并且仍然领先。
【解决方案2】:

想法是构建一棵满足的数字树:

class Node {
public:
    Node() : count( 0 ) {}
    // create a tree from substring [from, to[ interval
    void build( const std::string &str, size_t from, size_t to )
    {
        Node *node = this;
        while( from != to )
            node = node->insert( str[from++] );
    }

    std::string smallestNumber(  bool root = true, int limit = 0 ) const;

 private:
    Node *insert( char c ) 
    {
        int idx = c - '0';
        if( !children[idx] ) {
            ++count;
            children[idx].reset( new Node );
        }
        return children[idx].get();
    }

    int count;
    std::unique_ptr<Node> children[10];

};

std::string Node::smallestNumber( bool root, int limit ) const
{
    std::string rez;
    if( count < 10 ) { // for this node string is one symbol length
        for( int i = 0; i < 10; ++i )
            if( !children[i] ) return std::string( 1, '0' + i );
        throw std::sruntime_error( "should not happen!" );
    }
    if( limit ) { 
        if( --limit == 1 ) return rez; // we cannot make string length 1
    }
    char digit = '0';
    for( int i = 0; i < 10; ++i ) {
        if( root && i == 0 ) continue;
        std::string tmp = children[i]->smallestNumber( false, limit );
        if( !tmp.empty() ) {
            rez = tmp;
            digit = '0' + i;
            limit = rez.length();
            if( limit == 1 ) break;
        }
    }
    return digit + rez;
}

void calculate( const std::string &str )
{
    Node root;
    for( size_t i = 0; i < str.length(); ++i ) {
        root.build( str, i, i + std::min( 6UL, str.length() - i ) );
    }
    std::cout << "smallest number is:" << root.smallestNumber() << std::endl;
}

int main()
{
    calculate( "1023456789" );
    calculate( "1023479" );
    calculate( "112131405678910" );
    return 0;
}

编辑:经过一番思考,我意识到内部循环是完全没有必要的。 1个循环就足够了。字符串长度限制为 6,我依靠 OP 估计可能的最大数量。

输出:

smallest number is:11
smallest number is:5
smallest number is:15

【讨论】:

    【解决方案3】:

    以下是我解决问题的方法。这个想法是生成一组特定长度的唯一子串,从最短的开始,然后在生成更长的子串之前测试这些子串。这允许代码不对结果的上限做出假设,并且对于具有小结果的长输入字符串也应该更快。尽管如此,在最坏的情况下,它不一定会更好。

    int find_shortest_subnumber(std::string str) {
        static int starts[10] = {
            0, 10, 100, 1000, 10000, 
            100000, 1000000, 10000000, 100000000, 1000000000
        };
        // can't find substrings longer than 9 (won't fit in int)
        int limit = std::min((int)str.size(), 9);
        for(int length = 1; length <= limit; length++) {
            std::set<std::string> uniques; // unique substrings of current length
            for(int i = 0; i <= (int)str.size() - length; i++) {
                auto start = str.begin() + i;
                uniques.emplace(start, start + length);
            }
            for(int i = starts[length - 1]; i < starts[length]; i++) {
                if(uniques.find(std::to_string(i)) == uniques.end())
                    return i;
            }
        }
        return -1; // not found (empty string or too big result)
    }
    

    我没有进行适当的复杂性分析。我用一个长度为1 028 880 字符的特定测试字符串粗略地测试了该函数,结果为190 000。在我的机器上执行大约需要 2 秒(其中包括生成的测试字符串应该可以忽略不计)。

    【讨论】:

      【解决方案4】:

      您可以在线性时间(和空间)中为字符串构造suffix tree。一旦你有了后缀树,你只需要广度优先遍历它,按字典顺序扫描每个节点的子节点,并检查每个节点的所有 10 位数字。第一个缺失的是最小缺失数字中的最后一位。

      由于一个 1,000,000 位的序列只有 999,995 个六位子序列,因此必须至少有五个六位子序列不存在,因此广度优先搜索必须不迟于第六级终止;因此,它也是线性时间。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-07-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-30
        • 1970-01-01
        相关资源
        最近更新 更多