计算以最小移动量排列块的最佳解决方案

时间:2018-07-11 03:30:27

标签: algorithm optimization

从一个简单的问题开始,就变成了挑战。现在,我快要被它击败了。帮助吗?

它非常简单。画一个像这样的班级:

class Unit
{
    // Where we are
    public int CurrentPos;

    // How long we are
    public int Length;

    // Where we belong
    public int TargetPos;
}

现在,假设您有成千上万个这样的(静态)集合(例如this)。目标是将事物从其CurrentPos移到其TargetPos。要注意的是,有时TargetPos已有东西(或部分重叠)。在这种情况下,“某物”(或某物)将需要先移开。

由于“移动”是昂贵的操作,因此最佳解决方案只能将单元移动一次(从其当前位置到其目标位置)。因此,我首先将其TargetPos已释放的Units移开,然后将其移动到通过第一步释放的空间中,等等。

但是最终我遇到了真正的挑战。最简单的说:我试图移动A,但是B妨碍了,所以我试图移动B,但是C阻碍了,所以我尝试了C,但是A妨碍了。 A-> B-> C-> A。

对于喜欢数字的人:

+------------+--------+-----------+
| CurrentPos | Length | TargetPos |
+------------+--------+-----------+
|        100 |     10 |       110 | A
|        110 |      5 |       120 | B
|        120 |     10 |       100 | C
+------------+--------+-----------+

我想将Pos 100处的10移到110,但是已经有东西了。因此,我尝试将5从110移至120,但是那里已经有些东西了。最后,我尝试将10从120移到100,但是,等一下,这是一个循环!

要“打破”循环,我可以选择所有这些条目并将其移开。但是,选择“ 5”可使我最小化“移开方式”移动的大小(与“移至TargetPos”移动相反)。一旦路途通畅,“移开”的物品仍必须再次移动到自己的目标。

很明显,我要最小化的不是“移开方式”的 number ,而是 size 。移动四个长度为2的单位比移动一个长度为10的单位更好。

逻辑告诉我,对于给定的数据集,无论有多大,都必须有1个最佳解决方案。这是打破所有循环所需的绝对最小“移动长度”。诀窍是,如何找到它?

让我们直接看一下代码,而不是列出很难解决的所有原因。我创建了一个简单的框架(用c#编写),使我可以尝试各种策略而不必从头开始编写每个代码。

首先是我对Unit的实现:

class Unit : IComparable<int>
{
    /// <summary>
    /// Where we are
    /// </summary>
    public int CurrentPos;

    /// <summary>
    /// How long we are
    /// </summary>
    public readonly int Length;

    /// <summary>
    /// Where we belong
    /// </summary>
    public readonly int TargetPos;

    /// <summary>
    /// Units who are blocking me
    /// </summary>
    public List<Unit> WhoIsBlockingMe;

    public Unit(int c, int l, int t)
    {
        CurrentPos = c;
        Length = l;
        TargetPos = t;

        WhoIsBlockingMe = null;
    }

    /// <summary>
    /// Indicate that a child is no longer to be considered blocking.
    /// </summary>
    /// <param name="rb">The child to remove</param>
    /// <returns>How many Units are still blocking us.</returns>
    public int UnChild(Unit rb)
    {
        bool b = WhoIsBlockingMe.Remove(rb);
        Debug.Assert(b);

        return WhoIsBlockingMe.Count;
    }

    public override string ToString()
    {
        return string.Format("C:{0} L:{1} T:{2}", CurrentPos, Length, TargetPos);
    }

    public override int GetHashCode()
    {
        return TargetPos.GetHashCode();
    }

    /// <summary>
    /// Used by BinarySearch
    /// </summary>
    /// <param name="other">CurrentPos being sought.</param>
    /// <returns></returns>
    public int CompareTo(int other)
    {
        return CurrentPos.CompareTo(other);
    }
}

大部分是您所期望的。可能值得强调WhoIsBlockingMe。这是当前阻止该单元移动到其所需TargetPos的其他单元的列表。它由框架自动填充和维护。

这是框架:

abstract class FindOpt
{
    #region Members

    protected static readonly string DataDir = @"c:\vss\findopt2\data\"; // <--------- Adjust to suit!!!

    /// <summary>
    /// The Pos where I move Units "out of the way" (see MoveOut).
    /// </summary>
    private int m_RunningLast;

    /// <summary>
    /// Count of MoveOuts executed.
    /// </summary>
    private int m_Moves;

    /// <summary>
    /// The total size of MoveOuts executed.  This is what I'm trying to minimize.
    /// </summary>
    private int m_MoveSize;

    /// <summary>
    ///  The complete list of Units read from export.tab.
    /// </summary>
    protected readonly List<Unit> m_Units;

    /// <summary>
    /// A collection to keep track of who would get freed by moving a particular unit.
    /// </summary>
    protected readonly Dictionary<Unit, List<Unit>> m_Tree;

    /// <summary>
    /// Units freed (possibly due to cascading) waiting to be MoveDown.
    /// </summary>
    protected readonly Queue<Unit> m_ZeroChildren;

    /// <summary>
    /// Is m_Units currently sorted properly so BinarySearch will work?
    /// </summary>
    private bool UnitsOutOfDate;

    #endregion

    public FindOpt()
    {
        m_RunningLast = int.MaxValue;
        m_Moves = 0;
        m_MoveSize = 0;

        m_Units = new List<Unit>();
        m_Tree = new Dictionary<Unit, List<Unit>>();
        m_ZeroChildren = new Queue<Unit>();
        UnitsOutOfDate = true;

        // Load the Units
        using (StreamReader sr = new StreamReader(DataDir + @"export.tab"))
        {
            string s;

            while ((s = sr.ReadLine()) != null)
            {
                string[] sa = s.Split('\t');

                int c = int.Parse(sa[0]);
                int l = int.Parse(sa[1]);
                int t = int.Parse(sa[2]);

                Unit u = new Unit(c, l, t);
                m_Units.Add(u);
            }
        }
    }
    public int CalcBest()
    {
        // Build the dependency tree.
        BuildTree();

        // Process anything that got added to m_ZeroChildren Queue while 
        // building the tree.
        ProcessQueue();

        // Perform any one time initialization subclasses might require.
        Initialize();

        // Keep looping until no Units are blocking anything.
        while (m_Tree.Count > 0)
        {
            // Pick a Unit to MoveOut.  
            Unit rb = PickVictim();

            // Subclass gave up (or is broken)
            if (rb == null)
                return int.MaxValue;

            // When the Unit gets MoveOut, any items in
            // m_Tree that were (solely) blocked by it will get
            // added to the queue.
            WhackVictim(rb);

            // Process any additional Units freed by WhackVictim
            ProcessQueue();
        }

        Console.WriteLine("{0} Moves: {1}/{2}", this.GetType().Name, m_Moves, m_MoveSize);

        return m_MoveSize;
    }

    // Intended to be overridden by child class
    protected virtual void Initialize()
    {
    }
    // Intended to be overridden by child class
    protected abstract Unit PickVictim();

    // Called by BinarySearch to re-sort m_Units as 
    // needed.  Both MoveOut and MoveDown can trigger this.
    private void CheckUnits()
    {
        if (UnitsOutOfDate)
        {
            m_Units.Sort(delegate (Unit a, Unit b)
            {
                return a.CurrentPos.CompareTo(b.CurrentPos);
            });
            UnitsOutOfDate = false;
        }
    }
    protected int BinarySearch(int value)
    {
        CheckUnits();

        int lower = 0;
        int upper = m_Units.Count - 1;

        while (lower <= upper)
        {
            int adjustedIndex = lower + ((upper - lower) >> 1);

            Unit rb = m_Units[adjustedIndex];
            int comparison = rb.CompareTo(value);
            if (comparison == 0)
                return adjustedIndex;
            else if (comparison < 0)
                lower = adjustedIndex + 1;
            else
                upper = adjustedIndex - 1;
        }

        return ~lower;
    }
    // Figure out who all is blocking someone from moving to their
    // TargetPos.  Null means no one.
    protected List<Unit> WhoIsBlockingMe(int pos, int len)
    {
        List<Unit> ret = null;

        int a1 = BinarySearch(pos);

        if (a1 < 0)
        {
            a1 = ~a1;

            if (a1 > 0)
            {
                Unit prev = m_Units[a1 - 1];
                if (prev.CurrentPos + prev.Length > pos)
                {
                    ret = new List<Unit>(2);
                    ret.Add(prev);
                }
            }
        }

        int endpoint = pos + len;
        while (a1 < m_Units.Count)
        {
            Unit cur = m_Units[a1];

            if (cur.CurrentPos < endpoint)
            {
                if (ret == null)
                    ret = new List<Unit>(2);
                ret.Add(cur);
            }
            else
            {
                break;
            }

            a1++;
        }

        return ret;
    }
    // Move a Unit "Out of the way."  This is the one we are
    // trying to avoid.  And if we *must*, we still want to
    // pick the ones with the smallest rb.Length.
    protected void MoveOut(Unit rb)
    {
        // By definition: Units that have been "MovedOut" can't be blocking anyone.
        // Should never need to do this to a Unit more than once.
        Debug.Assert(rb.CurrentPos < m_RunningLast, "Calling MoveOut on something that was already moved out");

        // By definition: Something at its target can't be blocking anything and 
        // doesn't need to be "MovedOut."
        Debug.Assert(rb.CurrentPos != rb.TargetPos, "Moving from TargetPos to Out");

        m_Moves++;
        m_MoveSize += rb.Length;

        m_RunningLast -= rb.Length;
        rb.CurrentPos = m_RunningLast;
        UnitsOutOfDate = true;
    }
    // This is the "good" move that every Unit will eventually
    // execute, moving it from CurrentPos to TargetPos.  Units
    // that have been "MovedOut" will still need to be moved
    // again using this method to their final destination.
    protected void MoveDown(Unit rb)
    {
        rb.CurrentPos = rb.TargetPos;
        UnitsOutOfDate = true;
    }
    // child of rb has been moved, either out or down.  If
    // this was rb's last child, it's free to be MovedDown.
    protected void UnChild(Unit rb, Unit child)
    {
        if (rb.UnChild(child) == 0)
            m_ZeroChildren.Enqueue(rb);
    }
    // rb is being moved (either MoveOut or MoveDown).  This
    // means that all of the things that it was blocking now
    // have one fewer thing blocking them.
    protected void FreeParents(Unit rb)
    {
        List<Unit> list;

        // Note that a Unit might not be blocking anyone, and so
        // would not be in the tree.
        if (m_Tree.TryGetValue(rb, out list))
        {
            m_Tree.Remove(rb);

            foreach (Unit rb2 in list)
            {
                // Note that if rb was the last thing blocking rb2, rb2
                // will get added to the ZeroChildren queue for MoveDown.
                UnChild(rb2, rb);
            }
        }
    }
    protected void ProcessQueue()
    {
        // Note that FreeParents can add more entries to the queue.
        while (m_ZeroChildren.Count > 0)
        {
            Unit rb = m_ZeroChildren.Dequeue();

            FreeParents(rb);
            MoveDown(rb);
        }
    }
    protected bool IsMovedOut(Unit rb)
    {
        return (rb == null) || (rb.CurrentPos >= m_RunningLast) || (rb.CurrentPos == rb.TargetPos);
    }
    private void BuildTree()
    {
        // Builds m_Tree (Dictionary<Unit, List<Units>)
        // When the Unit in the Key in is moved (either MoveOut or MoveDown), each of 
        // the Values has one less thing blocking them.

        // Victims handles the special case of Units blocking themselves.  By definition,
        // no moving of other units can free this, so it must be a MoveOut.
        List<Unit> victims = new List<Unit>();

        foreach (Unit rb in m_Units)
        {
            rb.WhoIsBlockingMe = WhoIsBlockingMe(rb.TargetPos, rb.Length);
            if (rb.WhoIsBlockingMe == null)
            {
                m_ZeroChildren.Enqueue(rb);
            }
            else
            {
                // Is one of the things blocking me myself?
                if (rb.WhoIsBlockingMe.Contains(rb))
                {
                    victims.Add(rb);
                }

                // Add each of my children to the appropriate node in m_Tree, indicating
                // they are blocking me.
                foreach (Unit rb2 in rb.WhoIsBlockingMe)
                {
                    List<Unit> list;
                    if (!m_Tree.TryGetValue(rb2, out list))
                    {
                        // Node doesn't exist yet.
                        list = new List<Unit>(1);
                        m_Tree.Add(rb2, list);
                    }
                    list.Add(rb);
                }
            }
        }

        foreach (Unit rb in victims)
        {
            WhackVictim(rb);
        }
    }
    // Take the "Victim" proposed by a subclass's PickVictim
    // and MoveOut it.  This might cause other items to get added
    // to the ZeroChildren queue (generally a good thing).
    private void WhackVictim(Unit rb)
    {
        FreeParents(rb);
        MoveOut(rb);
    }
}

这里值得强调的地方:

  • DataDir控制在何处读取数据。将其调整为指向您下载(并提取)export.tab的位置。
  • 期望的是子班将选择移出哪个单位(又名“受害者”)。完成后,框架将对其进行移动,并随移动的所有单元一起释放。
  • 您可能还需要注意m_Tree。如果我有单位,则可以使用此词典查找被该单位阻止的所有人(与Unit.WhoIsBlockingMe相反)。

这是使用框架的简单类。其目的是告诉框架接下来应移出哪个单元。在这种情况下,它只是提供最大长度的受害者,一直到受害者。最终它将成功,因为它将继续提供单位,直到没有剩余为止。

class LargestSize : FindOpt
{
    /// <summary>
    /// The list of Units left that are blocking someone.
    /// </summary>
    protected readonly List<Unit> m_AltTree;
    private int m_Index;
    public LargestSize()
    {
        m_AltTree = new List<Unit>();
        m_Index = 0;
    }
    protected override void Initialize()
    {
        m_AltTree.Capacity = m_Tree.Keys.Count;

        // m_Tree.Keys is the complete list of Units that are blocking someone.
        foreach (Unit rb in m_Tree.Keys)
            m_AltTree.Add(rb);

        // Process the largest Units first.
        m_AltTree.Sort(delegate (Unit a, Unit b)
        {
            return b.Length.CompareTo(a.Length);
        });
    }
    protected override Unit PickVictim()
    {
        Unit rb = null;

        for (; m_Index < m_AltTree.Count; m_Index++)
        {
            rb = m_AltTree[m_Index];
            if (!IsMovedOut(rb))
            {
                m_Index++;
                break;
            }
        }

        return rb;
    }
}

没有什么奇怪的。也许值得注意的是,移动一个单元通常也会允许其他单元也移动(这是打破循环的关键)。在这种情况下,此代码使用IsMovedOut查看计划提供的下一个受害者是否已移至其TargetPos。如果是这样,我们跳过那个,转到下一个。

您可能会想到,LargestSize在最小化MoveOut的尺寸(移动总共近1200万个“长度”)方面做得非常糟糕。尽管它在最小化动作的数量方面做得很好(895),但这不是我要的。速度也不错(〜1秒)。

最大尺寸移动:895 / 11,949,281

可以使用类似的例程从最小的例程开始,然后逐步提高。这样就可以进行更多的移动(这很有趣,但并不是很重要),并且可以使移动幅度更小( )(这是一件好事):

最小尺寸移动:157013 / 2,987,687

正如我所提到的,我还有其他一些,有些更好(只有294个动作),还有一些更差。但是,到目前为止,我最大的移动 sizes 是1,974,831。这样好吗坏?好吧,我碰巧知道有一个解决方案需要少于340,000,所以...非常糟糕。

出于完整性考虑,以下是调用所有代码的代码:

class Program
{
    static void Main(string[] args)
    {
        FindOpt f1 = new LargestSize();
        f1.CalcBest();
    }
}

将这4块缝合在一起,您便拥有了完整的测试装置。要测试您自己的方法,只需修改子类中的PickVictim以返回您对下一个单位应移出的最佳猜测。

那么,我的目标是什么?

我正试图找到一种方法来计算MoveOut的“最佳”设置,以打破每个循环,其中最佳意味着最小的总长度。我不仅对这个样本集的答案感兴趣,还试图创建一种在合理的时间内(以秒为单位,而不是几天)为任何数据集找到最佳结果的方法。因此,遍历具有数十万条记录的数据集的所有可能排列(您可以说206858!?)可能不是我所追求的解决方案。

我不能完全确定要从哪里开始?我应该选择这个单位吗?或者那个一个?由于几乎每个单元都处于循环中,因此可以通过移动其他单元来释放每个单元。因此,给定2个单位,您如何确定哪个将导致最佳解决方案?

  • 最小和最大显然不会帮助您,至少不是一个人。
  • 希望了解哪些单位可以释放最多的父母?试过了。
  • 想看看谁释放了最大的父母?也尝试过。
  • 如何列出所有循环?然后,您可以选择循环中最小的一个。您甚至可以弄清楚哪些单元涉及多个循环。移动长度为10的单元可以打断10个不同的循环,这似乎比长度为5的单元只能打断1个循环更好。事实证明,这比您想象的要难得多。有很多循环(超出了我64gig RAM的容量)。不过,我目前的“最佳”仍然是这条路。当然,我目前最好的臭味...

还有什么值得一提的?

  • 这段代码是用c#编写的,但这只是因为我发现在此处进行原型制作更容易。由于我追求的是算法,因此请随时以自己喜欢的语言编写。示例数据集只是制表符分隔的文本,使用框架完全是可选的。如果您的解决方案是用我看不懂的东西写的,我会问问题。
  • 请记住,目标并非(只是)为数据集找出最佳解决方案。我想要一种有效的方法来计算任何数据集的最佳结果。
  • 不要使用线程/ GPU /等来加快处理速度。再次:寻找一种有效的算法。没有这个,您做什么也没关系。
  • 假设所有长度,CurrentPos和TargetPos均大于0。
  • 假设TargetPos +长度不会彼此重叠。
  • 不允许将单元拆分成更短的长度。
  • 如果您错过了上面的链接,则示例数据集为here。请注意,为了减小下载大小,我省略了未被循环阻止的(〜300,000)单位。该框架已经可以处理它们,因此它们只是分散注意力。
  • 有1个单位被自身挡住(旧型号为900)。我将其留在数据集中,但框架已经对其进行了显式处理。
  • 有些单元并没有阻塞任何东西,但由于某人正在阻止它们而仍然无法移动(即它们位于m_Units中,并且在其WhoIsBlockingMe中具有值,但不在m_Tree.Keys中,因为移动它们会赢释放其他任何东西)。不确定如何处理此信息。先将它们移动?持续?看不出有什么帮助,但确实有帮助。
  • 进行一些分析,我发现该数据集中206,858个单位中大约1/3的长度为1。实际上,2/3的长度为8或更短。它们中只有3个非常大(即大于当前已知的最佳解决方案)。先移动它们持续?也不太确定该信息怎么办。

StackOverflow是解决此问题的最佳场所吗?该代码“断”了,因为它没有给我我想要的结果。我听说过CodeGolf,但从未去过那里。由于这只是测试工具而不是生产代码,因此CodeReview似乎不合适。


编辑1:响应@ user58697的评论,这是一个循环,其中一个单元(A)阻止了另外10个单元。为了达到良好的效果,我将其循环:

+------------+--------+-----------+
| CurrentPos | Length | TargetPos |
+------------+--------+-----------+
|        100 |     10 |      1000 | A
|        120 |     20 |        81 | B
|        140 |      1 |       101 | C
|        141 |      1 |       102 | D
|        142 |      1 |       103 | E
|        143 |      1 |       104 | F
|        144 |      1 |       105 | G
|        145 |      1 |       106 | H
|        146 |      1 |       107 | I
|       1003 |      1 |       108 | J
|        148 |     50 |       109 | K
+------------+--------+-----------+

在这里,我们看到B被A阻塞(B的最后一位与A的第一位重叠)。同样,A的最后一位阻塞了K的第一位。C-J显然也被阻塞了。因此,A不仅阻塞了多个块,而且其阻塞长度总计为78,即使它本身仅是长度10也是如此。当然,A本身也会被J阻止。


编辑2:只是快速更新。

我的样本数据的原始大小刚刚超过500,000个单位。处理简单的情况(TargetPos已经免费,等等),我能够将其减少到刚好超过200,000(这是我发布的设置)。

但是,还有另一个可以删除的块。如上所述,循环通常看起来像A-> B-> C-> A。但是M-> N-> O-> A-> B-> C-> A呢? MNO并非真正处于循环中。特别是,N既有父母又有孩子,但仍然不是一个循环。一旦A-> B-> C损坏,MNO就可以了。相反,搬出MNO并不会释放在ABC中断时无法释放的任何东西。

修剪掉这些MNO类型的单位会使计数从200,000减少到52,000。这有什么区别吗?好吧甚至我上面的简单样本也有所改进。从1200万下降到9,跌幅最大,从300万下降到100万。

距离我所知道的<340,000,还有很长的路要走,但是还是有进步的。

我仍然选择相信,有一种方法可以通过此逻辑进行逻辑测试,而无需测试52,000!排列。


编辑3:在尝试了一些复杂的(最终最终没有结果的)替代方案,尝试绘制所有循环并弄清楚如何最好地打破它们之后,我从头开始并重新开始。

有两种移动单位的方法:

  • 使用MoveOut将其移出。
  • 在所有阻止它的事物上使用MoveOut(即,使用框架中的WhoIsBlockingMe)。

考虑到这一点,我尝试移动最大(剩余)的块。由于搬走所有孩子的总费用比较便宜,所以我逐个走一下,再弄清楚搬走孩子还是他们的孩子等便宜些。

有些装饰,但这是基本概念。

这使我的个人最佳成绩达到382,962。并不是最好的(已知)解决方案(<340,000),但是越来越接近。对于每秒运行1/2秒的东西来说还不错。

我仍然可以在这里使用一些帮助,因此,如果有人有动力去尝试一下,我准备发布更新的代码/文件。

0 个答案:

没有答案