代码平台
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);
}
}
}