实现二叉树的各种基本运算的算法_FrankWolfe算法基本原理及编程实现

点击蓝字 关注我们

珍惜当下,把握现在。

——王婷

7cdd51a66b53dc464037eb2a8b5333ce.png

引言:从本科的课程《交通规划》开始,我们就学习了UE平衡和交通分配(Traffic Assignment)的概念。而Frank-wolfe算法作为求解用户平衡交通分配问题的基本算法,是学习交通分配的重中之重,也是学习交通类优化算法的重点内容。本文介绍了用户平衡和Frank-wolfe算法的基本原理,并给出了非常详细的编程实现过程。文中的程序既可当作一次编程练习,也可以作为编写这一类算法的程序框架,为今后用程序求解线性约束下的优化问题提供了思路。

(注:文中重要的公式和算法用红色五角星★标明,以方便查询)

1 背景知识补充

在讲解Frank-Wolfe算法的基本原理之前,先补充一些背景知识。

  • 交通分配(Traffic Assignment):是将预测出来的OD(Origin-Destination)需求按照一定的规则符合实际地分配到网络中的各条道路上,并求出各条道路的交通流量。交通分配是四阶段交通规划方法中的最后一个阶段。

  • BPR函数:由美国联邦公路局(BPR—Bureau of public road)开发的阻抗函数,用来计算路段阻抗。

355e34cc98f7ef1534989775d601cfd7.png

  • Beckmann模型:用户平衡交通分配问题的数学规划问题。

9bdaa935637b586c6e0a4f53697400b3.png

  • 间隙函数(Gap Function):在UE交通分配问题的求解过程中,用于评价解的精度。

2ff9f1790674c0a849a4c77c65b1fca1.png

2 Frank-Wolfe算法基本原理

9ee08ef519ecca4dca689153e6be69f8.png

a9eb95036d6c3106560aa2df111f2b07.png

3 在UE交通分配问题中的应用

5e9cc9330023c33dfdd75058a0304042.png

73cc967877b9fda3d0f896c9d74966b5.png

9f3221edc1ba8ed1365d9d916b7bd369.png

e7efd9f35b7ef9bf8a4fbffaf778d832.png

4 Frank-Wolfe算法详细过程

全有全无算法是UE交通分配算法中的重要步骤,可以构造成函数供Frank-Wolfe算法调用。具体算法步骤如下:

e299aaf0a5a4b2789efdb072d547b569.png

采用Frank-wolf算法求解UE交通分配问题的详细算法步骤如下:

b58e76a7f29f900d96e9f6e548800c2e.png

5 Frank-Wolfe算法代码详解

考虑算法的计算量和网络规模,本文以效率更高的C#语言为例进行代码讲解。编程语言是相通的,擅长使用Python等其他语言的读者可以自行切换。

5.1 代码文件Network的编写

(1)准备工作

首先在项目中新建名为“Network.cs”的代码文件(主函数main在默认的“Program.cs”文件中),Network文件开头引用如下命名空间。

using System;using System.IO;using System.Collections.Generic;

(2)创建描述网络基本属性的类

构建三个类,分别对应网络的节点(CNode)、路段(CLink)和OD对(COrigin),每读取一个网络的节点,就相当于在类CNode中创建一个新的对象。这些类是网络数据结构的基础,在类中分别声明节点、路段和OD对的属性,具体属性见代码的注释部分。

// 节点public class CNode{    public int ID; // 节点的编号,从零开始编号    public double PositionX; // 节点的X坐标    public double PositionY; // 节点的Y坐标    public int Origin_ID = -1; // 节点对应的起点编号,-1表示不是起点    public List<int> IncomingLink = new List<int>(); // 进入节点的路段编号集合    public List<int> OutgoingLink = new List<int>(); // 离开节点的路段编号集合       }// 路段public class CLink{    public int ID; // 路段的编号,从零开始编号    public CNode pInNode; // 路段的起节点    public CNode pOutNode; // 路段的终节点    public double FreeFlowTravelTime; // 自由流走行时间    public double TravelTime; // 走行时间    public double Capacity; // 路段通行能力    public double Alpha = 0.15; // BPR函数参数,一般取0.15    public double Power = 4.0; // BPR函数参数,一般取4.0}// OD对public class COrigin{    public int ID; // 起点的编号,从零开始编号    public CNode pOriginNode; // 起点对应的节点    public List<int> DestinationNode = new List<int>(); // OD对,只记录有需求的OD    public List<double> ODDemand = new List<double>(); // OD需求,只记录有需求的OD}

(3)创建算法中变量和函数的类

在这个类中,定义了Frank-Wolfe算法中的一些重要的变量作为类CNetwork的属性,如路段流量、路段走行时间、UE误差等。此外,还在类中构造了实现算法的函数,这些构造函数是算法的主体。

创建类,并定义类属性的代码如下。

需要注意的是,后续所有的构造函数都在类CNetwork中编写。因此,这些函数将代替以下代码中“……”的部分。

public class CNetwork{    public List m_Node = new List(); //网络节点集合    public List m_Link = new List(); //网络路段集合    public List m_Origin = new List(); //网络的起点集合    public int m_nNode; //节点数    public int m_nLink; //路段数量    public int m_nOrigin; //起点数量    public double[] LinkFlow; //路段流量    public double[] LinkTravelTime; //路段走行时间    public double MaxUEGap = 1.0e-5; //UE的最大误差    public double UEGap; //UE误差    double[] ShortestPathCost;//临时变量,所有节点到起点的最短路int[] ShortestPathParent;//最短路上,所有节点到起点的在最短路上的前继路段……}

(4)构造读取文件的函数

采用如表所示的字段描述交通网络。节点、路段和OD对信息分别储存在三个TXT文档中,每行为一个节点(路段或OD对),不同字段之间用制表符分隔。三个文档包含的字段信息如下表:

b8b943a73c90d5a816bf13899e41d7c7.png

构造三个无返回值的函数读取文件,并将文件中的信息分别读入CNode,CLink和COrigin的代码如下:

    // 函数:读取节点文件    public void ReadNode(String DataPath)    {        if (!File.Exists(DataPath)) // 文件是否存在        {            Console.WriteLine("{0} does not exist!", DataPath);            return;        }        StreamReader f1 = File.OpenText(DataPath);        string row;        string[] Data;        m_Node.Clear();        m_nNode = 0;        while ((row = f1.ReadLine()) != null)        {            if (row == "") continue;            Data = row.Split('\t');            CNode pNode = new CNode();            pNode.ID = m_nNode;            pNode.PositionX = Convert.ToDouble(Data[1]);            pNode.PositionY = Convert.ToDouble(Data[2]);            m_nNode++;            m_Node.Add(pNode);        }        f1.Close();    }    // 函数:读取路段文件    public void ReadLink(string DataPath)    {        if (!File.Exists(DataPath)) // 文件是否存在        {            Console.WriteLine("{0} does not exist!", DataPath);            return;        }        StreamReader f2 = File.OpenText(DataPath);        string row;        string[] Data;        m_Link.Clear();        m_nLink = 0;        CNode pNode = new CNode();        while ((row = f2.ReadLine()) != null)        {            if (row == "") continue;            Data = row.Split('\t');            CLink pLink = new CLink();            pLink.ID = m_nLink;            pLink.pInNode = (CNode)m_Node[Convert.ToInt32(Data[0]) - 1];            pLink.pOutNode = (CNode)m_Node[Convert.ToInt32(Data[1]) - 1];            pLink.FreeFlowTravelTime = Convert.ToDouble(Data[2]);            pLink.Capacity = Convert.ToDouble(Data[3]);            pLink.pInNode.OutgoingLink.Add(pLink.ID);            pLink.pOutNode.IncomingLink.Add(pLink.ID);            m_nLink++;            m_Link.Add(pLink);        }        f2.Close();    }    // 函数:读取OD对文件    public void ReadODpairs(string DataPath)    {        if (!File.Exists(DataPath)) // 文件是否存在        {            Console.WriteLine("{0} does not exist!", DataPath);            return;        }        StreamReader f3 = File.OpenText(DataPath);        string row;        string[] Data;        m_Origin.Clear();        CNode pNode;        COrigin pOrigin;        m_nOrigin = 0;        while ((row = f3.ReadLine()) != null)        {            if (row == "") continue;            Data = row.Split('\t');            pNode = (CNode)m_Node[Convert.ToInt32(Data[0]) - 1];            if (pNode.Origin_ID == -1)            {                pOrigin = new COrigin();                m_nOrigin++;                pOrigin.ID = m_Origin.Count;                pOrigin.pOriginNode = pNode;                pNode.Origin_ID = pOrigin.ID;                m_Origin.Add(pOrigin);            }            else                pOrigin = m_Origin[pNode.Origin_ID];            pNode = (CNode)m_Node[Convert.ToInt32(Data[1]) - 1];            pOrigin.DestinationNode.Add(pNode.ID);            pOrigin.ODDemand.Add(Convert.ToDouble(Data[2]));        }        f3.Close();  }

(5)构造计算最短路的函数

在Frank-Wolfe算法的编写过程中遇到两种计算最短路的情况。首先是在算法1计算全有全无交通分配的line4中,需要返回最短路上的所有路段集合。其次是在间隙函数式(3)中,计算当前路段流量下的最小出行费用需要用到最短路。

分别构造两个名为GetShortestPath()和GetShortestCost()的函数用于返回最短路上的路段集合和最小出行费用。这里采用的是单队列Label-correcting的最短路算法。

        // 计算最短路:返回Start到End最短路径上的 *路段集合*    public List<int> GetShortestPath(int Start, int End)    {        CNode pNode;        CLink pLink;        // 初始化        int startposition = 0, endposition = 1;        ShortestPathCost = new double[m_nNode]; //当前最短路        ShortestPathParent = new int[m_nNode]; //前驱节点        int[] checkList = new int[m_nNode];//队列,循环使用        bool[] binCheckList = new bool[m_nNode]; //是否在队列中        bool[] bscanStatus = new bool[m_nNode];        for (int node = 0; node < m_nNode; node++)        {            ShortestPathParent[node] = -1;            ShortestPathCost[node] = double.MaxValue;            binCheckList[node] = false;        }        ShortestPathCost[Start] = 0;        checkList[0] = Start;        //开始while循环        while (startposition != endposition)        {            if (startposition >= m_nNode) startposition = 0;            int i = checkList[startposition];            startposition++;            pNode = m_Node[i];            for (int index = 0; index < pNode.OutgoingLink.Count; index++)            {                pLink = m_Link[pNode.OutgoingLink[index]];                int j = pLink.pOutNode.ID;                double value = pLink.TravelTime;                if (ShortestPathCost[j] > ShortestPathCost[i] + value)                {                    ShortestPathCost[j] = ShortestPathCost[i] + value;                    ShortestPathParent[j] = i;                    // 添加到队列尾部                    if (endposition >= m_nNode) endposition = 0;                    checkList[endposition] = j;                    endposition++;                    bscanStatus[j] = true;                }            }        }        // 返回最短路径集合        List<int> ShortestPath_Link = new List<int>();        int point_out = End;        for (; ; )        {            int i = 0;            int point_in = ShortestPathParent[point_out];            for (int link = 0; link < m_nLink; link++)            {                pLink = m_Link[link];                if ((point_in == pLink.pInNode.ID) && (point_out == pLink.pOutNode.ID))                {                    ShortestPath_Link.Insert(0, pLink.ID);                    point_out = point_in;                }            }            i++;            if (point_in == Start) break;        }        return ShortestPath_Link;    }    // 计算最短距离:返回Start到End最短路径上的 *最短距离*    public double GetShortestCost(int Start, int End)    {        CNode pNode;        CLink pLink;        // 初始化        int startposition = 0, endposition = 1;        ShortestPathCost = new double[m_nNode]; //当前最短路        ShortestPathParent = new int[m_nNode]; //前驱节点        int[] checkList = new int[m_nNode];//队列,循环使用        bool[] binCheckList = new bool[m_nNode]; //是否在队列中        bool[] bscanStatus = new bool[m_nNode];        for (int node = 0; node < m_nNode; node++)        {            ShortestPathParent[node] = -1;            ShortestPathCost[node] = double.MaxValue;            binCheckList[node] = false;        }        ShortestPathCost[Start] = 0;        checkList[0] = Start;        //开始while循环        while (startposition != endposition)        {            if (startposition >= m_nNode) startposition = 0;            int i = checkList[startposition];            startposition++;            pNode = m_Node[i];            for (int index = 0; index < pNode.OutgoingLink.Count; index++)            {                pLink = m_Link[pNode.OutgoingLink[index]];                int j = pLink.pOutNode.ID;                double value = pLink.TravelTime;                if (ShortestPathCost[j] > ShortestPathCost[i] + value)                {                    ShortestPathCost[j] = ShortestPathCost[i] + value;                    ShortestPathParent[j] = i;                    // 添加到队列尾部                    if (endposition >= m_nNode) endposition = 0;                    checkList[endposition] = j;                    endposition++;                    bscanStatus[j] = true;                }            }        }        // 返回最短路距离        return ShortestPathCost[End];    }

(6)构造全有全无交通分配的函数

全有全无交通分配直接按照算法1编写。

    // 全有全无交通分配:输入为初始路段流量(索引对应CLink中的ID)    public double[] AllorNothingAssignment(double[] LinkFlow)  // int[] LinkFlow是现有的路段流量    {        double[] ANLinkFlow = new double[m_Link.Count]; // 全有全无路段流量        CLink pLink;        COrigin pOrinin;        for (int link = 0; link < m_nLink; link++)        {            pLink = m_Link[link];            // 计算路段走行时间            pLink.TravelTime = pLink.FreeFlowTravelTime * (1 + pLink.Alpha * Math.Pow(LinkFlow[link] / pLink.Capacity, pLink.Power));            // 初始化全有全无路段流量            ANLinkFlow[link] = 0;        }        for (int origin = 0; origin < m_nOrigin; origin++) // 遍历O的循环        {            pOrinin = m_Origin[origin];            int OriginNode = pOrinin.pOriginNode.ID; // 任意一个O的编号            for (int i = 0; i < pOrinin.DestinationNode.Count;i++) // 遍历D的循环            {                int DestinationNode = pOrinin.DestinationNode[i]; // 对应D的编号                double Demand = pOrinin.ODDemand[i];                // 计算最短路上的路径集合                List<int> ShortestPath_Link = new List<int>(GetShortestPath(OriginNode, DestinationNode));                foreach (int link_index in ShortestPath_Link)                {                    ANLinkFlow[link_index] += Demand;                }            }        }        // 输出全有全无结果        // foreach (int index in ANLinkFlow) Console.WriteLine(index);        return ANLinkFlow;    }

(7)构造Frank-Wolfe算法的函数

Frank-Wolfe算法直接按照算法2编写。需要注意的是,在这个函数中引用了间隙函数和计算最优步长时式(14)等号左边的两个辅助函数,将在下一部分给出详细代码。计算最优步长时采用二分法求解式(14)在[0,1]区间内的近似根。

    // Frank-Wolfe算法    public void FrankWolfeAlgorithm()    {        // 初始化:        int k = 0; // 迭代次数        LinkFlow = new double[m_nLink];        for (int link = 0; link < m_nLink; link++) LinkFlow[link] = 0; // 所有路段流量为0        LinkFlow = AllorNothingAssignment(LinkFlow); // 做全有全无分配,得到初始路段流量        UEGap = GetUEGap(LinkFlow);        Console.WriteLine("第{0}次迭代", k);        Console.WriteLine("Gap函数值:" + UEGap);        Console.Write("LinkFlow:");        foreach (int index in LinkFlow) Console.Write(index + ",");        Console.WriteLine();        Console.WriteLine();        // 开始while循环        using (TextWriter tw = File.CreateText("C:/Users/王婷/Desktop/Gap.txt"))        {            while (UEGap > MaxUEGap)            {                double[] newLinkFlow = AllorNothingAssignment(LinkFlow);                // 计算可行下降方向                double[] FeasibleDescentDirection = new double[m_nLink];                for (int link = 0; link < m_nLink; link++)                {                    FeasibleDescentDirection[link] = newLinkFlow[link] - LinkFlow[link];                }                // 计算最优步长(二分法)                double paraLamuda; // 最优步长                double left = 0;                double right = 1;                double mid = 0;                double f_left = OptimalStepFunction(FeasibleDescentDirection, LinkFlow, left); // left的求根函数值                double f_right = OptimalStepFunction(FeasibleDescentDirection, LinkFlow, right); // right的求根函数值                double f_mid;                if ((f_left * f_right) > 0) // f_left和f_right同号,说明根不在[0,1]区间                {                    if (Math.Abs(f_left) > Math.Abs(f_right)) paraLamuda = right;                    else paraLamuda = left;                }                else // f_left和f_right异号,说明[0,1]区间存在方程的根                {                    // 用二分法求根                    while (right - left > MaxUEGap)                    {                        mid = (left + right) / 2;                        f_left = OptimalStepFunction(FeasibleDescentDirection, LinkFlow, left);                        f_right = OptimalStepFunction(FeasibleDescentDirection, LinkFlow, right);                        f_mid = OptimalStepFunction(FeasibleDescentDirection, LinkFlow, mid);                        if ((f_left * f_mid) > 0) left = mid;                        else right = mid;                    }                    paraLamuda = (left + right) / 2;                }                // 更新路段流量                for (int link = 0; link < m_nLink; link++)                {                    LinkFlow[link] = LinkFlow[link] + paraLamuda * FeasibleDescentDirection[link];                }                k += 1;                UEGap = GetUEGap(LinkFlow); // 计算Gap函数                // 将Gap函数值写入文档                tw.WriteLine(UEGap);                Console.WriteLine(UEGap);                // 输出结果                /*                Console.WriteLine("第{0}次迭代", k);                Console.WriteLine("Gap函数值:" + UEGap);                Console.Write("LinkFlow:");                foreach (int index in LinkFlow) Console.Write(index + ",");                Console.WriteLine();                Console.WriteLine();                */            }        }        Console.WriteLine("UE平衡的路段流量:");        foreach (int index in LinkFlow) Console.Write(index + ",");    }

(8)构造其他辅助函数

辅助函数包括间隙函数和计算最优步长等式的左边,具体公式分别见式(3)和(14)。

    // 间隙函数    public double GetUEGap(double[] LinkFlow)      //获取UE的误差    {        COrigin pOrigin;        CLink pLink;        // 计算被减项分母 + 更新路段走行时间(出行费用:TravelTime)        double num1 = 0;        for (int link = 0; link < m_nLink; link++)        {            pLink = m_Link[link];            // 更新TravelTime            pLink.TravelTime = pLink.FreeFlowTravelTime * (1 + pLink.Alpha * Math.Pow(LinkFlow[link] / pLink.Capacity, pLink.Power));            // 单一求和项的计算            num1 += (pLink.TravelTime * LinkFlow[link]);        }        // 计算被减项分子        double num2 = 0;        for (int origin = 0; origin < m_nOrigin; origin++) // 遍历O的循环        {            pOrigin = m_Origin[origin];            int OriginNode = pOrigin.pOriginNode.ID; // 任意一个O的编号            for (int i = 0; i < pOrigin.DestinationNode.Count; i++) // 遍历D的循环            {                int DestinationNode = pOrigin.DestinationNode[i]; // 对应D的编号                // 单一求和项的计算                double Demand = pOrigin.ODDemand[i];                double ODCost = GetShortestCost(OriginNode, DestinationNode);                num2 += (Demand * ODCost);            }        }        // 计算间隙函数        UEGap = 1 - num2 / num1;        return UEGap;    }    // 计算最优步长的求根函数的左边    public double OptimalStepFunction(double[] FeasibleDescent, double[] LinkFlow, double paraLamuda)    {        CLink pLink;        double Value = 0;        for (int link = 0; link < m_nLink; link++)        {            pLink = m_Link[link];            double para = LinkFlow[link] + paraLamuda * FeasibleDescent[link];            Value += FeasibleDescent[link] * (pLink.FreeFlowTravelTime * (1 + pLink.Alpha * Math.Pow(para / pLink.Capacity, pLink.Power)));        }        return Value;    }

5.2 代码文件Program的编写

在前述Network文件中编写的是Frank-Wolfe算法的计算过程,要使Network文件中的代码块能被调用且顺利运行,还需要设置程序的入口,即main()函数。Program文件中main()函数的操作包括用ReadNode(),ReadLink(),ReadODPairs()读取具体文件,和运行Frank-Wolfe算法。

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace WT_TrafficAssignment{    class Program    {        static void Main(string[] args)        {            CNetwork Network = new CNetwork();            // 读取文件            Network.ReadNode("C:\\……\\Node.txt");            Network.ReadLink("C:\\……\\Link.txt");            Network.ReadODpairs("C:\\……\\ODPairs.txt");            // 运行FW算法            Network.FrankWolfeAlgorithm();            Console.ReadKey();        }    }}

6 案例与结果

点击阅读原文获取案例中的数据!!!

6.1 样例网络

先在一个简单的样例网络上检验Frank-Wolf算法程序的计算结果,样例网络如下图所示。

9db5de3838a1811fb65e5eae1546f52a.png

(1)计算结果

代码输出的迭代情况、Gap函数值、及UE平衡的路段流量如下。可见模型经过一次迭代,Gap函数的精度满足要求而停止计算。

bfbded3c98d17c3d8999bfc015f30145.png

UE平衡时的路段流量见下图。

c8e2765c620f1c4e6f49e178ccb0123a.png

(2)正确性检验

OD对(2,3):只有唯一的路径,不做检验;

OD对(1,4):有3条可行路径,分别为:1-2-4,1-3-4,1-2-3-4,计算平衡路段流量条件下的走行时间:

0125e347676215f50d451219d9541aba.png

被采用的路径1和路径2平衡路段流量条件下的走行时间近似相等,而未被采用的路径3走行时间T3明显大于T1和T2,满足UE平衡条件。

6.2 Sioux Falls网络

算法在比较复杂的Sioux Falls网络中经过10044次迭代达到UE平衡,平衡时的路段流量如下图所示(流量大小的数字标签更接近O点):

1ea09de4aa909b4ddc3aada0f0589e79.png

用Frank-wolf算法求解UE交通分配问题的收敛过程如下。

ee1542fc7d0734b1b9492d8c46b25ac2.png

参考:

《城市动态交通流分配模型与算法》高自友,任华玲著。

《交通网络优化计算机编程讲义》

《最优化与最优控制》

阅读原文链接:

提取码:qy7s

编辑:庄桢

0313d23c342975e095b52b93ae3d297f.gif

“交通科研Lab”:分享学习点滴,期待科研交流!

400723500b03a6cde8245cfcc493f45a.png

如果觉得还不错

点这里???

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值