例题:
-
P1231 教辅的组成
-
P2598 [ZJOI2009]狼和羊的故事
-
P4016 负载平衡问题
- 最大流:
贪心流程:
-
找一条(s)到(t)的只经过(f(e)< c(e))的边的路径。
-
如果不存在满足条件的路径,则算法结束。否则,沿着该路径尽可能地增加(f(e)),返回第(1)步。这一步骤称作增广。
反悔机制:
-
只利用满足(f(e)< c(e))的(e)或(f(e)> 0)的(e)的反向边(rev(e)),寻找一条(s)到(t)的路径。
-
如果不存在满足条件的路径,则算法结束。否则,沿着该路径尽可能地增加流,返回(1)。
核心代码:
bool BFS(int s){
queue<int> q;
q.push(s);memset(dis,0,sizeof(dis));dis[0] = 1;
while (!q.empty()){
int x = q.front();q.pop();
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to]||!ed[i].w) continue;
dis[to] = dis[x] + 1;
q.push(to);
}
}
return dis[n1+n2+n1+n3+1];
}
int DFS(int x,int limit){
if (!limit||x == n1+n2+n1+n3+1) return limit;
int res = 0;
for (int &i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to] != dis[x]+1) continue;
int tmp = DFS(to,min(limit,ed[i].w));
limit -= tmp,ed[i].w -= tmp,ed[i^1].w += tmp,res += tmp;
if (!limit) break;
}
return res;
}
int main(){
for (int i = 0;i <= n1+n2+n1+n3+1;i++) cur[i] = head[i];
while (BFS(0)){
while (tmp = DFS(0,inf)) ans += tmp;
for (int i = 0;i <= n1+n2+n1+n3+1;i++) head[i] = cur[i];
}
printf("%d
",ans);
return 0;
}
最小割:
性质: 最小割 = 最大流
费用流:
在求解最大流时,我们在残量网络上不断贪心地增广而得到了最大流。现在边上多了费用,为了使最后总费用最小,我们自然能想到每次贪心地在残量网络上,沿着费用最小的那条路进行增广,此时残量网络中的反向边费用应该是原边费用的相反数,以保证退流操作是可逆、正确的。
核心代码:
bool SPFA(int s){
queue<int> q;
memset(vis,0,sizeof(vis));
for (int i = 1;i <= n;i++) dis[i] = inf;
q.push(s);dis[s] = 0;vis[s] = 1;
while (!q.empty()){
int x = q.front();q.pop();
vis[x] = 0;
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (!ed[i].lim) continue;
if (dis[to] > dis[x] + ed[i].w){
dis[to] = dis[x] + ed[i].w;
pre1[to] = x,pre2[to] = i;
if (!vis[to]){
vis[to] = 1;
q.push(to);
}
}
}
}
return dis[t] != inf;
}
int main(){
n = read(),m = read(),s = read(),t = read();
for (int i = 1;i <= m;i++){
int u = read(),v = read(),w = read(),c = read();
add(u,v,w,c),add(v,u,0,-c);
}
int ans1 = 0,ans2 = 0;
while (SPFA(s)){
int tmp = inf;
for (int i = t;i != s;i = pre1[i]) tmp = min(tmp,ed[pre2[i]].lim);
ans1 += tmp,ans2 += tmp*dis[t];
for(int i = t;i != s;i = pre1[i]) ed[pre2[i]].lim -= tmp,ed[pre2[i]^1].lim += tmp;
}
printf("%d %d
",ans1,ans2);
return 0;
}
例题:
对于第一道题,想要尽可能的多组出书的配套方案,我们可以联想到最大流,那么问题就在于如何连边,因为给出的都是书和练习册,或者书和答案之间的关系,所以我们把书放在中间,练习册和答案分别和他连边。
但是这就出现了一个问题,每本书只能选一次,怎样才能保证这个问题呢???我们把书拆成两个点,中间连一条边权为1的边就可以解决了。
code:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
int read(){
int x = 1,a = 0;char ch = getchar();
while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
return x*a;
}
const int maxn = 1e5+10,inf = 1e9+7;
int n1,n2,n3;
int m1,m2;
struct node{
int to,nxt,w;
}ed[maxn*4];
int head[maxn*4],tot = 1,cur[maxn*4];
void add(int u,int to,int w){
ed[++tot].to = to;
ed[tot].w = w;
ed[tot].nxt = head[u];
head[u] = tot;
}
int dis[maxn*4];
bool BFS(int s){
queue<int> q;
q.push(s);memset(dis,0,sizeof(dis));dis[0] = 1;
while (!q.empty()){
int x = q.front();q.pop();
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to]||!ed[i].w) continue;
dis[to] = dis[x] + 1;
q.push(to);
}
}
return dis[n1+n2+n1+n3+1];
}
int DFS(int x,int limit){
if (!limit||x == n1+n2+n1+n3+1) return limit;
int res = 0;
for (int &i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to] != dis[x]+1) continue;
int tmp = DFS(to,min(limit,ed[i].w));
limit -= tmp,ed[i].w -= tmp,ed[i^1].w += tmp,res += tmp;
if (!limit) break;
}
return res;
}
int main(){
n1 = read(),n2 = read(),n3 = read();
m1 = read();
for (int i = 1;i <= m1;i++){
int x = read(),y = read();
add(y,x+n2,1),add(x+n2,y,0);
}
m2 = read();
for (int i = 1;i <= m2;i++){
int x = read(),y = read();
add(x+n2+n1,y+n1+n1+n2,1),add(y+n1+n1+n2,x+n1+n2,0);
}
for (int i = 1;i <= n1;i++){
add(i+n2,i+n2+n1,1),add(i+n2+n1,i+n2,0);
}
for (int i = 1;i <= n2;i++) add(0,i,1),add(i,0,0);
for (int i = 1;i <= n3;i++) add(i+n1+n2+n1,n1+n2+n1+n3+1,1),add(n1+n2+n1+n3+1,i+n1+n2+n1,0);
int ans = 0,tmp;
for (int i = 0;i <= n1+n2+n1+n3+1;i++) cur[i] = head[i];
while (BFS(0)){
while (tmp = DFS(0,inf)) ans += tmp;
for (int i = 0;i <= n1+n2+n1+n3+1;i++) head[i] = cur[i];
}
printf("%d
",ans);
return 0;
}
第二道题主要看我们如何能把问题转化为最小割,连出一个四联通图后,建立源点和汇点,把羊向源点连边,狼向汇点连边,边权全为(inf),这样我们在求最小割的时候一定不会断掉他(因为他太大了啊)
正确性: 狼和羊能相遇的充分条件是能通过狼和羊亮点从源点走到汇点,如果现在不连通,且断掉的不是连接源点和汇点这些边,那么一定是狼和羊之间的路径
code:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
int read(){
int x = 1,a = 0;char ch = getchar();
while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
return x*a;
}
const int maxn = 1000*1000+10,inf = 1e9+7;
int n,m;
int s,t;
struct node{
int to,nxt,w;
}ed[maxn*4];
int head[maxn*4],tot = 1,cur[maxn*4];
void add(int u,int to,int w){
ed[++tot].to = to;
ed[tot].nxt = head[u];
ed[tot].w = w;
head[u] = tot;
}
int dis[maxn*4];
bool BFS(int s){
queue<int> q;
q.push(s);memset(dis,0,sizeof(dis));dis[0] = 1;
while (!q.empty()){
int x = q.front();q.pop();
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to]||!ed[i].w) continue;
dis[to] = dis[x] + 1;
q.push(to);
}
}
return dis[t];
}
int DFS(int x,int limit){
if (!limit||x == t) return limit;
int res = 0;
for (int &i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (dis[to] != dis[x]+1) continue;
int tmp = DFS(to,min(limit,ed[i].w));
limit -= tmp,ed[i].w -= tmp,ed[i^1].w += tmp,res += tmp;
if (!limit) break;
}
return res;
}
int main(){
n = read(),m = read();
s = 0,t = (1+n)*m+1;
for (int i = 1;i <= n;i++){
for (int j = 1;j <= m;j++){
int x = read();
int pos1 = i*m+j,pos2 = (i-1)*m+j,pos3 = (i+1)*m+j,pos4 = i*m+j-1,pos5 = i*m+j+1;
if (i > 1) add(pos1,pos2,1),add(pos2,pos1,0);
if (i < n) add(pos1,pos3,1),add(pos3,pos1,0);
if (j > 1) add(pos1,pos4,1),add(pos4,pos1,0);
if (j < m) add(pos1,pos5,1),add(pos5,pos1,0);
if (x == 1) add(pos1,t,inf),add(t,pos1,0);
if (x == 2) add(s,pos1,inf),add(pos1,s,0);
}
}
int tmp,ans = 0;
for (int i = s;i <= t;i++) cur[i] = head[i];
while (BFS(0)){
while (tmp = DFS(0,inf)) ans += tmp;
for (int i = s;i <= t;i++) head[i] = cur[i];
}
printf("%d
",ans);
return 0;
}
第三题而言重点还是在建模,贪心的想,多的我们移走,少的我们补充,正好的我们不动,那么建边就分为以下几种情况:
-
比平均值大的仓库,从源点向他连一条流量为(a_i-sum)的边,费用为0,说明要从他移出
-
比平均值小的仓库,从他向汇点连一条流量为(sum-a_i)的边,费用为0,说明要补充他
-
相邻仓库间建一条流量为(inf),费用为1的边,可以无限移动,但是与源点汇点连的边限制了他们
code:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
int read(){
int x = 1,a = 0;char ch = getchar();
while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
return x*a;
}
const int maxn = 500,inf = 1e9+7;
int n,a[maxn],s,t;
struct node{
int to,nxt,w,lim;
}ed[maxn*4];
int head[maxn*4],tot = 1;
void add(int u,int to,int lim,int w){
ed[++tot].to = to;
ed[tot].w = w;
ed[tot].lim = lim;
ed[tot].nxt = head[u];
head[u] = tot;
}
int vis[maxn],dis[maxn],pre1[maxn],pre2[maxn];
bool SPFA(int s){
queue<int> q;q.push(s);
memset(vis,0,sizeof(vis));vis[s] = 1;
for (int i = 0;i <= n+1;i++) dis[i] = inf;dis[s] = 0;
while (!q.empty()){
int x = q.front();q.pop();
vis[x] = 0;
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (!ed[i].lim) continue;
if (dis[to] > dis[x]+ed[i].w){
dis[to] = dis[x] + ed[i].w;
pre1[to] = x,pre2[to] = i;
if (!vis[to]){
vis[to] = 1;
q.push(to);
}
}
}
}
return dis[t] != inf;
}
int main(){
n = read();
s = 0,t = n+1;
int sum = 0;
for (int i = 1;i <= n;i++) a[i] = read(),sum += a[i];
sum /= n;
for (int i = 1;i <= n;i++){
if (a[i] < sum) add(i,t,sum-a[i],0),add(t,i,0,0);
if (a[i] > sum) add(s,i,a[i]-sum,0),add(i,s,0,0);
if (i > 1) add(i,i-1,inf,1),add(i-1,i,0,-1);
if (i < n) add(i,i+1,inf,1),add(i+1,i,0,-1);
}
add(1,n,inf,1),add(n,1,0,-1);
add(n,1,inf,1),add(1,n,0,-1);
int ans = 0;
while (SPFA(s)){
int tmp = inf;
for (int i = t;i != s;i = pre1[i]) tmp = min(tmp,ed[pre2[i]].lim);
ans += tmp*dis[t];
for (int i = t;i != s;i = pre1[i]) ed[pre2[i]].lim -= tmp,ed[pre2[i]^1].lim += tmp;
}
printf("%d
",ans);
return 0;
}