P6768题解
一 解题算法
本题所用的算法是最短路、二分答案、网络最大流。此处最短路使用 Floyd 算法,最大流使用 Dinic 算法。
二 解题方法
建图:
网络流问题中建图(未知问题已知化)很重要,那么这题应该怎么建图呢?
先分析题目,题目要求最小时间,所以肯定符合二分答案的性质。先拆点,把牛棚和雨棚拆开,对于每次二分到的时间,如果某两点之间可以在这个时间内到达,则建起一条无限流量的边。然后建一个超级源点,到每个牛棚建一条边权为牛的数量的边,最后建一个超级汇点,每个雨棚连一条边权为雨棚容量的边到汇点。
“检测”函数:
众所周知,二分答案中有一个检测函数,那么这个函数怎么写呢?
每次跑一下最大流算法,如果流量大于总牛量则返回真,否则返回假。
三 参考代码
我做这到题的时候最大流写的不太熟练,是照着题解写的(并非抄,哪里借鉴代码中我会尽量注明),所以我的代码和一些题解会比较相似。这是完整代码:
#include <cstdio>
#include <cstring>
#include <queue>
#define ll long long
#define maxn 4100
#define maxm 16000
#define inf 100000000000000000
using namespace std;
int n, m, cnt = 1, S, T;
ll ans = inf, sum;
int cow[maxn], house[maxn], head[maxn], nlast[maxn];
ll dis[maxn][maxn];
ll Min(ll x, ll y) { return x <= y ? x : y; }
struct edge
{
int to, next;
ll f;
}e[maxm << 3];
void add_edge(int f, int t, ll fl)
{
e[++cnt] = (edge){t, head[f], fl};
head[f] = cnt;
e[++cnt] = (edge){f, head[t], 0};
head[t] = cnt;
}
void input()
{
scanf("%d %d", &n, &m);
S = 2 * n + 1;
T = 2 * n + 2;
for (int i = 1; i <= n; ++i)
{
scanf("%d %d", &cow[i], &house[i]);
sum += (ll)cow[i];
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = inf;
for (int i = 1; i <= n; ++i)
dis[i][i] = 0;
for (int i = 1; i <= m; ++i)
{
int from, to, Dis;
scanf("%d %d %d", &from, &to, &Dis);
dis[from][to] = Min(dis[from][to], Dis);
dis[to][from] = Min(dis[to][from], Dis);
//这里借鉴了题解
}
}
void init()
{
memset(head, 0, sizeof(head));
memset(nlast, 0, sizeof(nlast));
cnt = 1;
}
void Floyd()
{
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = Min(dis[i][j], dis[i][k] + dis[k][j]);
// for (int i = 1; i <= n; ++i)
// {
// for (int j = 1; j <= n; ++j)
// printf("%lld ", dis[i][j]);
// puts(" ");
// }
}
int dist[maxn];
bool vis[maxn];
bool bfs()
{
//bfs判断连通性,以及每一个点到终点的距离
memset(dist, 0, sizeof(dist));
memset(vis, 0, sizeof(vis));
queue <int> q;
vis[T] = true;
dist[T] = 1;
q.push(T);
while (!q.empty())
{
const int now = q.front();
//printf("%d ", now);
q.pop();
for (int i = head[now]; i; i = e[i].next)
{
const int to = e[i].to;
if (vis[to] == false && e[i ^ 1].f)
{//如果这条边的反向边还有残量,而且这个节点没被访问过
vis[to] = true;
dist[to] = dist[now] + 1;
q.push(to);
}
}
}
return vis[S];
}
ll Dinic(int x, ll f)
{
if (x == T) return f;
//如果这个点就是终点
ll now = f;
//now表示剩余流量
for (int i = nlast[x]; i; i = e[i].next)
{
const int to = e[i].to;
if (dist[to] == dist[x] - 1 && e[i].f)
{//Dinic的增广原则为先增广较短的增广路
const ll a = Dinic(to, Min(now, e[i].f));
//a记录增广路上所有边的最小值
e[i].f -= a;
//e[i ^ 1] 为反悔边,即可以退回流量
e[i ^ 1].f += a;
now -= a;
//增广出去剩余流量减少
if (now == 0) return f;
}
}
return f - now;
//返回这条路径能增广的量
}
ll maxflow()
{
ll now = 0;
while (bfs())
{
for (int i = 1; i <= n * 2 + 2; ++i)
nlast[i] = head[i];
now += Dinic(S, inf);
}
return now;
}
bool check(ll mid)
{
init();
for (int i = 1; i <= n; ++i)
{
add_edge(S, i, cow[i]);
add_edge(i + n, T, house[i]);
for (int j = 1; j <= n; ++j)
{
if (i == j || dis[i][j] <= mid)
add_edge(i, j + n, inf);
//这里借鉴了题解。
}
}
ll F = maxflow();
// printf("%lld\n", F);
return (F >= sum);
}
void search()
{
ll l = 0, r = inf;
while (l <= r)
{
ll mid = (l + r) >> 1;
//printf("%lld %d\n", mid, check(mid));
if (check(mid) == true)
{
ans = mid;
r = mid - 1;
}
else l = mid + 1;
}
}
void output()
{
if (ans == inf || ans == 0) printf("-1\n");
else printf("%lld", ans);
}
int main()
{
input();
Floyd();
search();
output();
return 0;
}
管理员,求通过。