匈牙利算法求解指派问题(C++代码)

前言

匈牙利算法能精确求解指派问题,获取最优分配方案。匈牙利算法求解指派问题基于以下原理:在一个成本矩阵中,对某一行或者某一列加上或减去一个数,最优的分配方案不变。基于此原理,我们可以对成本矩阵进行变换,直到使用试指派能够找到最优解(对一个n*n的成本矩阵而言,找到n个独立0元素)。

一、指派问题

实际中,我们会经常碰到此类问题:有n项任务需要均分给n个工人完成,工人i完成任务j的成本为cij,我们要找到一种分配方案,使得总成本最小。
如下图是一个4员工4任务的指派问题:
指派问题

二、匈牙利算法

对于一个规模为n的指派问题而言,使用匈牙利算法求解的核心是找出n个独立0元素。匈牙利算法的大致流程如下图所示:
匈牙利算法
以前面介绍指派问题给出的数据为例,对匈牙利算法的流程做一个简单介绍,首先给出初始成本矩阵:
初始成本矩阵
对矩阵进行行归约,每行减去该行的最小值,保证每行都出现0元素:
行归约
对矩阵进行列归约,每列减去该列的最小值,保证每列都出现0元素:
列归约
接下来是试指派,检验是否能找到n个独立0元素,否则需要对矩阵进行进一步变换以增加矩阵中的0元素个数。试指派从0元素最少的行或列开始,当确定了cij=0为所选元素,划去第i行和第j列,即不在考虑该区域内的元素。从0元素最少的行开始指派,因此指派的顺序是(行):2->3->1->4,通过试指派找到了最优分配,即员工1执行任务4、员工2执行任务2、员工3执行任务1、员工4执行任务3。

试指派

并不是所有的情况都能通过试指派找到最优分配方案(找到n个独立0元素),此时需要对矩阵进行变换,目的是增加0矩阵中0元素。假设最后得到的矩阵如下所示:
试指派
矩阵变换的目的是使得成本矩阵中出现更多的0元素,我们执行以下操作:
一:打勾操作
(1)对没有独立0元素的行打勾
(2)对打勾行含0元素的列打勾
(3)对打勾列含独立0元素的行打勾
(4)重复(2)(3)直到没有新的勾出现
此步值得注意的是要分清0元素和独立0元素,标红的为本次选择的独立0元素。
二:画线操作
(1)对没有打勾的行和打勾的列进行画线操作

三:矩阵变换
(1)找到未被线覆盖的最小元素
(2)没有画线的行减去该元素
(3)画线的列加上该元素

矩阵变换
变换后的矩阵如下所示,对其进行试指派,可以找到n(此处为4)个独立0元素。
试指派

因此最后得到的分配方案是:员工1执行任务1、员工2执行任务2、员工3执行任务4、员工4执行任务3。

三 、C++代码

为了方便读者参考和使用,本文将匈牙利代码写在一个.cpp文件中,并将员工数量n和成本矩阵c的值存储在.dat文件中,对于不同数据,读者根据具体问题更改.dat文档中的数据即可。

数据文件

数据文件为一个后缀名为.dat的文档,格式如下图所示:
数据文件
其中4为n,其后数据为一个4*4的成本矩阵。

代码

#include<iostream>
#include<algorithm>
#include<fstream>


using namespace std;

int n;//元素个数
int *assign;//分配结果
int **mat;//代价矩阵
int **matRcd;//代价矩阵
int totalCost;//总成本

//-------------------------------------数据读取
bool read(const char* ad)
{
	//数据读取
	ifstream iff;
	iff.open(ad);
	if(!iff)return false;
	iff>>n;
	assign=new int[n];for(int i=0;i<n;i++)assign[i]=-1;
	mat=new int*[n];for(int i=0;i<n;i++)mat[i]=new int[n];
	matRcd=new int*[n];for(int i=0;i<n;i++)matRcd[i]=new int[n];
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			iff>>mat[i][j];
			matRcd[i][j]=mat[i][j];
		}
	}
	iff.close();
	totalCost=0;
	return true;
}

//-------------------------------------行归约
void rowSub()
{
	int *minEmt=new int[n];for(int i=0;i<n;i++)minEmt[i]=(int)1e8;

	for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(mat[i][j]<minEmt[i])minEmt[i]=mat[i][j];

	for(int i=0;i<n;i++)for(int j=0;j<n;j++)mat[i][j]-=minEmt[i];

	delete []minEmt;
}

//-------------------------------------列归约
void columnSub()
{
	int *minEmt=new int[n];for(int j=0;j<n;j++)minEmt[j]=(int)1e8;

	for(int j=0;j<n;j++)for(int i=0;i<n;i++)if(mat[i][j]<minEmt[j])minEmt[j]=mat[i][j];

	for(int j=0;j<n;j++)for(int i=0;i<n;i++)mat[i][j]-=minEmt[j];

	delete []minEmt;
}

//-------------------------------------检验最优
bool isOptimal(int *assign)
{

	int *tAssign=new int[n];for(int i=0;i<n;i++)tAssign[i]=-1;
	int *nZero=new int[n];
	bool *rowIsUsed=new bool[n];
	bool *columnIsUsed=new bool[n];
	for(int i=0;i<n;i++)rowIsUsed[i]=columnIsUsed[i]=0;

	int nLine=0;
	while(nLine<n){
		for(int i=0;i<n;i++)nZero[i]=0;
		for(int i=0;i<n;i++){
			if(rowIsUsed[i]==1)continue;
			for(int j=0;j<n;j++){
				if(columnIsUsed[j]!=1&&mat[i][j]==0)nZero[i]++;
			}
		}

		int minZeros=n;
		int rowId=-1;
		for(int i=0;i<n;i++){
			if(rowIsUsed[i]==0&&nZero[i]<minZeros&&nZero[i]>0){
				minZeros=nZero[i];
				rowId=i;
			}
		}
		if(rowId==-1)break;
		for(int j=0;j<n;j++){
			if(mat[rowId][j]==0&&columnIsUsed[j]==0){
				rowIsUsed[rowId]=1;
				columnIsUsed[j]=1;
				tAssign[rowId]=j;
				break;
			}
		}
		nLine++;
	}
	for(int i=0;i<n;i++)assign[i]=tAssign[i];
	delete []tAssign;
	delete []nZero;
	delete []rowIsUsed;
	delete []columnIsUsed;

	for(int i=0;i<n;i++)if(assign[i]==-1)return false;
	return true;
}

//-------------------------------------矩阵变换
void matTrans()
{
	bool *rowTip=new bool[n];
	bool *columnTip=new bool[n];
	bool *rowLine=new bool[n];
	bool *columnLine=new bool[n];
	for(int i=0;i<n;i++)rowTip[i]=columnTip[i]=rowLine[i]=columnLine[i]=0;

	//打勾
	for(int i=0;i<n;i++)if(assign[i]==-1)rowTip[i]=1;

	while(1){
		int preTip=0;
		for(int i=0;i<n;i++)preTip=preTip+rowTip[i]+columnTip[i];
		for(int i=0;i<n;i++){
			if(rowTip[i]==1){
				for(int j=0;j<n;j++){
					if(mat[i][j]==0)columnTip[j]=1;
				}
			}
		}
		for(int j=0;j<n;j++){
			if(columnTip[j]==1){
				for(int i=0;i<n;i++){
					if(assign[i]==j)rowTip[i]=1;
				}
			}
		}
		int curTip=0;
		for(int i=0;i<n;i++)curTip=curTip+rowTip[i]+columnTip[i];
		if(preTip==curTip)break;
	}
	
	//画线
	for(int i=0;i<n;i++){
		if(rowTip[i]==0)rowLine[i]=1;
		if(columnTip[i]==1)columnLine[i]=1;
	}

	//找最小元素
	int minElmt=(int)1e8;
	for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(rowLine[i]==0&&columnLine[j]==0&&mat[i][j]<minElmt)minElmt=mat[i][j];
	//变换
	for(int i=0;i<n;i++)if(rowTip[i]==1)for(int j=0;j<n;j++)mat[i][j]-=minElmt;
	for(int j=0;j<n;j++)if(columnTip[j]==1)for(int i=0;i<n;i++)mat[i][j]+=minElmt;

	delete []rowTip;
	delete []columnTip;
	delete []rowLine;
	delete []columnLine;
}

//-------------------------------------匈牙利算法
void hungary()
{
	
	read("cost.dat");//读取数据
	rowSub();//行归约
	columnSub();//列归约

	//如果不能找到n个独立的0元素,则对矩阵进行变换
	while(!isOptimal(assign)){
		matTrans();
	}

	for(int i=0;i<n;i++)totalCost+=matRcd[i][assign[i]];
	for(int i=0;i<n;i++)delete []mat[i];delete []mat;
	for(int i=0;i<n;i++)delete []matRcd[i];delete []matRcd;
}

int main()
{
	//调用匈牙利算法
	hungary();

	//输出结果
	cout<<"总成本为"<<totalCost<<endl;
	for(int i=0;i<n;i++)cout<<"员工"<<i+1<<"-->"<<"任务"<<assign[i]+1<<endl;
	
	cin.get();
}

代码中mat为成本矩阵,n为员工或任务数,assign数组为最后的分配方案,totalCost为此分配下的总成本。在主函数中调用匈牙算法函数hungary(),最后输出总成本以及分配方案。接口和结果都清晰明了,且代码按照上文叙述编写,结合注释,应该可以完全看懂。代码编译环境为VS2019,上述.dat文件运行结果如下所示:
运行结果

  • 32
    点赞
  • 137
    收藏
    觉得还不错? 一键收藏
  • 36
    评论
以下是使用匈牙利算法求解二分图最大权完美匹配的 Python 代码: ```python INF = float('inf') def hungarian_algorithm(graph): """ 使用匈牙利算法求解二分图最大权完美匹配 :param graph: 二分图的邻接矩阵表示 :return: 匹配结果,包含每个右侧节点所匹配的左侧节点的编号 """ n = len(graph) match = [-1] * n # 匹配结果,初始全部为-1 slack = [INF] * n # slack数组,记录每个右侧节点在当前匹配中可以被增广的最小权值 visited = [False] * n # 记录左侧节点是否已被访问 def dfs(u): """ 从左侧节点u开始进行增广,返回是否找到增广路 """ visited[u] = True for v in range(n): if not visited[v] and graph[u][v] < INF: gap = slack[v] - graph[u][v] if gap == 0: visited[v] = True if match[v] == -1 or dfs(match[v]): match[v] = u return True elif gap < slack[v]: slack[v] = gap return False # 对每个左侧节点进行增广 for u in range(n): # 初始化slack数组 slack = [INF] * n # 如果当前左侧节点还未匹配,则进行增广 if match[u] == -1: # 找到当前左侧节点能够到达的所有右侧节点的最小权值 min_weight = INF for v in range(n): min_weight = min(min_weight, graph[u][v]) # 如果当前左侧节点不能到达任何一个右侧节点,则无法形成完美匹配 if min_weight == INF: return None # 更新slack数组 for v in range(n): if graph[u][v] < INF: slack[v] = min(slack[v], graph[u][v] - min_weight) # 尝试从当前左侧节点开始增广 while True: visited = [False] * n if dfs(u): break # 如果找不到增广路,则更新slack数组 delta = INF for v in range(n): if not visited[v] and slack[v] < delta: delta = slack[v] for i in range(n): if visited[i]: match[i] = -1 if slack[i] < INF: slack[i] -= delta else: slack[i] = INF return match ``` 其中,二分图的邻接矩阵 `graph` 应为一个 $n \times n$ 的矩阵,第 $i$ 行第 $j$ 列表示从左侧节点 $i$ 到右侧节点 $j$ 的边的权值。如果不存在这条边,则权值应为正无穷(`INF`)。函数返回的匹配结果 `match` 为一个长度为 $n$ 的列表,其中第 $i$ 个元素表示右侧节点 $i$ 所匹配的左侧节点的编号。如果右侧节点 $i$ 没有被匹配,则对应元素的值为 -1。
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值