从一个简单的问题开始,就变成了挑战。现在,我快要被它击败了。帮助吗?
它非常简单。画一个像这样的班级:
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);
}
}
这里值得强调的地方:
这是使用框架的简单类。其目的是告诉框架接下来应移出哪个单元。在这种情况下,它只是提供最大长度的受害者,一直到受害者。最终它将成功,因为它将继续提供单位,直到没有剩余为止。
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个单位,您如何确定哪个将导致最佳解决方案?
还有什么值得一提的?
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:在尝试了一些复杂的(最终最终没有结果的)替代方案,尝试绘制所有循环并弄清楚如何最好地打破它们之后,我从头开始并重新开始。
有两种移动单位的方法:
考虑到这一点,我尝试移动最大(剩余)的块。由于搬走所有孩子的总费用比较便宜,所以我逐个走一下,再弄清楚搬走孩子还是他们的孩子等便宜些。
有些装饰,但这是基本概念。
这使我的个人最佳成绩达到382,962。并不是最好的(已知)解决方案(<340,000),但是越来越接近。对于每秒运行1/2秒的东西来说还不错。
我仍然可以在这里使用一些帮助,因此,如果有人有动力去尝试一下,我准备发布更新的代码/文件。