我相信以下方法在技术上可以达到O(N+Q) 的时间复杂度。
观察
子图:图不一定是连续的。所有图由一个或多个不相交的连续完整子图组成,含义:
- 子图之间没有共享节点(“不相交”)
- 子图中的所有节点都是连接的(“连续的”)
- 没有连接不同子图的路径(“完整”)
我以后将这些称为图的子图或简称为“子图”。这些子图具有以下附加属性,这是它们的定义(上图)和图中节点类型的结果(它们都是只有一个出边/指针的“父指针节点”):
- 所有此类子图中必须恰好有一个循环(因为循环是它们可以终止或关闭的唯一方式)
- 循环可以是任意长度
cycle.Len >= 1
- 此外,可能有任意数量的 (
t >= 0) 树在其根(基)处附加到循环
- 所有节点要么在循环中,要么在其中一棵树中(树的根在循环中,但也算作树的一部分)
条款:
-
cycle Length:一个循环中的节点数。
-
cycle Base:循环中任意选择的节点,用于测量同一循环中两个节点之间的距离和距离,以及同一子图中的任意两个节点。
-
tree Base:连接到循环的其中一棵树的基节点或根节点。由于树基也是将其附加到循环的节点,因此树基节点被计为在循环中(并且也是其树的一部分)。
-
距离:对于循环中的节点,这是从该节点到循环基的距离(跳数)(如果它是循环基则为零)。对于树中的一个节点(不计算树基节点,算在循环中),这是该节点到树基节点的距离。
碰撞案例
琐碎的
在一个图中有许多可能的碰撞方式或“形式”,但我们可以预先确定两种微不足道的情况:
| (A, B) Relation |
Collide? |
Collision Distance |
| same node |
Yes |
0 |
| different subgraphs |
No |
-1 |
很明显,如果 A 和 B 是同一个节点,那么它们在距离为零时很容易发生碰撞。同样,如果它们在两个不同的子图中,那么它们永远不会发生碰撞,因为子图之间没有连接。对于接下来的碰撞案例,我将假设这两个案例已经被过滤掉了:
- 假设 A 和 B 是不同的节点,并且
- 假设 A 和 B 在同一个子图中
不平凡的
下表涵盖了两个节点之间关系的所有其他重要情况。
| (A, B) Relation |
Collide? |
Collision Distance |
Notes |
| same cycle |
No |
-1 |
nodes in cycle always stay the same distance apart |
| A in a tree & B in the cycle (or vice-versa) |
if they both arrive at A's treeBase at the same time |
-1 OR A.treeDist |
if B.cycleDist = (A.treeDist MOD cycle.Len) |
| A and B are in different trees |
if A and B's distance to their cycle.Base is equal MOD cycle.Len |
MAX(A.treeDist, B.treeDist) |
They meet when the farther one gets to the cycle (tree root) |
| A & B are in the same tree, but have different treeDist's |
If their treeDist's are equal MOD cycle.Len |
MAX(A.treeDist, B.treeDist) |
They meet when the farther one gets to the cycle (tree root/base) |
| A & B are in the same tree, and have the same treeDist's |
Yes |
At their lowest common ancestor (LCA) in the tree |
Have to search up the tree |
上面多次应用的一个重要原则是循环中的两个不同节点永远不会发生冲突。这是因为当每个节点在循环中遵循其路径时,它们将始终保持相同的距离,一个节点的路径无法在循环中“赶上”另一个节点的路径。它们只能“碰撞”如果它们在同一个节点的循环中开始。
这样做的后果是:
- 循环中的两个不同节点永远不会发生冲突。
- 树中的节点只能与循环中的节点发生碰撞,前提是它们到循环基的总距离相同,以循环长度取模(即除以循环长度后的余数)。
- 这也适用于不同树中的两个节点和同一树中的两个节点,但到它们的树基的距离不同。
- 在所有这些情况下(来自 #2 和 #3),当离其树基最远的节点到达循环(也是其树基)时,它们将发生冲突。这是因为循环中的节点不能相互“追赶”,因此一旦它们都在循环中,它们必须始终相同。因此,当更远的人最终到达循环时,它们总是会发生碰撞。
另一个重要的结果是,上面两个表中的每个案例,除了最后一个,都可以在O(1) 时间内回答,只需用如此容易确定的信息注释节点:
- 它们的基础节点(树或循环)
- 他们到那个基础节点的距离
- 它们所属的子图
- 子图的循环长度
在以O(1) 每个节点的时间(或O(N) 总时间)初始化图表时,这些都可以很容易地确定。
方法
节点
在节点最初加载到图形(MPDGraph 对象)后,我使用上面列出的附加信息对节点进行注释。这个过程(代码中称为“映射”)进行如下:
- 选择任意节点
- 跟随它的路径,直到它通过到达已经在它的路径中的节点或之前映射的节点而“终止”
- 如果#2 越过了它自己的路径,那么我们发现了一个新的循环。将我们穿越的节点指定为循环的基节点,并填写映射属性(循环、基节点、距离等)。一次展开我们的路径一个节点,填充每个节点并将其标记为 InCycle,因为我们返回路径直到我们再次到达基本节点。现在我们吃掉了我们的路径进入循环的树的基础,所以当我们回到路径中的前一个节点时,我们切换到将其标记为树节点,直到我们返回到路径中的第一个节点。
- 如果相反,#2 到达了一个已经映射的节点,那么我们很好地将我们的路径附加到该节点并将其树/循环、基础等信息复制到我们当前的节点。然后我们将返回路径,如 #3 所示,设置每个节点的映射属性,
- 如果有任何未映射的节点,请选择一个并转到 #2。
这一切都需要O(N) 时间。
查询
我们有一个方法(称为 MPDGraph.FindCollision),给定两个节点将应用上面两个碰撞案例表中的规则并返回结果。对于除了最后一个(同一树中的节点和相同距离)之外的其他情况,可以使用映射属性在O(1) 时间确定距离。
如果这两个节点在同一棵树中,并且与它们的树基的距离也相同,那么它们可以在它们和它们的公共 treeBase 节点之间的任何地方相遇。对于这种情况,FindCollision(A,B) 方法调用 findTreeDistance(A,B),其中:
- 如果它们是同一个节点,则返回零。
- 否则,它会检查先前计算的距离的缓存,以查看是否已经为这两个节点计算过。如果是,则返回该值。
- 否则,它调用
findTreeDistance 传入当前两个节点的父节点以获取它们的距离,并将其加一。然后它将它添加到缓存中并返回值。
如果没有这种记忆(即缓存),这将平均需要大约 10 分钟。 O(log N) 用于此类型的每个查询。使用 memoization 很难计算,但我猜不会比 O(log log N) 差,但是对于比 N 大得多的 Q 计数,这将收敛到 O(1)。
这使得查询处理时间复杂度介于O(Q log log N) 和O(Q) 之间,总时间介于O(N + Q(log log N)) 和O(N + Q) 之间。
代码
public static int[] collisionTime(int N, int Q, int[] A, int[,] queries)
{
// create the graph and fill-in the mapping attributes for all nodes
var graph = new MPDGraph(A);
graph.MapAllNodes();
int[] answers = new int[queries.GetLength(0)];
for (int i = 0; i < answers.Length; i++)
{
answers[i] = graph.FindCollision(queries[i, 0], queries[i, 1]);
}
return answers;
}
这利用了以下类,
MPDGraph 类:
// MPDG: Mono-Pointing, Directed Graph
// An MPDG is a directed graph where every node has exactly one out-edge.
// (MPDG is my own term, I don't know the real name for these)
class MPDGraph
{
public Node[] Nodes;
Dictionary<(Node, Node), int> cachedDistances = new Dictionary<(Node, Node), int>();
// constructor
public MPDGraph(int[] Pointers)
{
Nodes = new Node[Pointers.Length];
// first fill the array with objects
for (int i = 0; i < Nodes.Length; i++) { Nodes[i] = new Node(i); }
// apply their pointers
for(int i = 0; i < Nodes.Length; i++) { Nodes[i].toNode = Nodes[Pointers[i]]; }
}
// map all of the nodes by filling their mapping properties
public void MapAllNodes()
{
for(int i=0; i<Nodes.Length; i++)
{
if (!Nodes[i].isMapped)
MapPath(Nodes[i], 1);
}
}
// recursively map a path of unmapped nodes, starting from curr
// returns true if curr is in a cycle, false otherwise
public Boolean MapPath(Node curr, int pathNo)
{
Boolean inCycle = false;
curr.pathNo = pathNo;
Node next = curr.toNode;
if (next.IsInPath)
{
// we have found a new cycle
Cycle Cycle = new Cycle(this, next, curr.pathNo - next.pathNo + 1);
curr.Map(Cycle);
return true;
}
else if (next.isMapped)
{
// we are joining an already partially mapped tree
if (next.IsInCycle)
{
// next is a tree-Base, the top of our tree and also in the cycle
curr.Map(next.Cycle, false, next, 1);
}
else
{
// next is a normal tree-node
curr.Map(next.Cycle, false, next.BaseNode, next.Distance + 1);
}
return false;
}
else
{
// continue forward on the path, recurse to the next node
inCycle = MapPath(next, pathNo+1);
if (inCycle)
{
if (next.Cycle.Base == next || next.Distance == 0)
{
//we have returned from the cycleBase, which is also a treeBase
// so, switch from Cycle to Tree
curr.Map(next.Cycle, false, next, 1);
return false;
}
else
{
// still in the cycle
curr.Map(next.Cycle);
}
}
else
{
//returned in tree
curr.Map(next.Cycle, false, next.BaseNode, next.Distance + 1);
}
return inCycle;
}
}
// Given two starting nodes, determine how many steps it takes until their
// paths collide. Returns -1 if they will never collide.
public int FindCollision(int index1, int index2)
{
Node node1 = Nodes[index1];
Node node2 = Nodes[index2];
// eliminate trivial cases
if (node1.Cycle != node2.Cycle)
return -1; // cant collide if they're in different subgraphs
else if (node1 == node2)
return 0; // if they're the same node, then distance = 0
else if (node1.IsInCycle && node2.IsInCycle)
return -1; // different nodes in a cycle never collide
else
{ // they're both in the same subgraph, use math to tell if they collide
// get their distances to the cycle base
int dist1 = node1.Distance + (node1.IsInCycle ? 0 : node1.BaseNode.Distance);
int dist2 = node2.Distance + (node2.IsInCycle ? 0 : node2.BaseNode.Distance);
int cycleLen = node1.Cycle.Length;
// use math: modulo(cycle length)
if ((dist1 % cycleLen) != (dist2 % cycleLen))
{
return -1; // incompatible distances: cannot possibly collide
}
else
{
// they must collide somewhere, figure out how far that is
if (node1.IsInCycle || node2.IsInCycle)
{
// if one is in the cycle, they will collide when
// the other one reaches the cycle (it's treeBase)
return (!node1.IsInCycle ? node1.Distance : node2.Distance);
}
else if (node1.BaseNode != node2.BaseNode)
{
// They are in different trees: they will collide at
// the treeBase of the node that is farther
return Math.Max(node1.Distance, node2.Distance);
}
else
{
// They are in the same tree:
if (node1.Distance != node2.Distance)
{
//if they are in the same tree, but have different distances
// to the treeBase, then they will collide at the treeBase
// when the farther one arrives at the treeBase
return Math.Max(node1.Distance, node2.Distance);
}
else
{
// the hard case, have to walk down their paths
// to find their LCA (Lowest Common Ancestor)
return findTreeDistance(node1, node2);
}
}
}
}
}
int findTreeDistance(Node node1, Node node2)
{
if (node1 == node2) return 0;
// normalize their order
if (node1.index > node2.index)
{
var tmp = node1;
node1 = node2;
node2 = tmp;
}
// check the cache
int dist;
if (cachedDistances.ContainsKey((node1,node2)))
{
dist = cachedDistances[(node1, node2)];
}
else
{
// keep recursing to find where they meet
dist = findTreeDistance(node1.toNode, node2.toNode) + 1;
// cache this new distance
cachedDistances.Add((node1, node2), dist);
}
return dist;
}
}
节点类:
// Represents a node in the MPDG (Mono-Pointing Directed Graph) with the constraint
// that all nodes have exactly one out-edge ("toNode").
class Node
{
// Primary properties (from input)
public int index { get; set; } // this node's unique index (to the original array)
public Node toNode { get; set; } // what our single out-edge is pointing to
public Node(int Index_) { this.index = Index_; }
// Mapping properties
// (these must be filled-in to finish mapping the node)
// The unique cycle of this node's subgraph (all MPDG-subgraphs have exactly one)
public Cycle Cycle;
// Every node is either in their subgraphs cycle or in one of the inverted
// trees whose apex (base) is attached to it. Only valid when BaseNode is set.
// (tree base nodes are counted as being in the cycle)
public Boolean IsInCycle = false;
// The base node of the tree or cycle that this node is in.
// If (IsInCycle) then it points to this cycle's base node (cycleBase)
// Else it points to base node of this node's tree (treeBase)
public Node BaseNode;
// The distance (hops) from this node to the BaseNode
public int Distance = -1; // -1 if not yet mapped
// Total distance from this node to the cycleBase
public int TotalDistance { get { return Distance + (IsInCycle ? 0 : BaseNode.Distance); } }
// housekeeping for mapping nodes
public int pathNo = -1; // order in our working path
// Derived (implicit) properties
public Boolean isMapped { get { return Cycle != null; } }
public Boolean IsInPath { get { return (pathNo >= 0); } }
public void Map(Cycle Cycle, bool InCycle = true, Node BaseNode = null, int distance_ = -1)
{
this.Cycle = Cycle;
IsInCycle = InCycle;
if (InCycle)
{
this.BaseNode = Cycle.Base;
Distance = (Cycle.Length + (Cycle.Base.pathNo - pathNo)) % Cycle.Length;
}
else
{
this.BaseNode = BaseNode;
Distance = distance_;
}
pathNo = -1; // clean-up the path once we're done
}
}
自行车类:
// represents the cycle of a unique MPDG-subgraph
// (should have one of these for each subgraph)
class Cycle
{
public MPDGraph Graph; // the MPDG that contains this cycle
public Node Base; // the base node of a subgraph's cycle
public int Length; // the length of this cycle
public Cycle(MPDGraph graph_, Node base_, int length_)
{
Graph = graph_;
Base = base_;
Length = length_;
}
}
性能测量:
| Node Count |
Build & Map Graph mean microsecs |
Question Count |
All Questions mean microsecs |
Question mean microseconds |
Total mean microseconds |
| 50 |
0.9 |
1225 |
26 |
0.0212 |
26.9 |
| 500 |
10.1 |
124750 |
2267 |
0.0182 |
2277.1 |
| 1000 |
23.4 |
499500 |
8720 |
0.0175 |
8743.4 |
| 5000 |
159.6 |
12497500 |
229000 |
0.0183 |
229159.6 |
| 10000 |
345.3 |
49995000 |
793212 |
0.0159 |
793557.3 |