复杂的树数据结构

Posted

技术标签:

【中文标题】复杂的树数据结构【英文标题】:Complex tree data structure 【发布时间】:2013-08-06 08:57:11 【问题描述】:

我正在为我们正在制作的一款游戏的物品系统工作,该游戏类似于旧经典的《生化危机》游戏。目前,我正在实现项目组合,您可以将不同的项目相互组合以获得新的东西。复杂性来自这样一个事实,即存在具有多个转换级别的项目,并且每个级别都有多个配对。请允许我澄清一下,让我们假设我们有绿色、红色和蓝色的药草。你不能结合红色+蓝色,但是你可以结合 G+B 你会得到类似 GreenBlueHerb 的东西,或者 G+R 得到 GreenRedHerb,现在如果你将这些结果中的任何一个与蓝色药草结合起来,您就会得到 GreyHerb。正如你从这个例子中看到的,绿色药草有 2 级转化,为了达到第一级,有两个可能的伴侣,(红色|蓝色),从那个点到第二级,只有一个伴侣(蓝色)。

所以我想出了一个有趣的树,它涵盖了 nLevels 的所有可能性,而不仅仅是 2 个,级别越多树越复杂,看看这个 3 个级别的示例,你在中间看到的三角形代表一个项目,它周围的其他彩色形状代表它可能达到下一个级别:

有很多不同的组合,我可以先将我的物品与蓝色的组合,然后是红色,然后是绿色,或者绿色,然后是红色,然后是蓝色,等等。达到我的最终等级。 我想出了代表所有可能组合的这棵树:

(右侧的数字是#levels,左侧是每个级别的#nodes)但是正如您所看到的,它是多余的。如果您查看末端节点,它们应该都是一个,因为它们都导致相同的最终结果,即 G+R+B。这种情况实际上总共有 7 种可能的状态,这是正确的树:

这很有意义,请注意节点数量的巨大差异。

现在我的问题是,什么是正确的数据结构? - 我很确定没有内置的,所以我将不得不制作我自己的自定义一个,我实际上做了,并设法让它工作但有一个问题。 (值得一提的是,我正在从 XML 文件中获取节点信息,通过信息我的意思是 itemRequired 到达节点/级别以及我在该节点上的项目名称,例如:要使绿色药草达到 RedGreenHerb 状态,它“需要”一个 RedHerb,当这种组合发生时,名称为“GreenHerb " 将更改为 "RedGreenHerb" 如果您想知道 RedHerb 发生了什么,它会消失,我不再需要它了),这是我的数据结构:

public struct TransData

    public TransData(string transItemName, string itemRequired)
    
        this.transItemName = transItemName;
        this.itemRequired = itemRequired;
    
    public string transItemName;
    public string itemRequired;


public class TransNode

    public List<TransNode> nodes = new List<TransNode>();
    public TransData data;
    public TransNode(TransNode node): this(node.data.transItemName, node.data.itemRequired)  
    public TransNode(string itemName, string itemRequired)
    
       data = new TransData(itemName, itemRequired);
    


public class TransLevel

    public List<TransNode> nodes = new List<TransNode>();
    public TransNode NextNode  get  return nodes[cnt++ % nodes.Count];  
    int cnt;


public class TransTree
    
    public TransTree(string itemName)
    
        this.itemName = itemName;
    
    public string itemName;
    public TransNode[] nodes;
    public List <TransLevel> levels = new List<TransLevel>();
    // other stuff...

让我解释一下:TransTree 实际上是基础节点,它在开始时具有项目的名称(例如,GreenHerb),树有多个级别(黑色的线条你见图片),每个级别都有许多节点,每个节点都带有一个新的项目数据和一些要指向的节点(子节点)。现在您可能会问,在TransTree 类中放置节点列表有什么必要? - 在我向您展示如何从我的 XML 文件中获取数据后,我会回答这个问题:

public TransTree GetTransItemData(string itemName)

    var doc = new XmlDocument();
    var tree = new TransTree(itemName);
    doc.LoadXml(databasePath.text);

    var itemNode = doc.DocumentElement.ChildNodes[GetIndex(itemName)];
    int nLevels = itemNode.ChildNodes.Count;
    for (int i = 0; i < nLevels; i++) 
       var levelNode = itemNode.ChildNodes[i];
       tree.levels.Add(new TransLevel());
       int nPaths = levelNode.ChildNodes.Count;
       for (int j = 0; j < nPaths; j++) 
            var pathNode = levelNode.ChildNodes[j];
        string newName = pathNode.SelectSingleNode("NewName").InnerText;
        string itemRequired = pathNode.SelectSingleNode("ItemRequired").InnerText;
        tree.levels[i].nodes.Add(new TransNode(newName, itemRequired));
       
    
    tree.ConnectNodes(); // pretend these two
    tree.RemoveLevels(); // lines don't exist for now
    return tree;

这是一个 XML 示例,可以让一切变得清晰: ItemName->Level->Path(只不过是一个节点)->Path data

<IOUTransformableItemsDatabaseManager>
  <GreenHerb>
    <Level_0>
      <Path_0>
        <NewName>RedGreenHerb</NewName>
        <ItemRequired>RedHerb</ItemRequired>
      </Path_0>
      <Path_1>
        <NewName>BlueGreenHerb</NewName>
        <ItemRequired>BlueHerb</ItemRequired>
      </Path_1>
    </Level_0>
    <Level_1>
      <Path_0>
        <NewName>GreyHerb</NewName>
        <ItemRequired>BlueHerb</ItemRequired>
      </Path_0>
    </Level_1>
  </GreenHerb>
</IOUTransformableItemsDatabaseManager>

现在这样做的问题是,节点之间没有相互连接,那么,这意味着什么?好吧,如果一个项目通过某个路径到达某个级别,那么我们当然不需要继续存储它没有采用的其他路径,那么为什么要将它们保存在内存中呢? (没有变回,一旦你走了一条路,就是你有义务沿着那条路走,永远不要回头)我想要的,是当我取出一个节点时,它下面的所有其余节点也会下降,这是有道理的,但目前我目前的做法是这样的:

如您所见,节点未连接,控制它们的是关卡!这意味着,我目前无法以取出所有子节点的方式取出节点。 (在没有节点连接的情况下这样做非常困难,并且会大大降低性能)这让我们:

tree.ConnectNodes(); 
tree.RemoveLevels();

我先连接节点,然后删除关卡?为什么,因为如果我不这样做,那么每个节点都有两个对它的引用,一个来自其父节点,另一个来自当前级别。 现在ConnectNode 实际上是针对我展示的长树,而不是针对具有 7 个状态的优化树:

    // this is an overloaded version I use inside a for loop in ConnectNodes()
    private void ConnectNodes(int level1, int level2)
    
        int level1_nNodes = levels[level1].nodes.Count;
        int level2_nNodes = levels[level2].nodes.Count;

        // the result of the division gives us the number of nodes in level2,
        // that should connect to each node in level1. 12/4 = 3, means that each
        // node from level1 will connect to 3 nodes from level2;
        int nNdsToAtch = level2_nNodes / level1_nNodes;
        for (int i = 0, j = 0; i < level2_nNodes; j++)
        
            var level1_nextNode = levels[level1].nodes[j];
            for (int cnt = 0; cnt < nNdsToAtch; cnt++, i++)
            
                var level2_nextNode = levels[level2].nodes[i];
                level1_nextNode.nodes.Add(new TransNode(level2_nextNode));
            
        
   

这最终是我想在我的另一棵树上拥有的,但我不知道该怎么做。我想连接节点并形成我展示的第二棵树,这比 4 级相对简单,我什至无法连接绘画中的节点! (当我尝试了 4 个级别时)

如果你盯着它看,你会发现它与二进制数有些相似,这是我的二进制形式的树:

001
010
100
011
101
110
111

每个“1”代表一个实际项目,“0”表示空。从我的树中,'001'=Blue,'010'=Green,'100'=Red,'011' 表示 Green+Blue,...'111'=Grey(最终级别)

所以现在我已经解释了一切,首先:我的方法正确吗?如果不是,那是什么? 如果是这样,那么我可以使用/制作的数据结构是什么?如果我想出的数据结构在他们的位置,我如何将数据从 XML 文件存储到我的数据结构中,以便将节点连接在一起,这样每当我取出一个节点时,它就会取出它的子节点有吗?

非常感谢您的帮助和耐心:)

编辑:有趣的是,整个系统适用于在整个游戏中仅出现一次的物品(被拾取一次)。这就是为什么每当我选择一条路径时,我都会将其从内存中删除,并且每当我拿起一个项目时,我都会从数据库中删除它的条目,因为我不会再遇到它了。

编辑:请注意,我不仅仅通过字符串表示我的项目,它们还有很多其他属性。但在这种情况下,我只关心他们的名字,这就是我处理字符串的原因。

【问题讨论】:

您的问题是否与为此找到最佳数据结构或编写此问题的最佳方法有关? 那么,最好的方法不应该来自最好/最合适的数据结构吗? 这取决于您所说的数据结构。我想问的是“这是一种纯粹的理论方法”,还是“此代码必须易于维护以供未来发展”(例如真实游戏项目)? 我所说的数据结构是指将所有东西结合在一起的结构(节点如何相互通信/连接,使用的类/结构等)-“真正的游戏项目”?有多少真实是真实的?我已经在为一个“真正的”游戏项目这样做了。是的,我当然希望它是可维护的。 我不会这样称呼它>> ioublog.wordpress.com/category/gallery 【参考方案1】:

我不喜欢这个解决方案:

简单的解决方案就是最好的解决方案 难以维护,因为您的 xml 是基于图表的。 不要真正利用 OOP 错误来源 Reflection 可能用于小问题(我说小问题是因为如果你玩这样的游戏,你将面临更多困难的问题;))。这意味着不必要的复杂性。

我喜欢这个解决方案:

您已经完全理解了这个问题。每个项目都有一个与其他一些对象的转换列表。现在的问题是如何表示(而不是存储)它

我会做什么(恕我直言,您的解决方案也很好):从仅节点的角度使用 OOP。因此,如果您想将树附加到数据结构上,您的树将成为 状态机(正如您所说的路径;))。

public class InventoryObject

    protected Dictionnary<Type, InventoryObject> _combinations = new Dictionnary<Type, InventoryObject>();

    public InventoryObject()        

    public InventoryObject Combine(InventoryObject o)
    
       foreach (var c in _combinations)
          if (typeof(o) == c.Key)
            return c.Value

       throw new Exception("These objects aren't combinable");
    


public class BlueHerb : InventoryObject

    public Herb()
    
       _combinations.Add(RedHerb, new BlueRedHerb());
       _combinations.Add(GreenHerb, new BlueGreenHerb());
    


public class BlueRedHerb: InventoryObject

    public BlueRedHerb()
    
       _combinations.Add(GreenHerb, new GreyHerb());
    

然后只需调用BlueHerb.Combine(myRedHerb); 即可获得结果。也可以BlueHerb.Combine(myStone);,轻松调试。

我尽量让我的示例尽可能简单。为了点亮代码,可以做很多修改(一个类Herb,一个类CombinedHerb,使用LINQ查询等等……)

【讨论】:

实际上,你刚刚想出的正是我要处理“HealthItems”的方式——一般来说:在游戏中出现多次的项目,原因有很多,其中之一是否不需要为他们提供数据库,因为数量有限(草药、药物、注射器等)。我可以通过代码预先定义所有这些。实际上,我确实有很多 OOP 正在进行,(继承、接口等)我只是没有展示它。另外,你提到字典的部分,我也有一本字典,和你的很相似。 但是对所有“其他”类型的项目执行此操作只是大量的手动工作,我可以使用涵盖“其他”类别项目的通用解决方案。查看此线程以获取有关我的项目gamedev.stackexchange.com/questions/59884/… 的更多信息 - 我有一些想法,我会看看我是否可以回答我自己。我也会研究你的想法。 另外,我的 db 方法的好处是现在即使游戏设计师/艺术家也可以设置伙伴/关卡/路径,我已经制作了一个简单的 C# 应用程序供他交互和使用用于生成 xml 文件。 好的,现在说得通了。你知道你可以在c#中自动生成类吗(和你的xml一样)? 我无法弄清楚您的对象是如何存在的。它们只是字符串吗?因为为了维护它,在你的游戏中处理这可能会很痛苦。【参考方案2】:

好的,终于在摆弄并盯着整个系统几个小时之后,我昨天想出了一个想法,我实现了它,它运行得很好:) 我刚刚在 3 级转换和它像魔术一样工作。 (图片只显示了一个组合,但我向你保证,所有组合都有效)

请允许我分享我的解决方案。我必须对以前的系统进行一些调整,我将首先说明这些调整是什么,然后是我为什么要进行每个调整。

我更改了存储在每个节点上的数据。在我之前的方法中,我 让每个节点都依赖于前一个节点,现在,节点要求 直接来自根。

这是结构现在的样子:

public struct TransData

    public TransData(string itemName, List <string> itemsRequired)
    
        this.itemName = itemName;
        this.itemsRequired = itemsRequired;
    
    public string itemName;
    public List <string> itemsRequired;

节点构造函数:

public TransNode(string itemName, List <string> itemsRequired)

    data = new TransData(itemName, itemsRequired);

现在如何处理需求的示例:

Necklace L.1_A: Requires BlueGem
Necklace L.1_B: Requires GreenGem
Necklace L.1_C: Requires RedGem
Necklace L.2_A: Requires BlueGem AND GreenGem
Necklace L.2_B: Requires GreenGem AND RedGem
Necklace L.2_C: Requires RedGem AND BlueGem
Necklace L.3_A: Requires RedGem AND BlueGem AND GreenGem

XML 现在是:

<IOUTransformableItemsDatabaseManager>
    <Necklace>
        <Level_0>
            <Path_0>
                <NewName>RedNecklace</NewName>
                <ItemsRequired>
                    <Item_0>Red</Item_0>
                </ItemsRequired>
            </Path_0>
            <Path_1>
                        .......
            </Level_0>
        <Level_1>
            <Path_0>
                <NewName>RedGreenNecklace</NewName>
                <ItemsRequired>
                    <Item_0>Red</Item_0>
                    <Item_1>Green</Item_1>
                </ItemsRequired>
            </Path_0>
            <Path_1>
                  .....
        </Level_1>
        <Level_2>
            <Path_0>
                <NewName>RedGreenBlueNecklace</NewName>
                <ItemsRequired>
                    <Item_0>Red</Item_0>
                    <Item_1>Green</Item_1>
                    <Item_2>Blue</Item_2>
                </ItemsRequired>
            </Path_0>
        </Level_2>
    </Necklace>
</IOUTransformableItemsDatabaseManager>
我在树中添加了一个root 节点,并删除了nodes 列表 有,这更有意义。以前的 nodes 列表现在相等 到root.nodes

这是树现在的样子:

public class TransTree

    //public string itemName;
    //public List <TransNode> nodes;
    public List <TransLevel> levels = new List<TransLevel>();
    public TransNode root  private set; get; 
    public TransTree(string itemName)
    
    //  this.itemName = itemName;
        root = new TransNode(itemName, null);
    

现在,我为什么要进行第一次更改? (要件)因为这个 允许我在节点之间进行一些通信, 这反过来又让我最终以我完全正确的方式连接它们 想要(它们在我上面的 7 状态树 ^ 中的连接方式 问)如何? - 在我的项链示例中,L.1_A 之间的联系是什么 和L.2_A?答案是,L.1_A 具有 L.2_A 的要求之一, 这是 BlueGem,它们都有共同点,这导致我们 结论:

每当两个节点之间被一层隔开时 common,上一级的节点应该指向上一级

所以我所做的只是遍历一个级别中的所有节点,并将该级别中的每个节点与下一级中的每个节点进行比较,如果第一级中的节点有一个要求,则下一级中的节点有, 有联系:

public void ConnectNodes()

   for (int i = 0; i < levels.Count - 1; i++)
       ConnectNodes(i, i + 1);
   ConnectRoot();

private void ConnectNodes(int level1, int level2)

    int level1_nNodes = levels[level1].nodes.Count;
    int level2_nNodes = levels[level2].nodes.Count;
    for (int i = 0; i < level1_nNodes; i++)
    
        var node1 = levels[level1].nodes[i];
        for (int j = 0; j < level2_nNodes; j++)
        
            var node2 = levels[level2].nodes[j];
            foreach (var itemReq in node1.data.itemsRequired)
            
                if (node2.data.itemsRequired.Contains(itemReq))
                
                    node1.nodes.Add(node2);
                    break;
                
            
        
     

注意非常重要的一行:

ConnectRoot();

public void ConnectRoot()

    foreach (var node in levels[0].nodes)
       root.nodes.Add(node);

很清楚吧? - 我只是将根节点连接到第一级的节点,然后将级别1的节点连接到2,将2连接到3等。 当然,我必须在清除关卡之前这样做。这没有改变:

// fetching the data from the xml file
    tree.ConnectNodes();
    tree.RemoveLevels();

我为什么要进行第二次更改? (将根节点添加到树中)。 那么,你可以清楚地看到拥有根节点的好处,它 更有意义。但我实现这个根本想法的主要原因, 是轻松地消灭我不会采取的节点。我在我的 问:每当我采用路径/节点时,同一级别的节点 应该走了,因为就是这样,我已经走了一条路,不走 背部。有了根在手,现在我可以轻松地说:

tree.SetRoot(nodeTakenToTransform);

无需遍历我没有采用的节点并将它们清空,只需将树的根更改为我采用的节点,这有额外的好处,可以减轻我将树视为某种类型的负担链表,(要访问树下的一个节点,我必须经过根,以及根和我要访问的节点之间是什么,最后到达我的目的地) - 每次转换后,根下降一级,我现在只需要访问根节点。

还有一个问题。这是我的自定义字典中处理项目的方法,当项目转换时,它“通知”字典采取必要的操作(更新其键、值等)

public void Notify_ItemTransformed(TransTree itemTree, TransNode nodeTaken)

   var key = new Tuple<string, string>(itemTree.root.data.itemName, nodeTaken.data.itemsRequired[0]);
   itemTree.SetRoot(nodeTaken);
   itemTree.UpdateNodesRequirements(itemTree.root); // take note here
   RemoveKey(key);
   RegisterItem(itemTree);

itemTree.UpdateNodesRequirements(itemTree.root) 是做什么的? 我的第一个更改引入了一个问题。例如:当我到达 L.1_A 时,我已经拥有 BlueGem 的所有权吗?否则我不会达到这个水平。但问题是所有节点,在这个需要 BlueGem 的关卡之后,现在 仍然 需要它,即使我有它!他们不应该要求它,即。 BlueGreenNecklace 现在应该只需要一个 GreenGem - 这就是为什么现在我必须更新这些节点要求,从我的新根开始递归:

tree.SetRoot(nodeTaken);
tree.UpdateNodesRequirements(tree.root);

方法如下:

public void UpdateNodesRequirements(TransNode node)

    foreach (var n in node.nodes)
    
        if (n.data.itemsRequired.Contains(root.data.itemsRequired[0]))
            n.data.itemsRequired.Remove(root.data.itemsRequired[0]);
        if (n.nodes != null && n.nodes.Count > 0)
            UpdateNodesRequirements(n);
    

我想说的是,“亲爱的下一级节点,如果您需要我已经拥有的东西,请删除该要求” - 就是这样 :) 希望你喜欢我的文章 XD - 我将把我的下一个视频演示放在问题出炉时更新。

性能方面?嗯,有几个地方我可以做一些优化,但现在,用 3 个级别测试它真的很好,一点也不降级。我怀疑我的游戏设计师会想出需要超过 4 个关卡的东西,但即使他这样做了,我的代码也应该足够快,如果没有,我可以在适当的地方使用 yield return null 来划分不同帧上的工作(我正在使用 Unity3D)

我真正喜欢这个解决方案的地方在于它适用于所有类型的转换树,即使它不是对称的,对于 nPaths 和 nLevels :)

感谢任何试图提供帮助并花时间阅读本文的人。 -- 烦恼

【讨论】:

以上是关于复杂的树数据结构的主要内容,如果未能解决你的问题,请参考以下文章

《数据结构与算法之美》22——递归树

使用特定方法拆分二叉树

Splay(伸展树分裂树):平衡二叉搜索树中功能最丰富的树

R语言使用rpart包构建决策树模型选择合适的树大小(复杂度)检查决策树对象的cptable内容(树的大小由分裂次数定义预测误差)使用plotcp函数可视化决策树复杂度参数与交叉验证错误的关系

数据结构--树

转载:数据结构 二项队列