题目链接
题意:有 N 栋大楼,M 个避难所,每个大楼里有 Bi 个人,每个避难所有 Ci 的容量,人去避难所避难,
现在给你一个避难的计划,问你这个计划是不是最优的,如果不是最优的,那就找一个比这个优的方案,并输出来。
思路:问的是,这个方案是不是最优的,如果不是,输出一个更优的,并不是输出最优的。
我们要知道,最小费用最大流 等价于 跑完图之后,这个图没有 负环。
所以这个题就可以看看 残余网络上 有没有负环,如果没有,那么就是最优的解,如果有负环,在负环上增广一下,那就是更优的解了。
首先我们就要见残余网络的图。
0 源点
1 ~ n 建筑物
n + 1 ~ n + m 避难所
n + m + 1 汇点
1、首先 人要去避难,, 建筑物 到 避难所 要建一条权值为距离的边
2、在给定的计划上,i 到 j 有人避难, 所以要建一条反向边, 权值为 负的。
3、在 j 这个避难所,是不是有避难的人,如果有,建一条从 汇点 到 j 的权值为 0 的边。
为什么?我的想法是, 人可以从 j 走到汇点,然后就要再一条 反向边。
如果 避难的人 没有超过当前避难所的容量,那就要建一条 从 避难所 到 汇点 的 权值 为 0 的边。
为什么? 因为,还有人可以从避难所走到汇点。
避难所就相当于汇点,不过避难所有多个,所以要一个超级汇点把避难所连接起来。
如果到了避难所,就相当于到了汇点,避难所有人,相当于到了汇点,然后建一条反向边,
如果避难所的人等于避难所的容量,相当于都到了汇点,就不会有人 还能从避难所到汇点,这种情况就不能建一条从避难所
到汇点的边,否则就建一条从避难所到汇点的边。
之后就是,SPFA 判断负环,如果有一个点的入度 > 点的总数,说明有负环,那就退出来,
但是这个点不一定在负环中,所以我们就要找到负环,
从当前点找前驱,如果有一个点的出现过了,说明这个点在负环中。
然后就是在负环中增广, 改变流量,找最优值。
这个就是看负环的流向了。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
#define mem(x,v) memset(x,v,sizeof(x))
const int INF = 0x3f3f3f3f;
const int M = 1000000;
const int N = 500;
struct edge{
int u,v,w,cost;
}f[M];
struct node{
int x,y,z;
}bu[250],sh[250];
int cnt = -1,Next[M],pre[N],dis[N],head[N],len[250][250]; //len 表示距离
int Map[250][250],c[250],du[250]; // 存原图,以及答案。
bool vis[N];
int n,m,s,t,sum = 0;
void _Add(int u, int v, int cost){
cnt++;
Next[cnt] = head[u];
head[u] = cnt;
f[cnt].u = u;
f[cnt].v = v;
f[cnt].cost = cost;
return;
}
void init() {
int z;
scanf("%d%d", &n, &m);
s = 0; t = n + m + 1;
mem(head,-1);
for (int i = 1; i <= n; i++)
scanf("%d%d%d", &bu[i].x, &bu[i].y, &bu[i].z);
for (int i = 1; i <= m; i++)
scanf("%d%d%d", &sh[i].x, &sh[i].y, &sh[i].z);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
len[i][j] = abs(bu[i].x - sh[j].x) + abs(bu[i].y - sh[j].y) + 1;//求建筑和避难所的距离。
_Add(i, n + j, len[i][j]);//然后就要建边,残余网络。
}
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
scanf("%d",&z);
c[j] += z;
Map[i][j] = z;
if (z > 0) _Add(n + j, i, -len[i][j]);//从 i 建筑到 j 避难所,要建一条反向边,权值为负的。
}
}
for (int i = 1; i <= m; i++){
if (c[i] > 0) _Add(t,n+i,0); //避难所有人,就要建一条从汇点到避难所的反向边,权值为0.因为可以从避难所走到汇点。
if (c[i] < sh[i].z) _Add(n + i, t, 0);//避难所的人没有超过最大限度,所以要建一条边到 汇点,权值为 0;
}
return;
}
int spfa(){
mem(pre,-1);//前驱
mem(dis,INF);//求距离,
mem(vis,0);
mem(du,0); //入度;
dis[t] = 0;
queue<int>q;
while (!q.empty()) q.pop();
q.push(t);
vis[t] = 1;
du[t] = 1;
while(!q.empty()){
int u = q.front(); q.pop();
vis[u] = 0;
for (int i = head[u];i != -1; i = Next[i]){
int v = f[i].v;
if (dis[v] > dis[u] + f[i].cost){
dis[v] = dis[u] + f[i].cost;
pre[v] = u;
if (du[v] > n) return v;
if (vis[v] == 0){
du[v]++;
if (du[v] > t) return v;//一开始,t 写成了 n; 照样过了。判断负环要 > t;
vis[v] = 1;
q.push(v);
}
}
}
}
return -1;
}
int main() {
init();
int ans = spfa();
if (ans == -1) printf("OPTIMAL\n"); else {
printf("SUBOPTIMAL\n");
int now = ans;
int last;
mem(vis,0);
while(!vis[now]){//找负环,spfa 出来的点不一定在负环上,所以要找负环上的点。
vis[now] = 1;
now = pre[now];
} //同一个点出现两次,就说明这个点在负环上。
ans = now;
last = pre[now];
do{ //在负环上增广,看负环流向,从建筑物流到避难所 ++, 从避难所流到建筑物 --。
//printf("%d %d\n",last,now);
if (last > 0 && last <= n && now > n) Map[last][now-n]++; else
if (last > n && now <= n && now > 0) Map[now][last-n]--;
now = last;
last = pre[now];
}while(now != ans);
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++)
printf("%d%c",Map[i][j], j == m ? '\n' :' ');
}
}
return 0;
}