A new Graph Game
题目链接:Click Here~
题目分析:
给你一张图,图上可能有多个哈密顿回路。叫你求出形成多个哈密顿回路的总距离最小值,即转换成最小花费。为什么形成回路还会有最小值呢?因为这就是费用流有时候不能用邻接矩阵的原因了。费用流一般都会有平行边的存在,所以一般都是用邻接表。
哈密顿图:每个点只能经过有且仅有一次。
欧拉回路:每条边只能有且仅有经过一次。
算法分析:
既然知道了是费用流,当然一遍费用流算法就可以了。但是这题的边密,点多。所以很容易超时,我就在这上面花了好多时间,写了个输入外挂才险过了。现在就开始写KM吧,不然下次比赛超时就不好了^ _ ^
建模分析:
这一题,跟普通的建模方法一样都是拆点,建立超级点。这题有一个需要注意的是,图是无向图,所以要建立边两次,一开始我就一直卡在这里了。但是,经过这题我终于弄懂了,为什么求圈的时候可以直接拆点用费用流了。
下面举两个例子简单的证明一下:
设汇点s,源点t.
----> 输入个边的关系: 拆点建图:
1 2 (1, 2')
2 3 (2, 3')
3 1 (3, 1')
(s,1) (s,2) (s,3)
(1',t) (2',t) (3',t)
----->不知道你发现上面的规律了没有,可能一时难一发现。其实,这就是跟哈密顿回路的性质有关了。因为每个点必须经过一次,且只能是一次。所以你拆点之后,实质上每个点是一一对应的关系的。每个点只能对一个点,且如果存在哈密顿回路的话。每个点必须有一个对立的点链接。这就是为什么费用流有时候也可以用二分匹配的原因了,因为费用流的本质思想我个人认为其实就是二分匹配。所以,最后我检验是否有回路的时候,只要检查是否每个点都有可匹配的点就可以了。即最大流为n。
先给个搓搓的代码,只有c++提交才能过^_^
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <cstdio>
#include <cstring>
using namespace std;
const int INF = ~0U>>2;
const int MAXN = 2e3+10;
struct Edge{
int from,to,cap,flow,cost;
Edge(int f,int t,int c,int _f,int _c)
:from(f),to(t),cap(c),flow(_f),cost(_c){}
};
class MCMF{
public:
void Init(int n)
{
s = 0; t = n+n+1;
for(int i = 0;i <= n;++i)
f[i] = i;
for(int i = 0;i <= t;++i)
G[i].clear();
edges.clear();
}
int Read()
{
char ch = getchar();
while(!isdigit(ch))ch = getchar();
int sum = 0;
while(isdigit(ch)){
sum *= 10;
sum += ch-'0';
ch = getchar();
}
return sum;
}
void AddEdge(int from,int to,int cap,int cost)
{
edges.push_back(Edge(from,to,cap,0,cost));
edges.push_back(Edge(to,from,0,0,-cost));
int sz = edges.size();
G[from].push_back(sz-2);
G[to].push_back(sz-1);
}
// int Find(int x);
// void Union(int u,int v);
// bool Judege(int u,int v);
void Solve(int n,int m)
{
Init(n);
int x,y,c;
for(int i = 1;i <= n;++i){
AddEdge(s,i,1,0);
AddEdge(i+n,t,1,0);
}
for(int i = 0;i < m;++i){
x = Read();
y = Read();
c = Read();
// Union(x,y);
AddEdge(x,y+n,1,c);
AddEdge(y,x+n,1,c);
}
}
int Mincost(int& flow,int cost)
{
while(BellmanFord(flow,cost));
return cost;
}
bool BellmanFord(int& flow,int& cost)
{
for(int i = 0;i <= t;++i)d[i] = INF;
memset(inq,false,sizeof(inq));
inq[s] = true;d[s] = 0;a[s] = INF;p[s] = 0;
queue<int> Q;
Q.push(s);
while(!Q.empty()){
int u = Q.front();
Q.pop();
inq[u] = false;
for(int i = 0;i < (int)G[u].size();++i){
Edge& e = edges[G[u][i]];
if(e.cap > e.flow&&d[e.to] > d[u]+e.cost){
d[e.to] = d[u]+e.cost;
p[e.to] = G[u][i];
a[e.to] = min(a[u],e.cap-e.flow);
if(!inq[e.to]){Q.push(e.to);inq[e.to] = true;}
}
}
}
if(d[t]==INF)return false;
int u = t;
flow += a[t];
cost += a[t]*d[t];
while(u != s){
edges[p[u]].flow += a[t];
edges[p[u]^1].flow -= a[t];
u = edges[p[u]].from;
}
return true;
}
private:
vector<Edge> edges;
vector<int> G[MAXN];
int n,m,s,t;
int f[MAXN],d[MAXN],p[MAXN],a[MAXN];
bool inq[MAXN];
};
//inline int MCMF::Find(int x)
//{
// if(f[x]==x)
// return f[x];
// return f[x] = Find(f[x]);
//}
//inline void MCMF::Union(int u,int v)
//{
// int a = Find(u),b = Find(v);
// if(a != b)
// f[a] = b;
//}
//bool MCMF::Judege(int u,int v)
//{
// return Find(u)==Find(v);
//}
MCMF mcmf;
int main()
{
int T,n,m;
scanf("%d",&T);
for(int kase = 1;kase <= T;++kase)
{
scanf("%d%d",&n,&m);
mcmf.Solve(n,m);
int cost = 0,flow = 0;
cost = mcmf.Mincost(flow,cost);
if(flow==n){
printf("Case %d: %d\n",kase,cost);
}
else
printf("Case %d: NO\n",kase);
}
return 0;
}
刚学的EK算法,因为KM是O(N^4)的所以懒得写了。对KM算法的理解,我个人学习的时候感觉是在松弛的时候有点不明白,后来通过反复的查找资料。终于弄懂了,好开心 ^_^
为什么在松弛的时候要把左自己的S[]集合的数减去a呢?而又把T[]集合的数加上a呢?
这是因为我们之所以,会把顶标加到S[]和T[]中。是因为L(x)+L(y) = w(x,y),显然,这时候如果我们把L(x)的值减了一个a,此时的等式显然不等。所以为了让等式相等,我们就要把L(y)加上a。这就是我对这个松弛操做的理解。因为,我学习算法的资料最要是LRJ的书。所以,用的名词都是那书上的,我就不解释那名词的意思了,如果不懂得话,自己在看别的资料吧。
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <cstdio>
#include <cstring>
using namespace std;
const int INF = ~0U>>2;
const int MAXN = 1e3 + 5;
const int MAXM = 1e4 + 5;
int top,Head[MAXN],Next[2*MAXM],Key[2*MAXM],W[2*MAXM];
int n,m,slack,Lx[MAXN],Ly[MAXN],Left[MAXN],Sum[MAXN];
bool T[MAXN],S[MAXN];
void AddEdge(int u,int v,int c)
{
int e;
for(e = Head[u];e != -1;e = Next[e])
if(Key[e]==v)break;
if(e==-1){
Key[top] = v;
W[top] = c;
Next[top] = Head[u];
Head[u] = top++;
return;
}
if(W[e] < c)
W[e] = c;
}
void Init()
{
int x,y,c;
top = 0;
memset(Head,-1,sizeof(Head));
for(int i = 0;i < m;++i){
scanf("%d%d%d",&x,&y,&c);
AddEdge(x,y,-c);
AddEdge(y,x,-c);
}
}
bool Match(int u)
{
int e,tmp;
S[u] = true;
for(e = Head[u];e != -1;e = Next[e]){
int v = Key[e];
if(!T[v]){
tmp = Lx[u]+Ly[v]-W[e];
if(tmp == 0){
T[v] = true;
if(Left[v]==-1||Match(Left[v])){
Left[v] = u;
Sum[v] = W[e];
return true;
}
}
else if(tmp < slack)
slack = tmp;
}
}
return false;
}
void Update(int slack)
{
for(int i = 1;i <= n;++i){
if(S[i]) //一增一减,保持L(x)+L(y) = W(x,y)的值不变
Lx[i] -= slack;
if(T[i])
Ly[i] += slack;
}
}
bool EK()
{
int u,e;
for(u = 1;u <= n;++u){
Lx[u] = Ly[u] = 0;
for(e = Head[u];e != -1;e = Next[e])
Lx[u] = max(Lx[u],W[e]);
}
memset(Left,-1,sizeof(Left));
for(int u = 1;u <= n;++u){
for(;;){
slack = INF;
memset(S,false,sizeof(S));
memset(T,false,sizeof(T));
if(Match(u))
break;
if(slack == INF)
return false;
Update(slack);
}
}
return true;
}
int GetAns()
{
int ans = 0;
for(int i = 1;i <= n;++i)
ans += -Sum[i];
return ans;
}
int main()
{
// freopen("Input.txt","r",stdin);
// freopen("Out.txt","w",stdout);
int T;
scanf("%d",&T);
for(int kase = 1;kase <= T;++kase){
scanf("%d%d",&n,&m);
Init();
if(EK())
printf("Case %d: %d\n",kase,GetAns());
else
printf("Case %d: NO\n",kase);
}
return 0;