37、数据结构与约束处理算法:二叉搜索树与音高约束引擎设计

二叉搜索树与音高约束引擎设计

数据结构与约束处理算法:二叉搜索树与音高约束引擎设计

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 应对措施

为了避免算法处理时间过长,我们可以添加各种阈值检查,例如:
- 限制解决方案或部分解决方案的数量。
- 检查处理时间,当超过时间阈值时退出算法。

以下是一个简单的阈值检查措施列表:
| 检查类型 | 作用 |
| ---- | ---- |
| 限制解决方案数量 | 避免生成过多的解决方案,节省时间和空间 |
| 限制部分解决方案数量 | 如果需要部分解决方案,可控制其数量 |
| 处理时间检查 | 当处理时间超过阈值时,停止算法,避免无限循环 |

总之,约束求解是一个复杂的问题,有多种解决方法。在实际应用中,分析输入标准可以帮助我们预测处理时间,并采取相应的措施。同时,参考历史解决方案和实践也有助于我们更好地解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值