数据结构与约束处理算法:二叉搜索树与音高约束引擎设计
1. 二叉搜索树
1.1 二叉搜索树简介
二叉搜索树是搜索树的一种,它由带有键值(可能还有其他相关值)的节点组成,这些节点以树状结构相互连接。在二叉搜索树中,每个节点最多有两个子节点。节点添加到树中的方式遵循以下原则:对于给定的节点,键值较小的节点在其左侧,键值较大的节点在其右侧,相同键值的节点可以在任意一侧。
1.2 搜索算法
搜索算法通过遍历树节点并进行键比较,尝试找到与搜索值匹配的节点。当二叉搜索树构建正确时,搜索速度非常快,平均搜索时间复杂度为 $O(log_2(n))$,其中 $n$ 是树中节点的数量。不过,在特殊情况下,比如树是由 $n$ 个节点组成的序列时,搜索时间复杂度会变为 $O(n)$。
以下是搜索算法的伪代码:
search(key):
x = root
while x and key != x.key:
x = x.left if key < x.key else x.right
return x
1.3 二叉搜索树的优势
虽然哈希表的访问时间复杂度为 $O(1)$,但二叉搜索树具有两个关键优势:
- 通常占用更少的空间。
- 节点值是自然排序的,可以通过简单的树遍历进行检索。
1.4 二叉搜索树示例
下面是一个二叉树搜索的示例图:
graph TD;
A(107) --> B(128);
A --> C(90);
B --> D(115);
B --> E(130);
D --> F(120);
F --> G(121);
在这个示例中,搜索值为 121 的节点时,会依次访问键值为 107、128、115、120、121 的节点。
2. 音高约束引擎设计
2.1 问题背景
我们从一个 PMap 开始,其源包含某段旋律的所有音符/参与者,目标是一组上下文音符。每个上下文音符都有一个上下文策略,包括目标和声上下文、音高范围,但没有分配的音符。我们还有一组约束或策略,每个策略都派生自 AbstractConstraint 并基于 PMap 源参与者的一个子集进行参数化。我们的目标是找到一个解决方案,使每个 PMap 的上下文音符都获得一个分配的音符,并且所有 PMap 的参与者都映射到其分配的音符满足所有策略的上下文音符。
2.2 约束引擎
寻找这些解决方案的过程称为约束引擎。我们采用的技术方法是细化模型,其中约束引擎作为一个搜索引擎,在可能的变量设置空间中寻找解决方案。
2.3 两种直接解决方案
2.3.1 深度优先搜索
深度优先搜索是一种“垂直”搜索算法,带有回溯机制。其基本思想是先收集起始 pmap 中未分配的参与者或节点集合,然后从第一个节点开始,遍历其可以分配的音高值,每次分配后移动到下一个未分配的节点并分配一个有效的音高值,以此类推。
以下是深度优先搜索算法的伪代码:
solve(p_map, policies):
partial_results = []
unsolved_nodes = all p_map unsolved actors, sorted on number solution values
node_stack # over all unsolved nodes
value_map # for tracking value loop indices over nodes
while node != None:
if node in value_map:
values = value_map[node]
else:
value_map[node] = values = all values that node can take
v = values.next()
if v is None:
remove node from value_map
p_map[node].note = None
node = node_stack.prev()
else:
p_map[node].note = v
if p_map is full:
add p_map to full_results
else:
node = node_stack.next()
return full_results, partial_results
该算法性能较慢的主要原因有两个:
- 运行时具有乘法性质,需要遍历每个节点的所有可能值,导致运行时间很长,尤其是在约束较少时,节点的音高集合基数可能很高。
- 遍历过程中不断重新计算音高集合,尽管值栈有助于保存迭代上下文,但重新计算会抵消其带来的性能提升。
2.3.2 广度优先搜索和链
广度优先搜索是一种“水平”搜索算法。它从 pmap 中未分配的参与者或节点集合开始,为每个未分配的节点生成所有可能值的 pmap 实例,然后将这些实例作为输入,继续处理下一个未分配的节点。
为了改进该算法,我们引入了“链”的概念。对于给定的未分配参与者,该参与者参与一组约束,这些约束中所有其他未分配参与者的并集称为该参与者的链,这些未分配参与者称为该参与者的“同行”。
以下是广度优先搜索算法的可视化流程:
graph LR;
A(actor n) --> B(pmap1);
A --> C(pmap2);
A --> D(pmap3);
B --> E(peer1 - pmap1);
B --> F(peer1 - pmap2);
C --> G(peer1 - pmap3);
E --> H(peer2 - pmap1);
F --> I(peer2 - pmap2);
G --> J(peer2 - pmap3);
该算法性能不佳的原因主要是:
- 会生成大量的 pmap 副本,占用时间和空间。
- 值集合的基数没有像预期那样被修剪,尤其是在约束较少时,值集合往往较大。
2.4 约束解决方案算法的改进
2.4.1 结合深度优先搜索和同行链技术
为了改进之前的约束求解算法,我们结合了深度优先搜索和同行链技术。这种方法更积极地递归处理同行,尽可能地为同行分配音高值。由于每个同行的策略集可能不同但会有重叠,通过为更多的同行分配音高值,算法可以不断为更多的策略提供部分解决方案,从而减少其他同行的音高值候选集的基数,进而减少搜索量和处理时间,产生类似连锁反应的效果。
以下是填充链的算法伪代码:
fill_chain(p_map, v_note):
results = {}
peers = all of v_note’s peers relative to p_map
v_note_values = all possible values for v_note
for value in v_note_values:
p_map[v_note].note = value
v_note_p_map_set = {p_map}
for peer in peers:
peer_results = {}
peer_values = possible values for peer given p_map
if len(peer_values) == 0:
v_note_p_map_set = {}
break
for pmap in v_note_p_map_set:
for pm_v in peer_values:
pmap[peer].note = pm_v
if pmap is full:
add pmap to full results
else:
add pmap to peer_results
v_note_p_map_set = peer_results
results = results.union(v_note_p_map_set)
return results
2.4.2 改进算法的可视化与伪代码
改进算法的可视化解释如下:
graph LR;
A(Original pmap - unassigned actor n) --> B(visit(n));
B --> C(Loop over values of n);
C --> D(For each peer of n);
D --> E(Recursive visit(peer));
E --> F(Generate pmaps);
F --> G(Incorporate results);
以下是
visit()
方法的伪代码:
visit(p_map, v_note):
results = {}
values = All values for v_note
for value in values:
p_map[v_note].note = value
value_results = {}
peers = v_note's peers
if peers is None:
if p_map is full and valid:
add p_map to full_results
else:
add p_map to value_results
else:
for peer in peers:
if first_time:
value_results = visit(p_map, peer)
else:
for r in value_results:
r_results = visit(r, peer)
value_results = value_results + r_results
results = results + value_results
p_map[v_note].note = None
return results
2.4.3 改进算法的性能分析
虽然这种改进算法看起来性能更优,但并不能保证在所有情况下都如此。在某些条件下,如策略较少、未分配节点过多或策略组碎片化,算法的处理时间或内存使用可能会急剧增加,难以对搜索空间进行有效修剪。
2.4.4 应对措施
为了避免算法处理时间过长,我们可以添加各种阈值检查,例如:
- 限制解决方案或部分解决方案的数量。
- 检查处理时间,当超过时间阈值时退出算法。
以下是一个简单的阈值检查措施列表:
| 检查类型 | 作用 |
| ---- | ---- |
| 限制解决方案数量 | 避免生成过多的解决方案,节省时间和空间 |
| 限制部分解决方案数量 | 如果需要部分解决方案,可控制其数量 |
| 处理时间检查 | 当处理时间超过阈值时,停止算法,避免无限循环 |
总之,约束求解是一个复杂的问题,有多种解决方法。在实际应用中,分析输入标准可以帮助我们预测处理时间,并采取相应的措施。同时,参考历史解决方案和实践也有助于我们更好地解决问题。
二叉搜索树与音高约束引擎设计
超级会员免费看
27

被折叠的 条评论
为什么被折叠?



