String 使用类似语法的规则减少字符串
我试图找到一个合适的DP算法来简化字符串。例如,我有一个字符串String 使用类似语法的规则减少字符串,string,algorithm,language-agnostic,dynamic-programming,String,Algorithm,Language Agnostic,Dynamic Programming,我试图找到一个合适的DP算法来简化字符串。例如,我有一个字符串abab和一个规则列表 ab->b ab->c ba->a c>b 目的是使用这些规则获取可以从给定字符串接收的所有单个字符。对于本例,它将是b,c。给定字符串的长度最多可达200个符号。你能提示一个有效的算法吗 规则总是2->1。我有一个创建树的想法,根被赋予字符串,每个子元素在一次转换后都是字符串,但我不确定这是否是最好的方法。对于DP问题,您始终需要了解如何根据较小的子问题构造大问题的答案。假设您有一个函数simplify,它是
abab
和一个规则列表
ab->b
ab->c
ba->a
c>b
b,c
。给定字符串的长度最多可达200个符号。你能提示一个有效的算法吗
规则总是
2->1
。我有一个创建树的想法,根被赋予字符串,每个子元素在一次转换后都是字符串,但我不确定这是否是最好的方法。对于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'}
为了使这个速度足够快,以避免大型字符串的指数行为,您应该使用。这是解决DP问题的关键一步,否则您只是在进行蛮力计算。通过尽可能快地从函数返回,可以获得更微小的加速,因为在这一点上,您已经确定可以生成所有可能的结果
运行时间分析:对于长度
n
的输入,有2个子串长度n-1
,3个子串长度n-2
。。。n个长度为1的子字符串,用于总共O(n^2)
子问题。由于内存化,每个子问题最多调用一次函数。单个子问题的最大运行时间是O(n)
,因为i在范围内(len(s)),所以总体运行时间最多为O(n^3)
设n-给定字符串的长度和R-规则数
在最坏的情况下(输入类型为aaa…
和规则aa->a
)以自顶向下的方式扩展树会产生计算复杂性O(NR^N)
证明:
树的根有(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))=(使用)=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);
}
}
}
}
两种方法的产出:
[b, c]
如果你从右到左阅读这些规则,它们看起来完全像上下文无关语法的规则,并且具有基本相同的含义。您可以对您的数据应用自下而上的解析算法,如,以及合适的起始规则;差不多
start <- start a
| start b
| start c
start规则是否始终具有2对1映射?你的最佳解决方案是什么?最短的绳子?字母大部分相同的字符串?你需要给我们一些努力。你有没有考虑过如何处理这个问题?告诉我们你的想法。试试看。当你陷入困境时,向我们展示你所做的尝试并解释问题所在。你的意思是什么?@Bas动态编程在本质上未经修改的情况下可能不起作用?我不擅长Python,有几个问题。Doeshead=s[:i];tail=s[i::
在索引i上将字符串分成两部分,还是从字符串的两端提取i字符?是什么使得字符更新成为可能(rules.get(c1+c2,set())
?1)这就是切片表示法,s[:i]==s[0:i]
,它对应于第一个i
字符,而s[i::==s[i:len(s)]
除了第一个i
字符外,其余字符都使用。因此,对于输入abcd
,循环i
会将其分为head,tail='a','bcd'
,head,tail='ab','cd'
和head,tail='abc','d'
。2)这是一种紧凑的方式:连接字符c1
和c2
,在规则字典中查找此组合的可能答案集,最后使可能\u chars
自身和规则中可能的答案集成为可能。字典上的get(key,default)
方法在字典中查找键,如果找不到,则返回默认值。如果规则中没有两个字母的组合,则我使用此函数返回一个空集set()
,因此与该组合的并集不起任何作用。更明确的方法是,如果规则中有c1+c2,则写入:可能字符=可能字符并集(规则[c1+c2])
。为了代码的完整性,我认为最好添加memoize
的定义。或者它是从我不知道的某个Python库中获取的?我对Java不是很精通,但是您的第二个实现是否使用了记忆(即,将结果缓存为固定输入)?如果不是,它不是O(n^3),而是指数型的。谢谢,两种算法都很好地工作,但我需要一些优化方面的帮助。第二个算法很快,但我需要它快一点。也许你可以给我一些优化方面的建议,例如使用更快的数据结构或更有效的循环?@user2875945我没有实现Bas Swinckels的“speed hack”(看看他的代码),这应该有助于提高性能。
start <- start a
| start b
| start c