复杂的树数据结构

时间:2013-08-06 08:57:12

标签: c# algorithm data-structures tree

我正在制作一个游戏的物品系统,我们正在制作类似于古老的古典居民邪恶游戏。目前,我正在实施项目组合,您可以将不同的项目相互组合以获得新的内容。并发症来自这样的事实:存在具有多个转换级别的项目,并且每个级别具有多个配合。请允许我澄清,让我们假设我们有绿色,红色和蓝色草药。您无法将红色+蓝色组合在一起,但是您可以将G + B与 GreenBlueHerb 或G + R结合使用以获得 GreenRedHerb ,现在如果你把这些结果中的任何一个结合到一个蓝色药草上,你就会得到一个 GreyHerb 。正如你从这个例子中看到的那样,对于绿色草本有两个转换级别,因为它达到第一级,有两个可能的配偶,(红色|蓝色),从那一点到第二级,那里'只有一个配偶(蓝色)。

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

enter image description here

有许多不同的组合,我可以先将我的项目与蓝色组合,然后是红色,然后是绿色,或绿色,然后是红色,然后是蓝色等。达到我的最终水平。 我想出了代表所有可能组合的树:

enter image description here

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

enter image description here

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

现在我的问题是,这是什么样的数据结构? - 我很确定没有内置的,所以我必须制作我自己的定制,我实际上做了,并设法让它工作,但有一个问题。 (有一点值得一提的是,我从XML文件中获取节点信息,通过信息我的意思是 itemRequired 来达到节点/级别以及它将成为什么名称我的项目在该节点,例如:绿色药草到达 RedGreenHerb 状态,它需要&#39; RedHerb ,当这种组合发生时,名称&#34; GreenHerb &#34;将更改为&#34; RedGreenHerb &#34;如果您想知道< strong> 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 为ex),树有多个级别(你在图片中看到的黑色线条),每个级别都有许多节点,每个节点都带有一个新的项目数据和一些指向的节点(子节点)。现在您可能会问在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-&gt; Level-&gt; Path(它只是一个节点) - &gt;路径数据

<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>

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

enter image description here

正如您所看到的那样,节点没有连接,持有它们的是什么级别!这意味着,目前我没有办法取出一个节点,以一种方式取出它的所有子节点。 (在没有连接节点的情况下这样做非常困难,而且会降低性能)这将我们带到:

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

每个&#39; 1&#39;表示实际项目,&#39; 0&#39; 0意思是空的。在我的树上,&#39; 001&#39; =蓝色,&#39; 010&#39; =绿色,&#39; 100&#39; =红色,&#39; 011&#39;表示绿色+蓝色,......&#39; 111&#39; =灰色(最终级别)

所以现在我解释了一切,首先:我的方法是否正确?如果不是那么是什么? 如果是这样,那么我可以使用/ make来实现这个目标的数据结构是什么?如果我提出的数据结构在他们的位置,我怎样才能将XML文件中的数据存储到我的数据结构中,将节点连接在一起,这样每当我取出一个节点时它就会取出它的子节点用它?

提前感谢您的帮助和耐心:)

编辑:有趣的是,整个系统适用于整个游戏中只出现一次的项目(获得一次)。这就是为什么每当我走一条路径时,我都会从记忆中删除它,而且每当我拿起一个项目时,我都会从数据库中删除它的条目,因为我再也不会遇到它了。

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

2 个答案:

答案 0 :(得分:2)

我不喜欢这个解决方案:

  • 简单的解决方案是最佳解决方案
  • 由于您的xml基于图表,因此难以维护。
  • 不要真正利用OOP
  • 错误来源
  • 可能使用Reflection来处理小问题(我说很小,因为如果你做这样的游戏,你会遇到更难的问题;))。这意味着不必要的复杂性。

我喜欢这个解决方案:

  • 你完全理解了这个问题。每个项目都有一个包含一些其他对象的transormations列表。现在的问题是如何代表(而不是存储)

我会做什么(juste恕我直言,你的解决方案也很好):在仅节点的观点中使用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查询等...)

答案 1 :(得分:2)

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

enter image description here

请允许我分享我的解决方案。在我以前的系统上我做了一些调整,我先说一下调整是什么,然后我为什么要制作每个调整。

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

以下是结构的外观:

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,它们都有它共同引导我们 结论:
  

每当两个节点之间由一个级别分隔时,就会出现问题   通常,上述级别的节点应指向上面的节点

所以我所做的只是遍历一个级别的所有节点,并将每个级别节点与下一级别中的每个节点进行比较,如果第一级节点有一个要求,即下一级节点具有,这是一个连接:

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);
}

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

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

为什么我要进行第二次更改? (在树中添加了一个根节点)。    好吧,你可以清楚地看到拥有根节点的好处吧    更有意义。但是我实现这个根本想法的主要原因,    是为了轻松地核实我不会接受的节点。我提到了我的    问:无论何时我采用路径/节点,同一级别的节点    应该走了,因为它,我走了一条路,没有去    背部。有了根,现在我可以很容易地说:

tree.SetRoot(nodeTakenToTransform);

无需查看我没有采取的节点并将其置空,只需将树的根更改为我所接受的节点,这样可以减轻我对待树的负担的额外好处一些排序链表,(要访问树下的节点,我必须通过根,以及我想访问的根和节点之间的内容,最后到达我的目的地) - 每次转换后root下降到一个级别,我现在需要访问的是根节点。

还有一个问题。这是我自定义字典中用于处理项目的方法,当项目转换时,它会通知&#39;字典采取必要的行动(更新其键,值等)

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 - 这就是为什么现在我必须更新那些节点要求,从新的root开始递归:

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);
    }
}

我只是说,&#34;亲爱的下一级节点,如果您需要我已经拥有的东西,请删除该要求&#34; - 那就是它:)希望你喜欢我的文章XD - 我将把我的下一个视频演示放在问题的更新中。

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

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

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