1. 程式人生 > 其它 >題解 P2457 【[SDOI2006]倉庫管理員的煩惱】

題解 P2457 【[SDOI2006]倉庫管理員的煩惱】

\[\texttt{題目大意} \]

\(\quad\)每個倉庫中只放一種物品,同種物體必須放在同一個倉庫裡,有 \(n\) 個倉庫,\(n\) 種物品,轉移物品的代價是其數量,求滿足條件的最小代價。

\(\quad\)這題簡直就是模板題,很適合練習二分圖最大權的 \(KM\) 演算法和最小費用最大流 \(EK\) 演算法。

\(\quad\)兩種方法我都會介紹並貼出程式碼,想看 \(EK\) 演算法的可以直接跳過 \(KM\) 演算法

\[\texttt{KM 演算法} \]

\(\quad\)這種題型不是很多,沒有做過的可以先看看P1559 運動員最佳匹配問題,就是一道基礎的KM演算法模板題,另外這個部落格寫的不錯,很適合學習。

\(\quad\)首先KM演算法的正確性基於以下定理:

\(\quad\)若由二分圖中所有滿足 的邊 構成的子圖(稱做相等子圖)有完備匹配,那麼這個完備匹配就是二分圖的最大權匹配。

\(\quad\)KM演算法的正確性:

  1. KM演算法要求的是圖中最大權匹配是完備匹配也就是說都匹配上了。我想這個條件要不是題目中自己給出了,要不就是邊權都是正值且每個點想其他點都有連邊如本題,此時這個性質是可以被保證的。

  2. 這個演算法是圍繞著頂點的定標匹配的我來定性的描述這個演算法的過程:首先每個點都和自己最大的邊權進行匹配,然後發現有些點沒有匹配物件的話更換交錯樹中定標的值然後再次尋找增廣路。當然新能溝通的路是邊權變化最小的。

  3. 經過我長期的研究我終於把我的反例證明出來了我的意思是指是否存在一種情況使得當前直接點匹配上比兩個已匹配邊更換匹配然後是當前點得到匹配更優,這個主意很容易走到這個誤區經過我畫的多張圖我發現出現這種情況的是不存在完備匹配的情況的否則皆可以利用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;
}
\[\texttt{EK 演算法} \]

\(\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;
}