题解 P2457 【[SDOI2006]仓库管理员的烦恼】
\(\quad\)每个仓库中只放一种物品,同种物体必须放在同一个仓库里,有 \(n\) 个仓库,\(n\) 种物品,转移物品的代价是其数量,求满足条件的最小代价。
\(\quad\)这题简直就是模板题,很适合练习二分图最大权的 \(KM\) 算法和最小费用最大流 \(EK\) 算法。
\(\quad\)两种方法我都会介绍并贴出代码,想看 \(EK\) 算法的可以直接跳过 \(KM\) 算法
\(\quad\)这种题型不是很多,没有做过的可以先看看P1559 运动员最佳匹配问题,就是一道基础的KM算法模板题,另外这个博客写的不错,很适合学习。
\(\quad\)首先KM算法的正确性基于以下定理:
\(\quad\)若由二分图中所有满足 的边 构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。
\(\quad\)KM算法的正确性:
-
KM算法要求的是图中最大权匹配是完备匹配也就是说都匹配上了。我想这个条件要不是题目中自己给出了,要不就是边权都是正值且每个点想其他点都有连边如本题,此时这个性质是可以被保证的。
-
这个算法是围绕着顶点的定标匹配的我来定性的描述这个算法的过程:首先每个点都和自己最大的边权进行匹配,然后发现有些点没有匹配对象的话更换交错树中定标的值然后再次寻找增广路。当然新能沟通的路是边权变化最小的。
-
经过我长期的研究我终于把我的反例证明出来了我的意思是指是否存在一种情况使得当前直接点匹配上比两个已匹配边更换匹配然后是当前点得到匹配更优,这个主意很容易走到这个误区经过我画的多张图我发现出现这种情况的是不存在完备匹配的情况的否则皆可以利用KM网上的证明方法来证明。
\(\quad\)Kuhn-Munkres算法流程:
- 初始化可行顶标的值;
- 用匈牙利算法寻找完备匹配;
- 若未找到完备匹配则修改可行顶标的值;
- 重复(2)(3)直到找到相等子图的完备匹配为止。
\(\quad\)另外KM算法求的是最大匹配下的最大值,本题要求最小代价,所以边权要取反。
\(\quad\)可以看看代码的注释,感觉不是很好理解(当时新学的时候感觉很难)。
#include<iostream>//KM算法
#include<cstdio>
#include<cmath>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#define int long long
#define re register int
#define il inline
#define inf 1e18+5
#define next nee
using namespace std;
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=155;
int n,a[N][N];
int minz,lx[N],ly[N];//顶标
int w[N],b[N][N];//边权
int match[N];//match[i]表示第i个仓库的匹配对象
bool visx[N],visy[N];//标记,每个仓库只走一遍
il bool dfs(int x)
{
visx[x]=1;
for(re i=1;i<=n;i++)
if(!visy[i]){//未访问过
int t=lx[x]+ly[i]-b[x][i];
if(t==0){//满足平衡条件
visy[i]=1;
if(match[i]==0||dfs(match[i]))
{
match[i]=x;return 1;
}
}
else if(t>0)minz=min(minz,t);//更新最小修改值
}
return 0;
}
il void KM()
{
for(re i=1;i<=n;i++)
{
while(1)
{
memset(visx,0,sizeof(visx));//清零
memset(visy,0,sizeof(visy));//清零
minz=inf; //初始化为inf
if(dfs(i))break;//找到就下一个,直到找到为止
for(re j=1;j<=n;j++)
{
if(visx[j])lx[j]-=minz;//降低x的要求
if(visy[j])ly[j]+=minz;//增加y的要求
}
}
}
}
signed main()
{
n=read();
for(re i=1;i<=n;i++)
{
lx[i]=-inf;//初始化为极小值
for(re j=1;j<=n;j++)a[i][j]=read(),w[j]+=a[i][j];
}
for(re i=1;i<=n;i++)for(re j=1;j<=n;j++)
{
b[i][j]=-w[i]+a[j][i];//记得取反
lx[i]=max(lx[i],b[i][j]);//顶标初始化
}
KM();int ans=0;
for(re i=1;i<=n;i++)ans+=b[match[i]][i];//统计答案
print(-ans);
return 0;
}
\(\quad\)没做过的建议先做模板,其实一样简单(P3381 【模板】最小费用最大流)
\(\quad\)EK算法流程:
- 在残余网络上寻找最短路
- 对该路径进行增广, 对答案产生贡献
- 不断重复opt.1操作, 直至s\to ts→t不存在路径
\(\quad\)其实就是把网络流的 \(bfs\) 换成了 \(SPFA\),一次只找一条增广路,把代价看做距离,另外建一个超级源点 \(s\) 和超级汇点 \(t\),\(s\) 连向所有代表物品的点 \((1...n)\),没有代价,所有代表仓库的点 \((n+1...2n)\) 连向 \(t\),没有代价,所有物品和仓库两两相连,流量都为 \(1\) 即可(其他题解已经介绍的很详细了)。
\(\quad\)这里 \(SPFA\) 也可以换成 \(Dijkstra\),只不过费用流会有负边权,需要加上势,稍微有点麻烦。
\(\quad\)然后直接跑费用流就 \(OK\) 了。
#include<iostream>//EK算法
#include<cstdio>
#include<cmath>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#define int long long
#define re register int
#define il inline
#define inf 1e18+5
#define next nee
using namespace std;
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=250;
int n,s,t,a[N][N],maxcost,w[N],dis[N<<1],pre[N<<1];
int next[N*N*2],go[N*N*2],head[N<<1],tot=1,d[N*N*2],val[N*N*2];
bool vis[N<<1];
il void Add(int x,int y,int z,int u)
{
next[++tot]=head[x];
head[x]=tot;go[tot]=y;val[tot]=z;d[tot]=u;
}
il bool SPFA()
{
queue<int>q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));//初始化
int maxn=dis[0];
q.push(s);vis[s]=1;dis[s]=0;
while(!q.empty())
{
int x=q.front();q.pop();vis[x]=0;
for(re i=head[x],y;i,y=go[i];i=next[i])
if(val[i]){
if(dis[y]>dis[x]+d[i])
{
dis[y]=dis[x]+d[i];
pre[y]=i;//记录前驱
if(!vis[y])vis[y]=1,q.push(y);
}
}
}
if(dis[t]==maxn)return 0;//未被跑到
return 1;
}
il void EK()
{
while(SPFA())
{
int x=t;
maxcost+=dis[t];//因为每条路流量为1,没有必要记录流量
while(x!=s){//回流
int i=pre[x];
val[i]-=1;
val[i^1]+=1;
x=go[i^1];
}
}
}
signed main()
{
n=read();s=2*n+1;t=2*n+2;
for(re i=1;i<=n;i++)for(re j=1;j<=n;j++)a[i][j]=read(),w[j]+=a[i][j];//记录每个物品总数量
for(re i=1;i<=n;i++)
{
Add(s,i,1,0);Add(i,s,0,0);
Add(i+n,t,1,0);Add(t,i+n,0,0);
}
for(re i=1;i<=n;i++)for(re j=1;j<=n;j++)
Add(i,j+n,1,w[i]-a[j][i]),Add(j+n,i,0,a[j][i]-w[i]);//建边
EK();
print(maxcost);
return 0;
}