C#实现指派问题的匈牙利算法(运筹学)

代码平台

VS2019(32位) + Office2019(64位)

注意事项

1.运行过程中出现未注册JET.OLEDB.12.0错误信息,需要下载AccessDatabaseEngine数据访问组件,2010版本即可,下载时需要注意,AccessDatabaseEngine位数应该与office位数一致!!
2.本程序只解决行列相等的矩阵对应的指派问题,不考虑因行列不相等而引入的虚拟矩阵;
3.本程序的费用矩阵存储于Excel,存储格式为:
费用矩阵存储格式
4.代码可复制,根据具体情况,仅修改static void Main(string[] args)函数中的Excel表绝对路径strPath和Sheet表名sheetName字段即可运行;
5.步骤1~4详细列出,是为了防止小白不会用,对于大佬,完全可以读懂程序,随意修改;
6.本代码不是本人原创,参考来源https://blog.csdn.net/weixin_33939380/article/details/85401031,在此感谢博主!
7.初心不变,以便以后查看。代码若有错误之处,望路过的大佬指正,感谢!

代码

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Data;
using System.Data.OleDb;

namespace Hungarian_Algorithm
{
    class Program
    {
        static void Main(string[] args)
        {
            //Excel表绝对路径
            string strPath = @"E:\VS2019\C#\Winform\LuoPeng_Algorithm\obj\Debug\DataSheets.xlsx";
            //Excel表中的数据表名称
            string sheetName = "ExpenseData";

            Console.WriteLine("程序开始");
            DataTable dt = new DataTable();
            DataFromExcel dataFromExcel = new DataFromExcel(sheetName, strPath);
            dt = dataFromExcel.dt;
            Hungarian H = new Hungarian(dt.Rows.Count, dt.Rows.Count, dt);

            H.Calculation();
            for (int i = 0; i < H.listResult.Count; i++)
            {
                Console.WriteLine("{0}{1}", H.listResult[i].X, H.listResult[i].Y);
                Console.WriteLine();
            }


            Console.WriteLine("程序结束");
            Console.ReadLine();
        }
    }

    /// <summary>
    /// 匈牙利算法--运输指派问题类
    /// </summary>
    class Hungarian
    {
        private double[,] expenseData;//费用矩阵
        public List<Point> listResult = new List<Point>();//最优解集
        private int rowX;//矩阵的行数
        private int rowY;//矩阵的列数

        /// <summary>
        /// 构造函数--初始化费用矩阵
        /// </summary>
        /// <param name="rowx">矩阵行数</param>
        /// <param name="rowy">矩阵列数</param>
        public Hungarian(int rowx, int rowy, DataTable dataTable)
        {
            rowX = rowx;
            rowY = rowy;
            expenseData = new double[rowX, rowY];
            //填充费用矩阵
            for (int i = 0; i < dataTable.Rows.Count; i++)
            {
                for (int j = 0; j < dataTable.Columns.Count-1; j++)
                {
                    expenseData[i, j] = Convert.ToDouble(dataTable.Rows[i][j+1]);
                }
            }
        }
        /// <summary>
        /// Hungarian算法迭代求解
        /// </summary>
        public void Calculation()
        {
            Step1();
            while (!Step2())
            {
                Step3();
            }
        }

        /// <summary>
        /// Hungarian算法第一步:行、列找最小值,然后行、列分别减去该最小值,得到等效矩阵
        /// </summary>
        private void Step1()
        {
            //行操作
            for (int x = 0; x < rowX; x++)
            {
                double minX = double.MaxValue;
                //找到每行最小的值
                for (int y = 0; y < rowY; y++)
                {
                    if (expenseData[x, y] < minX)
                    {
                        minX = expenseData[x, y];
                    }
                }
                //让该行所有元素减去该行最小值
                for (int y = 0; y < rowY; y++)
                {
                    expenseData[x, y] -= minX;
                }
            }
            //列操作
            for (int y = 0; y < rowY; y++)
            {
                double minY = double.MaxValue;
                //找到每列最小的值
                for (int x = 0; x < rowX; x++)
                {
                    if (expenseData[x, y] < minY)
                    {
                        minY = expenseData[x, y];
                    }
                }
                //让该列所有元素减去该列最小值
                for (int x = 0; x < rowX; x++)
                {
                    expenseData[x, y] -= minY;
                }
            }
        }

        /// <summary>
        /// Hungarian算法第二步:检验各行,对碰上的第一个零做True记号,同列其余零元素也做True记号;
        /// </summary>
        /// <returns>True:找到最优值;  False:未找到最优值</returns>
        private bool Step2()
        {
            listResult.Clear();//最优解集合清零
            bool[,] isDelete = new bool[rowX, rowY];//标记元素是否为删除状态
            List<ZZeroNode> zeroNodes = new List<ZZeroNode>();//存储包含零元素的各行索引及零元素个数
            //填充zeroNodes
            for (int x = 0; x < rowX; x++)
            {
                int zeroNum = 0;
                for (int y = 0; y < rowY; y++)
                {
                    if (expenseData[x, y] == 0)
                    {
                        zeroNum++;
                    }
                }
                if (zeroNum > 0)
                {
                    zeroNodes.Add(new ZZeroNode(x, zeroNum));
                }
            }
            //按零元素个数对zeroNodes进行排序
            zeroNodes.Sort(ZZeroNode.Cmp);
            //从零较少的行开始,寻找独立零元素,填充listResult
            while (zeroNodes.Count > 0)
            {
                ZZeroNode node = zeroNodes[0];

                if (node.ZeroNum <= 0)
                {
                    zeroNodes.RemoveAt(0);
                }
                else
                {
                    for (int y = 0; y < rowY; y++)
                    {
                        if (expenseData[node.X, y] == 0 && !isDelete[node.X, y])
                        {
                            listResult.Add(new Point(node.X, y));
                            zeroNodes.RemoveAt(0);
                            //删除与该零在同一列的其他零
                            for (int x = 0; x < rowX; x++)
                            {
                                if (expenseData[x, y] == 0)
                                {
                                    isDelete[x, y] = true;
                                    for (int i = 0; i < zeroNodes.Count; i++)
                                    {
                                        if (zeroNodes[i].X == x)
                                        {
                                            zeroNodes[i].ZeroNum--;
                                        }
                                    }
                                }
                            }
                            break;
                        }
                    }
                }
                zeroNodes.Sort(ZZeroNode.Cmp); //添加方法给委托,进行排序
            }
            return listResult.Count == rowX;
        }

        /// <summary>
        /// Hungarian算法第三步:找出最少数目的垂直与水平删除线来包含所有的零至少一次
        /// </summary>
        private void Step3()
        {
            bool[,] isDelete = new bool[rowX, rowY];//意义同上
            for (int x = 0; x < rowX; x++)
            {
                for (int y = 0; y < rowY; y++)
                {
                    if (expenseData[x, y] == 0 && !isDelete[x, y])
                    {
                        int xc = 0;//记录y列零元素个数
                        int yc = 0;//记录x行零元素个数
                        //y列中其余零元素个数之和
                        for (int nx = 0; nx < rowX; nx++)
                        {
                            if (nx != x && expenseData[nx, y] == 0)
                            {
                                xc++;
                            }
                        }
                        //x行中其余零元素个数之和
                        for (int ny = 0; ny < rowY; ny++)
                        {
                            if (ny != y && expenseData[x, ny] == 0)
                            {
                                yc++;
                            }
                        }
                        //将最多零个数的列标记为True
                        if (xc > yc)
                        {
                            for (int xx = 0; xx < rowX; xx++)
                            {
                                isDelete[xx, y] = true;
                            }
                        }
                        //将最多零个数的行标记为True
                        else
                        {
                            for (int yy = 0; yy < rowY; yy++)
                            {
                                isDelete[x, yy] = true;
                            }
                        }
                    }
                }
            }
            //找出未被划线的元素中的最小值
            double k = double.MaxValue;
            for (int x = 0; x < rowX; x++)
            {
                for (int y = 0; y < rowY; y++)
                {
                    if (!isDelete[x, y])
                    {
                        if (expenseData[x, y] < k)
                        {
                            k = expenseData[x, y];
                        }
                    }
                }
            }
            //未被划线各行所有元素减去最小值k
            for (int x = 0; x < rowX; x++)
            {
                for (int y = 0; y < rowY; y++)
                {
                    if (!isDelete[x, y])
                    {
                        for (int y1 = 0; y1 < rowY; y1++)
                        {
                            expenseData[x, y1] -= k;
                        }
                        break;
                    }
                }
            }
            //若造成负值,则将该列加上K,形成新矩阵后回到Step2
            for (int x = 0; x < rowX; x++)
            {
                for (int y = 0; y < rowY; y++)
                {
                    if (expenseData[x, y] < 0)
                    {
                        for (int x1 = 0; x1 < rowX; x1++)
                        {
                            expenseData[x1, y] += k;
                        }
                        break;
                    }
                }
            }

        }
    }


    /// <summary>
    /// 行零数量类
    /// </summary>
    class ZZeroNode
    {
        public int X;//行索引
        public int ZeroNum;//X行所含零的个数

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="x">行索引</param>
        /// <param name="zeroNum">零个数</param>
        public ZZeroNode(int x, int zeroNum)
        {
            X = x;
            ZeroNum = zeroNum;
        }

        /// <summary>
        /// 比较函数(若a小于b,则比较结果返回小于0的值;若a等于b,则比较结果返回等于0的值;若a大于b,则比较结果返回大于0的值)
        /// </summary>
        /// <param name="a">比较值1</param>
        /// <param name="b">比较值2</param>
        /// <returns>比较结果(整数值)</returns>
        public static int Cmp(ZZeroNode a, ZZeroNode b)
        {
            return a.ZeroNum.CompareTo(b.ZeroNum);
        }
    }

    /// <summary>
    /// 从Excel中读取费用矩阵
    /// </summary>
    class DataFromExcel
    {
        public DataTable dt = new DataTable();//供外部调用的数据表

        private string sheetName;//DataSheets中的表名

        /// <summary>
        /// 连接EXCEL并读取数据
        /// </summary>
        /// <param name="filePath">数据表路径</param>
        /// <returns>数据集</returns>
        private DataTable LoadDataFormExcel(string filePath)
        {
            string strConn;//连接字符串
            ///Provider=Microsoft.Ace.OleDa.12.0表示数据源类型
            ///Data Source:数据源绝对路径
            ///Extended Properties为Excel拓展参数:HDR表示第一行是否为字段名,HDR=1表示第一行为字段名,否则无字段名
            ///IMEX表示对同一列中有混合数据类型的列,是统一按字符型处理,还是将个别不同类型的值读为BDNull,1为混合,2为不混合
            strConn = "Provider=Microsoft.Ace.OleDb.12.0;Data Source=" + filePath + ";Extended Properties='Excel 12.0;HDR=Yes;IMEX=1;'";
            OleDbConnection oleConn = new OleDbConnection(strConn);
            DataTable oleDsExcel = new DataTable();
            try
            {
                oleConn.Open();

                string sql = "";
                OleDbDataAdapter oleDaExcel;

                sql = "SELECT * FROM [" + sheetName + "$] where 指标 is not null";

                oleDaExcel = new OleDbDataAdapter(sql, oleConn);
                oleDaExcel.Fill(oleDsExcel);
            }
            catch (Exception err)
            {
              Console.WriteLine("数据绑定Excel失败!!\n失败原因:" + err.Message, "提示信息");
            }
            finally
            {
                oleConn.Close();
            }
            return oleDsExcel;
        }

        /// <summary>
        /// 构造函数
        /// </summary>
        public DataFromExcel(string _sheetName,string _filePath) 
        {
            sheetName = _sheetName;
            dt = LoadDataFormExcel(_filePath);
        }
    }
}

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值