C#图表算法之有向图

目录
  • 1.术语
  • 2.有向图的数据类型
    • 有向图表示
    • 有向图取反
    • 顶点的符号名
  • 3.有向图的可达性
  • 4.环和有向无环图
    • 调度问题
    • 有向图中的环
    • 顶点的深度优先次序与拓扑排序
    • 拓扑排序
  • 5.有向图中的强连通性
    • 强连通分量
      • 强连通分量API
    • Kosaraju算法
    • 再谈可达性
  • 总结

在有向图中,边是单向的:每条边连接的两个顶点都是一个有序对,它们的邻接性是单向的。许多应用都是天然的有向图,如下图。为实现添加这种单向性的限制很容易也很自然,看起来没什么坏处。但实际上这种组合性的结构对算法有深刻的影响,使得有向图和无向图的处理大有不同。

1.术语

虽然我们为有向图的定义和无向图几乎相同(将使用的部分算法和代码也是),但为了说明边的方向性而产生的细小文字差异所代表的结构特性是重点。

定义:一幅有方向性的图(或有向图)是由一组顶点和一组有方向的边组成的,每条有方向的边都连着有序的一对顶点。

我们称一条有向边由第一个顶点指出并指向第二个顶点。在一幅有向图中,一个顶点的出度为由该顶点指出的边的总数;一个顶点的入度为指向该顶点的边的总数。一条有向边的第一个顶点称为它的头,第二个顶点则称为它的尾。用 v->w 表示有向图中一条由v 指向 w 的边。一幅有向图的两个顶点的关系可能有四种:没有边相连;v->w; w-> v;v->w 和 w->v。

在一幅有向图中,有向路径由一系列顶点组成,对于其中的每个顶点都存在一条有向边从它指向序列中的下一个顶点。有向环为一条至少含有一条边且起点和终点相同的有向路径。路径或环的长度即为其中所包含的边数。

当存在从 v 到 w 的有向路径时,称顶点 w 能够由顶点 v 达到。我们需要理解有向图中的可达性和无向图中的连通性的区别。

2.有向图的数据类型

有向图API

有向图表示

我们使用邻接表来表示有向图,其中边 v -> w 表示顶点 v 所对应的邻接链表中包含一个 w 顶点。这种表示方法和无向图几乎相同而且更明晰,因为每条边都只会出现一次。

有向图取反

Digraph 的 API 中还添加了一个 Reverse 方法。它返回该有向图的一个副本,但将其中所有边的方向反转。在处理有向图时这个方法有时很有用,因为这样用例就可以找出“指向”每个顶点的所有边,而 Adj 方法给出的是由每个顶点指出的边所连接的所有顶点。

顶点的符号名

在有向图中,使用符号名作为顶点也很简单,参考SymbolGraph。

namespace Digraphs
{
    public class Digraph
    {
        private int v;
        private int e;
        private List<int>[] adj;

        public Digraph(int V)
        {
            this.v = V;
            this.e = 0;
            adj = new List<int>[v];
            for (var i = 0; i < v; i++)
            {
                adj[i] = new List<int>();
            }
        }

        public int V()
        {
            return v;
        }

        public int E()
        {
            return e;
        }

        public List<int> Adj(int v)
        {
            return adj[v];
        }

        public void AddEdge(int v, int w)
        {
            adj[v].Add(w);
            e++;
        }

        public Digraph Reverse()
        {
            Digraph R = new Digraph(v);
            for (var i = 0; i < v; i++)
                foreach (var w in Adj(i))
                    R.AddEdge(w,i);

            return R;
        }
    }
}

3.有向图的可达性

在无向图中介绍的深度优先搜索DepthFirstSearch ,解决了单点连通性的问题,使得用例可以判定其他顶点和给定的起点是否连通。使用完全相同的代码,将其中的 Graph 替换成 Digraph 也可以解决有向图中的单点可达性问题(给定一幅有向图和一个起点 s ,是否存在一条从 s 到达给定顶点 v 的有向路径?)。

在添加了一个接受多个顶点的构造函数之后,这份 API 使得用例能够解决一个更加一般的问题 -- 多点可达性 (给定一幅有向图和顶点的集合,是否存在一条从集合中的任意顶点到达给定顶点 v 的有向路径?)

下面的DirectedDFS 算法使用了解决图处理的标准范例和标准的深度优先搜索来解决。对每个起点调用递归方法 Dfs ,以标记遇到的任意顶点。

namespace Digraphs
{
    public class DirectedDFS
    {
        private bool[] marked;

        public DirectedDFS(Digraph G, int s)
        {
            marked = new bool[G.V()];
            Dfs(G,s);
        }

        public DirectedDFS(Digraph G, IEnumerable<int> sources)
        {
            marked = new bool[G.V()];
            foreach (var s in sources)
            {
                if (!marked[s])
                    Dfs(G,s);
            }
        }

        private void Dfs(Digraph G, int V)
        {
            marked[V] = true;
            foreach (var w in G.Adj(V))
            {
                if (!marked[w])
                    Dfs(G,w);
            }
        }

        public bool Marked(int v)
        {
            return marked[v];
        }
    }
}

在有向图中,深度优先搜索标记由一个集合的顶点可达的所有顶点所需的时间与被标记的所有顶点的出度之和成正比。

有向图的寻路

在无向图中的寻找路径的算法,只需将 Graph 替换为 Digraph 就能够解决下面问题:

  • 1.单点有向路径:给定一幅有向图和一个起点 s ,从 s 到给定目的顶点是否存在一条有向路径?如果有,找出这条路径。
  • 2.单点最短有向路径:给定一幅有向图和一个起点 s ,从 s 到给定目的顶点 v 是否存在一条有向路径?如果有,找出其中最短的那条(所含边数最少)。

4.环和有向无环图

在和有向图相关的实际应用中,有向环特别的重要。没有计算机的帮助,在一幅普通的有向图中找出有向环可能会很困难。从原则上来说,一幅有向图可能含有大量的环;在实际应用中,我们一般只重点关注其中一小部分,或者只想知道它们是否存在。

调度问题

一种应用广泛的模型是给定一组任务并安排它们的执行顺序,限制条件是这些任务的执行方法和开始时间。限制条件还可能包括任务的耗时以及消耗的资源。最重要的一种限制条件叫做优先级限制,它指明了哪些任务必须在哪些任务之前完成。不同类型的限制条件会产生不同类型不同难度的调度问题。

下面以一个正在安排课程的大学生为例,有些课程是其他课程的先导课程:

如果假设该学生一次只能修一门课程,就会遇到优先级下的调度问题:给定一组需要完成的任务,以及一组关于任务完成的先后次序的优先级限制。在满足限制条件的前提下应该如何安排并完成所有任务?

对于任意一个这样的问题,我们先画出一幅有向图,其中顶点对应任务,有向边对应优先级顺序。为了简化问题,我们以整数为顶点:

在有向图中,优先级限制下的调度问题等价于一个基本问题--拓扑排序:给定一幅图,将所有顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素(或者说明无法做到这一点)。

如图,所有的边都是向下的,所以清晰地表示了这幅有向图模型所代表的有优先级限制的调度问题的一个解决方法:按照这个顺序,该同学可以满足先导课程限制的条件下修完所有课程。

有向图中的环

如果任务 x 必须在任务 y 之前完成,而任务 y 必须在任务 z 之前完成,但任务 z 又必须在任务 x 之前完成,那肯定是有人搞错了,因为这三个限制条件是不可能被同时满足的。一般来说,如果一个优先级限制的问题中存在有向环,那么这个问题肯定是无解的。要检查这种错误,需要解决 有向环检测:给定的有向图中包含有向环吗?如果有,按照路径的方向从某个顶点并返回自己来找到环上的所有顶点。

一幅有向图中含有环的数量可能是图的大小的指数级别,因此我们只需找到一个环即可,而不是所有环。在任务调度和其他许多实际问题中不允许出现有向环,因此有向无环图就变得很特殊。

基于深度优先搜索可以解决有向环检测的问题,因为由系统维护的递归调用的栈表示的正是“当前”正在遍历的有向路径。一旦我们找到了一条有向边 v -> w 且 w 已经存在于栈中,就找到了一个环,因为栈表示的是一条由 w 到 v 的有向路径,而 v -> w 正好补全了这个环。如果没有找到这样的边,就意味着这副有向图是无环的。DirectedCycle 基于这个思想实现的:

namespace Digraphs
{
    public class DirectedCycle
    {
        private bool[] marked;
        private int[] edgeTo;
        private Stack<int> cycle;//有向环中的所有顶点(如果存在)
        private bool[] onStack;//递归调用的栈上的所有顶点

        public DirectedCycle(Digraph G)
        {
            onStack = new bool[G.V()];
            edgeTo = new int[G.V()];
            marked = new bool[G.V()];

            for (int v = 0; v < G.V(); v++)
            {
                if (!marked[v])
                    Dfs(G,v);
            }
        }

        private void Dfs(Digraph G, int v)
        {
            onStack[v] = true;
            marked[v] = true;
            foreach (var w in G.Adj(v))
            {
                if (hasCycle())
                    return;
                else if (!marked[w])
                {
                    edgeTo[w] = v;
                    Dfs(G, w);
                }
                else if (onStack[w])
                {
                    cycle = new Stack<int>();
                    for (int x = v; x != w; x = edgeTo[x])
                        cycle.Push(x);
                    cycle.Push(w);
                    cycle.Push(v);
                }
            }
            onStack[v] = false;
        }

        private bool hasCycle()
        {
            return cycle != null;
        }

        public IEnumerable<int> Cycle()
        {
            return cycle;
        }
    }
}

该类为标准的的递归 Dfs 方法添加了一个布尔类型的数组 onStack 来保存递归调用期间栈上的所有顶点。当它找到一条边 v -> w 且 w 在栈中时,它就找到了一个有向环。环上的所有顶点可以通过 edgeTo 中的链接得到。

在执行 Dfs 时,查找的是一条由起点到 v 的有向路径。要保存这条路径,DirectedCycle 维护了一个由顶点索引的数组onStack,以标记递归调用的栈上的所有顶点(在调用 Dfs 时将 onStack[ v ] 设为 true,在调用结束时将其设为 false)。DirectedCycle 同时也使用了一个edgeTo 数组,在找到有向环时返回环中的所有顶点。

顶点的深度优先次序与拓扑排序

优先级限制下的调度问题等价于计算有向无环图中的所有顶点的拓扑排序:

下面算法的基本思想是深度优先搜索正好只会访问每个顶点一次。如果将 Dfs 的参数顶点保存在一个数据结构中,遍历这个数据结构实际上就能访问图中的所有顶点,遍历的顺序取决于这个数据结构的性质以及是在递归调用之前还是之后进行保存。在典型的应用中,顶点一下三种排列顺序:

  • 前序:在递归调用之前将顶点加入队列;
  • 后序:在递归调用之后将顶点加入队列;
  • 逆后序:在递归调用之后将顶点压入栈。

该类允许用例用各种顺序遍历深度优先搜索经过得顶点。这在高级得有向图处理算法非常有用,因为搜索得递归性使得我们能够证明这段计算得许多性质。

namespace Digraphs
{
    public class DepthFirstOrder
    {
        private bool[] marked;
        private Queue<int> pre;//所有顶点的前序排列
        private Queue<int> post;//所有顶点的后序排列
        private Stack<int> reversePost;//所有顶点的逆后序排列

        public DepthFirstOrder(Digraph G)
        {
            marked = new bool[G.V()];
            pre = new Queue<int>();
            post = new Queue<int>();
            reversePost = new Stack<int>();

            for (var v = 0; v < G.V(); v++)
            {
                if (!marked[v])
                    Dfs(G,v);
            }
        }

        private void Dfs(Digraph G, int v)
        {
            pre.Enqueue(v);

            marked[v] = true;
            foreach (var w in G.Adj(v))
            {
                if (!marked[w])
                    Dfs(G,w);
            }

            post.Enqueue(v);
            reversePost.Push(v);
        }

        public IEnumerable<int> Pre()
        {
            return pre;
        }

        public IEnumerable<int> Post()
        {
            return post;
        }

        public IEnumerable<int> ReversePost()
        {
            return reversePost;
        }
    }
}

一幅有向无环图得拓扑排序即为所有顶点的逆后序排列。

拓扑排序

namespace Digraphs
{
    public class Topological
    {
        private IEnumerable<int> order;
        public Topological(Digraph G)
        {
            DirectedCycle cycleFinder = new DirectedCycle(G);
            if (cycleFinder.HasCycle())
            {
                DepthFirstOrder dfs = new DepthFirstOrder(G);
                order = dfs.ReversePost();
            }
        }

        public IEnumerable<int> Order()
        {
            return order;
        }

        public bool IsDAG()
        {
            return order != null;
        }
    }
}

这段使用DirectedCycle 检测是否有环,使用DepthFirstOrder 返回有向图的逆后序。

使用深度优先搜索对有向无环图进行拓扑排序所需的时间和 V+E 成正比。第一遍深度优先搜索保证了不存在有向环,第二遍深度优先搜索产生了顶点的逆后序排列。

在实际应用中,拓扑排序和有向环的检测总是一起出现,因为有向环的检测是排序的前提。例如,在一个任务调度应用中,无论计划如何安排,其背后的有向图中包含的环意味着存在一个必须被纠正的严重错误。因此,解决任务调度类应用通常需要一下3步:

  • 1.指明任务和优先级条件;
  • 2.不断检测并去除有向图中的所有环,以确保存在可行方案;
  • 3.使用拓扑排序解决调度问题。

类似地,调度方案的任何变动之后都需要再次检查是否存在环,然后再计算新的调度安排。

5.有向图中的强连通性

如果两个顶点 v 和 w 是相互可达的,则称它们为强连通的。也就是说,即存在一条从 v 到 w 的有向路径,也存在一条从 w 到 v 的有向路径。如果一幅有向图中的任意两个顶点都是强连通的,则称这副有向图也是强连通的。

下面是强连通图的例子,可以看到,环在强连通性的理解上起着重要的作用。

强连通分量

和无向图中的连通性一样,有向图中的强连通性也是一种顶点之间的等价关系:

  • 自反性:任意顶点 v 和自己都是强连通的。
  • 对称性:如果 v 和 w 是强连通的,那么 w 和 v 也是。
  • 传递性:如果 v 和 w 是强连通的且 w 和 x 也是强连通的,那么 v 和 x 也是强连通的。

作为一种等价关系,强连通性将所有顶点分为了一些等价类,每个等价类都是由相互均为强连通的顶点的最大子集组成。我们称这些子集为强连通分量。如下图,一个含有 V 个顶点的有向图含有 1~ V个强连通分量——一个强连通图只含有一个强连通分量,而一个有向无环图则含有 V 个强连通分量。需要注意的是强连通分量的定义是基于顶点的,而不是边。有些边连接的两个顶点都在同一个强连通分量中,而有些边连接的两个顶点则不在同一强连通分量中。

强连通分量API

设计一种平方级别的算法来计算强连通分量并不困难,单对于处理实际应用中的大型图来说,平方级别的时间和空间需求是不可接受的。

Kosaraju算法

在有向图中如何高效地计算强连通分量?我们只需修改无向图连通分量的算法 CC,KosarajuCC 算法如下,它将会完成一下任务:

1.在给定的一幅有向图 G 中,使用 DepthFirstOrder 来计算它的反向图 GR 的逆后序排列;

2.在 G 中进行标准的深度优先搜索,但是要按照刚才计算得到的顺序而非标准的顺序来访问所有未被标记的顶点;

3.在构造函数中,所有在同一个递归 Dfs() 调用中被访问到的顶点都在同一个强连通分量中,将它们按照和 CC 相同的方式识别出来。

namespace Digraphs
{
    public class KosarajuCC
    {
        private bool[] marked;//已访问的顶点
        private int[] id;//强连通分量的标识符
        private int count;//强连通分量的数量

        public KosarajuCC(Digraph G)
        {
            marked = new bool[G.V()];
            id = new int[G.V()];
            DepthFirstOrder order = new DepthFirstOrder(G.Reverse());
            foreach (var s in order.ReversePost())
            {
                if (!marked[s])
                {
                    Dfs(G,s);
                    count++;
                }
            }
        }

        private void Dfs(Digraph G, int v)
        {
            marked[v] = true;
            id[v] = count;
            foreach (var w in G.Adj(v))
            {
                if (!marked[w])
                    Dfs(G,w);
            }
        }

        public bool StronglyConnected(int v, int w)
        {
            return id[v] == id[w];
        }

        public int Id(int v)
        {
            return id[v];
        }

        public int Count()
        {
            return count;
        }
    }
}

Kosaraju 算法的预处理所需的时间和空间与 V+E 成正比且支持常数时间的有向图强连通性的查询。

再谈可达性

在无向图中如果两个顶点 V 和 W 是连通的,那么就既存在一条从 v 到 w 的路径也存在一条从 w 到 v 的路径。在有向图中如果两个顶点 v 和 w 是强连通的,那么就既存在一条从 v 到 w 的路径也存在另一条从 w 到 v 的路径。但对于一对非强连通的顶点,也许存在一条从 v 到 w 的路径,也许存在一条从 w 到 v 的路径,也许两条都不存在,但不可能两条都存在。

顶点对的可达性:对于无向图,等价于连通性问题;对于有向图,它和强连通性有很大区别。 CC 实现需要线性级别的预处理时间才能支持常数时间的操作。在有向图的相应实现中能否达到这样的性能?

有向图 G 的传递闭包是由相同的一组顶点组成的另一幅有向图,在传递闭包中存在一条从 v 指向 w 的边当且仅当在 G 中 w 是从 v 可达的。

根据约定,每个顶点对于自己都是可达的,因此传递闭包会含有 V 个自环。上图只有 22 条有向边,但它的传递闭包含有可能的 169 条有向边中的 102 条。一般来说,一幅有向图的传递闭包中所含的边都比原图中多得多。例如,含有 V 个顶点和 V 条边的有向环的传递闭包是一幅含有 V 的平方条边的有向完全图。因为传递闭包一般都是稠密的,我们通常都将它们表示为一个布尔值矩阵,其中 v 行 w 列的值为 true 当且仅当 w 是从 v 可达的。与其计算一幅有向图的传递闭包,不如使用深度优先搜索来实现如下API:

下面的算法使用DirectedDFS 实现:

namespace Digraphs
{
    public class TransitiveClosure
    {
        private DirectedDFS[] all;

        public TransitiveClosure(Digraph G)
        {
            all = new DirectedDFS[G.V()];
            for (var v = 0; v < G.V(); v++)
                all[v] = new DirectedDFS(G,v);
        }

        public bool Reachable(int v, int w)
        {
            return all[v].Marked(w);
        }
    }
}

该算法无论对于稀疏图还是稠密图,都是理想解决方案,但对于大型有向图不适用,因为构造函数所需的空间和 V 的平方成正比,所需的时间和 V(V+ E) 成正比。

总结

到此这篇关于C#图表算法之有向图的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#实现递归算法经典实例

    目录 一 .递归算法简介 二 .Fibonacci数列和阶乘 1.Fibonacci数列 2.阶乘 三 .汉诺塔问题 四 .排列组合 1.输出任意个数字母.数字的全排列 2.将全排列结果保存到链表中 总结 一 .递归算法简介 在数学与计算机科学中,递归是指在函数的定义中使用函数自身的方法. 递归算法是一种直接或者间接地调用自身算法的过程.在计算机编写程序中,递归算法对解决一大类问题是十分有效的,它往往使算法的描述简洁而且易于理解. 递归算法解决问题的特点: (1) 递归就是在过程或函数里调用自身

  • C#使用符号表实现查找算法

    高效检索海量信息(经典查找算法)是现代信息世界的基础设施.我们使用符号表描述一张抽象的表格,将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息.键和值的具体意义取决于不同的应用.符号表中可能会保存很多键和很多信息,因此实现一张高效的符号表是很重要的任务. 符号表有时被称为字典,有时被称为索引. 1.符号表 符号表是一种存储键值对的数据结构,支持两种操作:插入(put),即将一组新的键值对存入表中:查找(get),即根据给定的键得到相应的值.符号表最主要的目的就是将一个健和一个值联系起来

  • C#图表算法之无向图

    目录 1.相关术语 2.表示无向图的数据结构 3.图的处理算法的设计模式 4.深度优先搜索 5.寻找路径 实现 6.广度优先搜索 实现 7.连通分量 实现 union-find 算法 8.符号图 实现 间隔的度数 总结 图是由一组顶点和一组能够将两个顶点相连的边组成. 顶点叫什么名字并不重要,但我们需要一个方法来指代这些顶点.一般使用 0 至 V-1 来表示一张含有 V 个顶点的图中的各个顶点.这样约定是为了方便使用数组的索引来编写能够高效访问各个顶点信息的代码.用一张符号表来为顶点的名字和 0

  • C#实现冒泡排序和插入排序算法

    1.选择排序(冒泡排序) 升序 用第一个元素跟其他元素比较,如果该元素比其他元素,则交换,保证该元素是最小的.然后再用第二个元素跟后面其他的比较,保证第二个元素是除第一个最小的.依次循环,直到整个数组. /// <summary> /// 选择排序 /// </summary> public class Selection:BaseSort { public static long usedTimes = 0; public Selection() { } public stati

  • C#算法之实现阿姆斯特朗数

    阿姆斯特朗数 阿姆斯特朗数是一个数字,等于每个数字的幂乘以总位数. 例如,诸如0.1.153.370.371和407.1634.8208.9474的数字是阿姆斯特朗数. 例如: 371 为3位数, 则用每位数的3次方 (3 * 3 * 3)=27 (7 * 7 * 7)=343 (1 * 1 * 1) =1 总数: 27+343+1=371 判断数字是否属于阿姆斯特朗数? static void Main(string[] args) { int i = 0; int digitCount =

  • C#并查集(union-find)算法详解

    目录 算法的主题思想: 1. 动态连通性 2. 定义问题 3. quick-find算法实现 算法分析 4. quick-union算法实现 森林表示 算法分析 5.加权 quick-union 算法实现 算法分析 6.最优算法 - 路径压缩 算法的主题思想: 1.优秀的算法因为能够解决实际问题而变得更为重要: 2.高效算法的代码也可以很简单: 3.理解某个实现的性能特点是一个挑战: 4.在解决同一个问题的多种算法之间进行选择时,科学方法是一种重要的工具: 5.迭代式改进能够让算法的效率越来越高

  • C#实现快速排序算法

    快速排序是应用最广泛的排序算法,流行的原因是它实现简单,适用于各种不同情况的输入数据且在一般情况下比其他排序都快得多. 快速排序是原地排序(只需要一个很小的辅助栈),将长度为 N 的数组排序所需的时间和 N lg N 成正比. 1.算法 快速排序也是一种分治的排序算法.它将一个数组分成两个子数组,将两部分独立地排序. 快速排序和归并排序是互补:归并排序是将数组分成两个子数组分别排序,并将有序数组归并,这样数组就是有序的了:而快速排序将数组通过切分变成部分有序数组,然后拆成成两个子数组,当两个子数

  • C#算法之散列表

    目录 1.散列函数 正整数 浮点数 字符串 组合键 将 HashCode() 的返回值转化为一个数组索引 自定义的 HashCode 软缓存 2.基于拉链法的散列表 散列表的大小 删除操作 有序性相关的操作 3.基于线性探测法的散列表 删除操作 键簇 线性探测法的性能分析 调整数组大小 拉链法 均摊分析 4.内存的使用 如果所有的键都是小整数,我们可以使用一个数组来实现无序的符号表,将键作为数组的索引而数组中键 i 处存储的就是它对应的值.散列表就是用来处理这种情况,它是简易方法的扩展并能够处理

  • C#算法设计与分析详解

    目录 1. 什么是科学方法?? 1.观察 2.将问题规模和运行时间的关系量化 2.数学模型 近似 近似运行时间 成本模型 总结 3. 增长数量级的分类 4. 倍率实验 5.注意事项 6. 处理对于输入的依赖 7.内存 1. 对象 2. 链表 3. 数组 4. 字符串对象 作为程序员,开发完一段代码,实现了某个功能时,有必要知道: 我的程序需要多长时间? 是什么导致我的程序消耗很多内存? 比如,统计或者处理了一大批数据.影响这些问题的因素很多,例如,电脑的性能,数据的性质(值类型和引用类型的区别)

  • C#实现抢红包算法的示例代码

    目录 二倍均值法(公平版) 线段切割法(手速版) 二倍均值法(公平版) 发出一个固定金额的红包,由若干个人来抢,需要满足哪些规则? 1.所有人抢到金额之和等于红包金额,不能超过,也不能少于. 2.每个人至少抢到一分钱. 3.要保证所有人抢到金额的几率相等. 假设剩余红包金额为M,剩余人数为N,那么有如下公式: 每次抢到的金额 = 随机区间 (0, M / N × 2) 这个公式,保证了每次随机金额的平均值是相等的,不会因为抢红包的先后顺序而造成不公平.举个例子: 假设有10个人,红包总额100元

随机推荐