【问题标题】:Reduce a string using grammar-like rules使用类似语法的规则减少字符串
【发布时间】:2015-07-19 20:57:29
【问题描述】:

我正在尝试寻找一种合适的 DP 算法来简化字符串。例如我有一个字符串a b a b 和一个规则列表

  1. a b -> b
  2. a b -> c
  3. b a -> a
  4. c c -> b

目的是使用这些规则获取可以从给定字符串中接收的所有单个字符。对于此示例,它将是 b, c。给定字符串的长度最多为 200 个符号。能否请教一个有效的算法?

规则总是2 -> 1。我有一个创建树的想法,root 是字符串,每个孩子都是经过一次转换后的字符串,但我不确定这是否是最好的方法。

【问题讨论】:

  • 规则是否总是有 2 对 1 的映射?你最好的解决方案是什么?最短的字符串?具有最多相同字母的字符串?
  • 您需要向我们展示一些努力。你有没有考虑过如何解决这个问题?给我们你的想法。 尝试一下。当您遇到困难时,发帖向我们展示您尝试过的方法并解释问题所在。
  • DP是什么意思?
  • @Bas 可能是动态规划
  • CKY 不能用于这种基本上未修改的情况吗?

标签: string algorithm language-agnostic dynamic-programming


【解决方案1】:

令 N - 给定字符串的长度和 R - 规则数。

以自上而下的方式展开树会在最坏的情况下产生计算复杂度 O(NR^N)(输入字符串类型为 aaa... 和规则 aa -> a)。

证明:

树的根有 (N-1)R 个孩子,有 (N-1)R^2 个孩子,...,有 (N-1)R^N 个孩子(叶子)。因此,总复杂度为 O((N-1)R + (N-1)R^2 + ... (N-1)R^N) = O(N(1 + R^2 + ... + R^N)) = (使用binomial theorem) = O(N(R+1)^N) = O(NR^N)。

这种幼稚方法的递归 Java 实现:

public static void main(String[] args) {
    Map<String, Character[]> rules = new HashMap<String, Character[]>() {{
        put("ab", new Character[]{'b', 'c'});
        put("ba", new Character[]{'a'});
        put("cc", new Character[]{'b'});
    }};
    System.out.println(simplify("abab", rules));
}

public static Set<String> simplify(String in, Map<String, Character[]> rules) {
    Set<String> result = new HashSet<String>();
    simplify(in, rules, result);
    return result;
}

private static void simplify(String in, Map<String, Character[]> rules, Set<String> result) {
    if (in.length() == 1) {
        result.add(in);
    }
    for (int i = 0; i < in.length() - 1; i++) {
        String two = in.substring(i, i + 2);
        Character[] rep = rules.get(two);
        if (rep != null) {
            for (Character c : rep) {
                simplify(in.substring(0, i) + c + in.substring(i + 2, in.length()), rules, result);
            }
        }
    }
}

Bas Swinckels's O(RN^3) Java 实现(使用HashMap 作为记忆缓存):

public static Set<String> simplify2(final String in, Map<String, Character[]> rules) {
    Map<String, Set<String>> cache = new HashMap<String, Set<String>>();
    return simplify2(in, rules, cache);
}

private static Set<String> simplify2(final String in, Map<String, Character[]> rules, Map<String, Set<String>> cache) {
    final Set<String> cached = cache.get(in);
    if (cached != null) {
        return cached;
    }
    Set<String> ret = new HashSet<String>();
    if (in.length() == 1) {
        ret.add(in);
        return ret;
    }
    for (int i = 1; i < in.length(); i++) {
        String head = in.substring(0, i);
        String tail = in.substring(i, in.length());
        for (String c1 : simplify2(head, rules)) {
            for (String c2 : simplify2(tail, rules, cache)) {
                Character[] rep = rules.get(c1 + c2);
                if (rep != null) {
                    for (Character c : rep) {
                        ret.add(c.toString());
                    }
                }
            }
        }
    }
    cache.put(in, ret);
    return ret;
}

两种方法的输出:

[b, c]

【讨论】:

  • 我对 Java 不是很流利,但是您的第二个实现是否使用记忆化(即缓存固定输入的结果)?如果不是,则不是O(n^3),而是指数级。
  • 谢谢,这两种算法都能完美运行,但我需要一点优化方面的帮助。第二种算法很快,但我需要它快一点。您能否给我一些优化建议,例如使用更快的数据结构或更有效的循环?
  • @user2875945 我没有实现 Bas Swinckels “speed hack”(查看他的代码),这应该有助于提高性能。
【解决方案2】:

对于 DP 问题,您始终需要了解如何根据较小的子问题构建大问题的答案。假设你有你的函数simplify,它被调用,输入长度为n。有n-1 方法可以将输入分成第一部分和最后一部分。对于这些拆分中的每一个,您应该在第一部分和最后一部分递归调用 simplify 函数。长度为n 的输入的最终答案是规则允许的第一部分和最后一部分答案的所有可能组合的集合。

在 Python 中,可以这样实现:

rules = {'ab': set('bc'), 'ba': set('a'), 'cc': set('b')}
all_chars = set(c for cc in rules.values() for c in cc)

@ memoize
def simplify(s):
    if len(s) == 1:  # base case to end recursion
        return set(s)

    possible_chars = set()

    # iterate over all the possible splits of s
    for i in range(1, len(s)):
        head = s[:i]
        tail = s[i:]

        # check all possible combinations of answers of sub-problems
        for c1 in simplify(head):
            for c2 in simplify(tail):
                possible_chars.update(rules.get(c1+c2, set()))

                # speed hack
                if possible_chars == all_chars: #  won't get any bigger
                    return all_chars

    return possible_chars

快速检查:

In [53]: simplify('abab')
Out[53]: {'b', 'c'}

为了使大字符串足够快(以避免指数行为),您应该使用memoize decorator。这是解决 DP 问题的关键步骤,否则您只是在进行蛮力计算。通过尽快从函数返回possible_chars == set('abc'),可以获得进一步的微小加速,因为此时,您已经确定可以生成所有可能的结果。

运行时间分析:对于一个长度为n的输入,有2个长度为n-1的子字符串,3个长度为n-2的子字符串,……n个长度为1的子字符串,一共@987654334 @ 子问题。由于记忆化,每个子问题最多调用一次该函数。由于for i in range(len(s)),单个子问题的最大运行时间为O(n),因此整体运行时间最多为O(n^3)

【讨论】:

  • 我不擅长 Python,有几个问题。 head = s[:i]; tail = s[i:] 是在索引 i 上将字符串分成两部分还是从字符串的两端取出 i 字符?是什么让possible_chars.update(rules.get(c1+c2, set()))
  • 1) 是切片表示法,s[:i] == s[0:i],对应于第一个 i 字符,s[i:] == s[i:len(s)] 采用除第一个 i 之外的所有字符。因此,对于输入 abcdi 上的循环会将其拆分为 head, tail = 'a','bcd'head, tail = 'ab', 'cd'head, tail = 'abc', 'd'
  • 2) 这是一种简洁的方式:连接字符 c1c2,在规则字典中查找此组合的可能答案集,最后使 possible_chars 成为 @ 987654322@ 本身和规则中可能的一组答案。字典上的get(key, default) 方法在字典中查找一个键,如果找不到则返回一个默认值。我用它来返回一个空集set(),以防两个字母组合不在规则中,所以与它的联合什么都不做。
  • 一个更明确的方法是写if c1+c2 in rules: possible_chars = possible_chars.union(rules[c1+c2])
  • 为了代码的完整性,我认为添加memoize的定义很好。还是取自一些我不知道的 Python 库?
【解决方案3】:

如果你从右到左阅读这些规则,它们看起来就像上下文无关语法的规则,并且具有基本相同的含义。您可以将 Earley algorithm 之类的自下而上解析算法与合适的起始规则一起应用于您的数据;像

start <- start a
       | start b
       | start c

然后只检查解析森林以找到最短的starts 链。当然,最坏的情况仍然是 O(n^3),但如今 Earley 相当有效。

parsing with derivatives 时也可以生成解析森林。您也许可以有效地检查它们是否有 starts 的短链。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-08
    • 1970-01-01
    • 2017-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多