lightoj12221222 - Gift Packing

题意很简单,就是裸的km算法。

//*******************************以下是转载的**********************************************

二分图匹配算法总结 

二分图最大匹配的匈牙利算法

二分图是这样一个图,它的顶点可以分类两个集合XY,所有的边关联在两个顶点中,恰好一个属于集合X,另一个属于集合Y。

最大匹配:图中包含边数最多的匹配称为图的最大匹配。

完美匹配:如果所有点都在匹配边上,称这个最大匹配是完美匹配。 

最小覆盖: 最小覆盖要求用最少的点(X集合或Y集合的都行)让每条边都至少和其中一个点关联。可以证明:最少的点(即覆盖数)=最大匹配数 

最小路径覆盖: 

用尽量少的不相交简单路径覆盖有向无环图G的所有结点。解决此类问题可以建立一个二分图模型。把所有顶点i拆成两个:X结点集中的iY结点集中的i',如果有边i->j,则在二分图中引入边i->j',设二分图最大匹配为m,则结果就是n-m

最大独立集问题: 

在N个点的图G中选出m个点,使这m个点两两之间没有边.求m最大值.

如果图G满足二分图条件,则可以用二分图匹配来做.最大独立集点数 = N - 最大匹配数

一、匈牙利算法

G=(V,{R})是一个无向图。如顶点集V可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属两个不同的子集。则称图G为二分图。

   v       给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。

v       选择这样的边数最大的子集称为图的最大匹配问题(maximal matching problem)

v       如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备匹配。

最大匹配在实际中有广泛的用处,求最大匹配的一种显而易见的算法是:先找出全部匹配,然后保留匹配数最多的。但是这个算法的复杂度为边数的指数级函数。因此,需要寻求一种更加高效的算法。

匈牙利算法是求解最大匹配的有效算法,该算法用到了增广路的定义(也称增广轨或交错轨):若P是图G中一条连通两个未匹配顶点的路径,并且属M的边和不属M的边(即已匹配和待匹配的边)P上交替出现,则称P为相对于M的一条增广路径。

由增广路径的定义可以推出下述三个结论:

v       1.   P的路径长度必定为奇数,第一条边和最后一条边都不属于M

v       2.   P经过取反操作(即非M中的边变为M中的边,原来M中的边去掉)可以得到一个更大的匹配M’

v       3.   MG的最大匹配当且仅当不存在相对于M的增广路径。

从而可以得到求解最大匹配的匈牙利算法:

v       (1)M为空

v       (2)找出一条增广路径P,通过取反操作获得更大的匹配M’代替M

v       (3)重复(2)操作直到找不出增广路径为止

二、KM算法:

二分图最优匹配:对于二分图的每条边都有一个权(非负),要求一种完备匹配方案,使得所有匹配边的权和最大,记做最优完备匹配。(特殊的,当所有边的权为1时,就是最大完备匹配问题)

解二分图最优匹配问题可用穷举的方法,但穷举的效率=n!,所以我们需要更加优秀的算法。

先说一个定理:设M是一个带权完全二分图G的一个完备匹配,给每个顶点一个可行顶标(ix顶点的可行标用lx[i]表示,第jy顶点的可行标用ly[j]表示),如果对所有的边(i,j) in G,都有lx[i]+ly[j]>=w[i,j]成立(w[i,j]表示边的权),且对所有的边(i,j) in M,都有lx[i]+ly[j]=w[i,j]成立,则M是图G的一个最优匹配。

KuhnMunkras算法(即KM算法)流程:

v       (1)初始化可行顶标的值

v       (2)用匈牙利算法寻找完备匹配

v       (3)若未找到完备匹配则修改可行顶标的值

v       (4)重复(2)(3)直到找到相等子图的完备匹配为止

KM算法主要就是控制怎样修改可行顶标的策略使得最终可以达到一个完美匹配,首先任意设置可行顶标(如每个X节点的可行顶标设为它出发的所有弧的最大权,Y节点的可行顶标设为0),然后在相等子图中寻找增广路,找到增广路就沿着增广路增广。而如果没有找到增广路呢,那么就考虑所有现在在匈牙利树中的X节点(记为S集合),所有现在在匈牙利树中的Y节点(记为T集合),考察所有一段在S集合,一段在not T集合中的弧,取       delta = min {l(xi)+l(yj)-w(xi,yj) , | xi in S, yj   in not T}    。明显的,当我们把所有S集合中的l(xi)减少delta之后,一定会有至少一条属于(S, not T)的边进入相等子图,进而可以继续扩展匈牙利树,为了保证原来属于(S,T )的边不退出相等子图,把所有在T集合中的点的可行顶标增加delta。随后匈牙利树继续扩展,如果新加入匈牙利树的Y节点是未盖点,那么找到增广路,否则把该节点的对应的X匹配点加入匈牙利树继续尝试增广。

复杂度分析:由于在不扩大匹配的情况下每次匈牙利树做如上调整之后至少增加一个元素,因此最多执行n次就可以找到一条增广路,最多需要找n条增广路,故最多执行n^2次修改顶标的操作,而每次修改顶标需要扫描所有弧,这样修改顶标的复杂度就是O(n^2)的,总的复杂度是O(n^4)的。

     对于not T的每个元素yj,定义松弛变量slack(yj) =min{l(xi)+l(yj)-w(xi,yj), | xi in S},很明显每次的delta = min{slack(yj), | yj   in not T},每次增广之后用O(n^2)的时间计算所有点的初始slack,由于生长匈牙利树的时候每条弧的顶标增量相同,因此修改每个slack需要常数时间(注意在修改顶标后和把已盖Y节点对应的X节点加入匈牙利树的时候是需要修改slack的)。这样修改所有slack值时间是O(n)的,每次增广后最多修改n次顶标,那么修改顶标的总时间降为O(n^2)n次增广的总时间复杂度降为O(n^3)。事实上这样实现之后对于大部分的数据可以比O(n^4)的算法快一倍左右。

利用二分图匹配的匈牙利算法和KM算法,我们可以求解大部分的关于二分图的问题,它们提供了求解最大匹配和最优匹配的有效算法,在具体编程时我们只要多注意优化,我们就可以得出求解这类问题的有效方法,从而可以对这类实际问题进行有效合理的解决。

另一种说法:

KM算法(转)

      KM算法是通过给每个顶点一个标号(叫做顶标)来把求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B [i],顶点XiYj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j)A[i]+B[j]>=w[i,j]始终 成立。KM算法的正确性基于以下定理:
   若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。
  这个定理是显然的。因为对于二分图的任意一个匹配,如果它包含于相等子图,那么它的边权和等于所有顶点的顶标和;如果它有的边不包含于相等子图,那么它的边权和小于所有顶点的顶标和。所以相等子图的完备匹配一定是二分图的最大权匹配。
   初始时为了使A[i]+B[j]>=w[i,j]恒成立,令A[i]为所有与顶点Xi关联的边的最大权,B[j]=0。如果当前的相等子图没有完备匹配,就按下面的方法修改顶标以使扩大相等子图,直到相等子图具有完备匹配为止。
   我们求当前相等子图的完备匹配失败了,是因为对于某个X顶点,我们找不到一条从它出发的交错路。这时我们获得了一棵交错树,它的叶子结点全部是X顶点。现在我们把交错树中X顶点的顶标全都减小某个值dY顶点的顶标全都增加同一个值d,那么我们会发现:
 两端都在交错树中的边(i,j)A[i]+B[j]的值没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。
 两端都不在交错树中的边(i,j)A[i]B[j]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。
 X端不在交错树中,Y端在交错树中的边(i,j),它的A[i]+B[j]的值有所增大。它原来不属于相等子图,现在仍不属于相等子图。
 X端在交错树中,Y端不在交错树中的边(i,j),它的A[i]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。
   现在的问题就是求d值了。为了使A[i]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于min{A[i]+B[j]-w[i,j]|Xi在交错树中,Yi不在交错树中}
   以上就是KM算法的基本思路。但是朴素的实现方法,时间复杂度为O(n4)——需要找O(n)次增广路,每次增广最多需要修改O(n)次顶 标,每次修改顶标时由于要枚举边来求d值,复杂度为O(n2)。实际上KM算法的复杂度是可以做到O(n3)的。我们给每个Y顶点一个松弛量函数 slack,每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边(i,j)时,如果它不在相等子图中,则让slack[j]变成原值与A [i]+B[j]-w[i,j]的较小值。这样,在修改顶标时,取所有不在交错树中的Y顶点的slack值中的最小值作为d值即可。但还要注意一点:修改 顶标后,要把所有的slack值都减去d

原文:http://www.360doc.com/content/11/0718/14/3701281_134273282.shtml

//*******************************以上是转载的**********************************************

/*****************************************
Author      :Crazy_AC(JamesQi)
Time        :2015
File Name   :
*****************************************/
// #pragma comment(linker, "/STACK:1024000000,1024000000")
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
using namespace std;
#define MEM(x,y) memset(x, y,sizeof x)
#define pk push_back
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int,int> ii;
typedef pair<ii,int> iii;
const double eps = 1e-10;
const int inf = 1 << 30;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
/**********************Point*****************************/
struct Point{
	double x,y;
	Point(double x=0,double y=0):x(x),y(y){}
};
typedef Point Vector;
Vector operator + (Vector A,Vector B){
	return Vector(A.x + B.x,A.y + B.y);
}
Vector operator - (Vector A,Vector B){//向量减法
	return Vector(A.x - B.x,A.y - B.y);
}
Vector operator * (Vector A,double p){//向量数乘
	return Vector(A.x * p,A.y * p);
}
Vector operator / (Vector A,double p){//向量除实数
	return Vector(A.x / p,A.y / p);
}
int dcmp(double x){//精度正负、0的判断
	if (fabs(x) < eps) return 0;
	return x < 0?-1:1;
}
bool operator < (const Point& A,const Point& B){//小于符号的重载
	return A.x < B.x || (A.x == B.x && A.y < B.y);
}
bool operator == (const Point& A,const Point& B){//点重的判断
	return dcmp(A.x - B.x) == 0&& dcmp(A.y - B.y) == 0;
}
double Dot(Vector A,Vector B){//向量的点乘
	return A.x * B.x + A.y * B.y;
}
double Length(Vector A){//向量的模
	return sqrt(Dot(A,A));
}
double Angle(Vector A,Vector B){//向量的夹角
	return acos(Dot(A,B) / Length(A) / Length(B));
}
double Cross(Vector A,Vector B){//向量的叉积
	return A.x * B.y - A.y * B.x;
}
double Area2(Point A,Point B,Point C){//三角形面积
	return Cross(B - A,C - A);
}
Vector Rotate(Vector A,double rad){//向量的旋转
	return Vector(A.x * cos(rad) - A.y * sin(rad),A.x * sin(rad) + A.y * cos(rad));
}
Vector Normal(Vector A){//法向量
	int L = Length(A);
	return Vector(-A.y / L,A.x / L);
}
double DistanceToLine(Point p,Point A,Point B){//p到直线AB的距离
	Vector v1 = B - A,v2 = p - A;
	return fabs(Cross(v1,v2)) / Length(v1);
}
double DistanceToSegment(Point p,Point A,Point B){//p到线段AB的距离
	if (A == B) return Length(p - A);
	Vector v1 = B - A, v2 = p - A,v3 = p - B;
	if (dcmp(Dot(v1,v2) < 0)) return Length(v2);
	else if (dcmp(Dot(v1,v3)) > 0) return Length(v3);
	else return DistanceToLine(p,A,B);
}
bool SegmentProperIntersection(Point A1,Point A2,Point B1,Point B2){//线段相交
	double c1 = Cross(A2 - A1,B1 - A1),c2 = Cross(A2 - A1,B2 - A1);
	double c3 = Cross(B2 - B1,A1 - B1),c4 = Cross(B2 - B1,A2 - B1);
	return dcmp(c1)*dcmp(c2) < 0 && dcmp(c3)*dcmp(c4) < 0;
}
const int N = 510;
struct KM{
	int n, m;
	int g[N][N];//邻接矩阵
	int Lx[N], Ly[N], slack[N];
	int left[N], right[N];
	bool S[N], T[N];

	void Initation(int n,int m) {
		this->n = n,this->m = m;
		memset(g, 0,sizeof g);
	}

	void addedge(int u,int v,int val) {
		g[u][v] += val;
	}

	bool Search(int u) {
		S[u] = true;
		for (int v = 0;v < m;++v) {
			if (T[v]) continue;
			int tmp = Lx[u] + Ly[v] - g[u][v];
			if (!tmp) {//相等子图
				T[v] = true;
				if (left[v] == -1 || Search(left[v])) {
					left[v] = u;
					right[u] = v;
					return true;
				}
			}else slack[v] = min(slack[v], tmp);
		}
		return false;
	}

	void updata() {
		int a = INF;
		for (int i = 0;i < m;++i)
			if (!T[i]) a = min(a, slack[i]);
		for (int i = 0;i < n;++i) 
			if (S[i]) Lx[i] -= a;
		for (int j = 0;j < m;++j)
			if (T[j]) Ly[j] += a;
	}

	int km() {
		memset(left, -1,sizeof left);
		memset(right, -1,sizeof right);
		memset(Ly, 0,sizeof Ly);

		for (int i = 0;i < n;++i) {
			Lx[i] = -INF;
			for (int j = 0;j < m;++j) 
				Lx[i] = max(Lx[i], g[i][j]);
			//开始获取该点出发的边的最大边权。
		}

		for (int i = 0;i < n;++i) {
			for (int j = 0;j < m;++j) 
				slack[j] = INF;

			while(true) {
				memset(S, 0,sizeof S);
				memset(T, 0,sizeof T);
				if (Search(i)) break;
				updata();
			}
		}

		int ans = 0;
		for (int i = 0;i < n;++i)
			ans += g[i][right[i]];
		return ans;
	}
}km;

int n, icase = 0;

void Input() {
	scanf("%d",&n);
	km.Initation(n, n);

	int val;
	for (int i = 0;i < n;++i) {
		for (int j = 0;j < n;++j) {
			scanf("%d",&val);
			km.addedge(i, j, val);
		}
	}
}

int main()
{	
	freopen("in.txt","r",stdin);
	// freopen("out.txt","w",stdout);
	int t;
	scanf("%d",&t);
	while (t--) {
		Input();
		printf("Case %d: %d\n", ++icase, km.km());
	}
	return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值