problem
solution
志愿者连续工作 [ s i , t i ] [s_i,t_i] [si,ti] 天,我们可以提炼出网络流二十四题中《最长k可重区间集问题》的模型。
同样地,把 1 ∼ n 1\sim n 1∼n 天抽象成一条 1 ∼ n + 1 1\sim n+1 1∼n+1 个点的链条。
- 源点 s → 1 s\rightarrow 1 s→1 容量无穷费用零, n + 1 → t n+1\rightarrow t n+1→t 汇点 容量无穷费用零。
- 然后 i → i + 1 i\rightarrow i+1 i→i+1 容量 ∞ − a i \infty-a_i ∞−ai 费用零。
- 对于第 i i i 种志愿者, s i → t i + 1 s_i\rightarrow t_i+1 si→ti+1 容量无穷费用 c i c_i ci。
最后跑最小费用最大流即为答案。
如果无法直接理解这样建图的正确性,可以考虑把网络图中的流量流起来。
如果从 s s s 沿着费用零的边向 t t t 流,由于链条上的边流量为 ∞ − a i \infty-a_i ∞−ai,所以先前 ∞ \infty ∞ 的不能流完。
那么势必要通过有志愿者(花费)的边流。
假设流到了点 i i i,那么剩下不能流过 ( i , i + 1 ) (i,i+1) (i,i+1) 的流量我们得从 i i i 连出去的志愿者边流,并且流一个就要花费 c i c_i ci 的代价。
然后在点 x + 1 x+1 x+1 的时候这些流量又会汇合。
这就相当于招募了从 i i i 开始到 x x x 结束的志愿者。(当然可能有多个 x x x 结束点)
反正到最后 t t t 的时候,流量总和一定会汇聚成从 s s s 开始流的 ∞ \infty ∞。
你可以理解一队人去闯密室逃脱,在一定关卡要进行多人支线任务,需要大部队派一些人去完成,然后主线队继续往下走主线任务,到了一定关卡有些人完成了自己的支线任务可以归队了。最后通关的时候,一定是大家都从主线任务关卡口出来。
code
#include <bits/stdc++.h>
using namespace std;
#define maxn 2000
#define maxm 50000
#define int long long
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
int head[maxn], dis[maxn], lst[maxn], vis[maxn], a[maxn];
int cnt = -1, n, m, s, t;
queue < int > q;
void addedge( int u, int v, int w, int c ) {
E[++ cnt] = { v, head[u], w, c }; head[u] = cnt;
E[++ cnt] = { u, head[v], 0,-c }, head[v] = cnt;
}
bool SPFA() {
memset( lst, -1, sizeof( lst ) );
memset( dis, 0x3f, sizeof( dis ) );
q.push( dis[s] = 0 );
while( ! q.empty() ) {
int u = q.front(); q.pop(); vis[u] = 0;
for( int i = head[u];~ i;i = E[i].nxt ) {
int v = E[i].to;
if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
dis[v] = dis[u] + E[i].cost; lst[v] = i;
if( ! vis[v] ) vis[v] = 1, q.push( v );
}
}
}
return ~ lst[t];
}
int MCMF() {
int ans = 0;
while( SPFA() ) {
int flow = inf;
for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
flow = min( flow, E[i].flow );
for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
E[i ^ 1].flow += flow;
E[i].flow -= flow;
ans += flow * E[i].cost;
}
}
return ans;
}
signed main() {
memset( head, -1, sizeof( head ) );
scanf( "%lld %lld", &n, &m );
s = 0, t = n + 2;
for( int i = 1;i <= n;i ++ ) scanf( "%lld", &a[i] );
for( int i = 1;i <= n;i ++ ) addedge( i, i + 1, inf - a[i], 0 );
addedge( s, 1, inf, 0 );
addedge( n + 1, t, inf, 0 );
for( int i = 1, u, v, w;i <= m;i ++ ) {
scanf( "%lld %lld %lld", &u, &v, &w );
addedge( u, v + 1, inf, w );
}
printf( "%lld\n", MCMF() );
return 0;
}
solution(流量平衡)
假设共 3 3 3 天,第 i i i 天招募 p i p_i pi 人。
共有三类志愿者:
- 从第 1 1 1 天工作到第 3 3 3 天,费用为 c 1 c_1 c1,招募了 b 1 b_1 b1 人。
- 从第 2 2 2 天工作到第 3 3 3 天,费用为 c 2 c_2 c2,招募了 b 2 b_2 b2 人。
- 从第 1 1 1 天工作到第 2 2 2 天,费用为 c 3 c_3 c3,招募了 b 3 b_3 b3 人。
则有以下不等式:
{
b
1
+
b
3
≥
a
1
b
1
+
b
2
+
b
3
≥
a
2
b
1
+
b
2
≥
a
3
\begin{cases} b_1+b_3\ge a_1\\b_1+b_2+b_3\ge a_2\\b_1+b_2\ge a_3 \end{cases}
⎩⎪⎨⎪⎧b1+b3≥a1b1+b2+b3≥a2b1+b2≥a3
记第
i
i
i 天招募的志愿者超出最少要求人数
d
i
d_i
di 人,显然
d
i
≥
0
d_i\ge 0
di≥0。则可改写成以下等式:
{
p
1
=
b
1
+
b
3
=
a
1
+
d
1
p
2
=
b
1
+
b
2
+
b
3
=
a
2
+
d
2
p
3
=
b
1
+
b
2
=
a
3
+
d
3
\begin{cases}p_1=b_1+b_3=a_1+d_1\\p_2=b_1+b_2+b_3=a_2+d_2\\p_3=b_1+b_2=a_3+d_3\end{cases}
⎩⎪⎨⎪⎧p1=b1+b3=a1+d1p2=b1+b2+b3=a2+d2p3=b1+b2=a3+d3
将相邻两两等式作差后移项整理得:
{
p
1
=
b
1
+
b
3
=
a
1
+
d
1
p
2
−
p
1
=
b
2
−
b
3
=
a
2
−
a
1
+
d
2
−
d
1
p
3
−
p
2
=
−
b
3
=
a
3
−
a
2
+
d
3
−
d
2
−
p
3
=
−
b
1
−
b
2
=
−
a
3
−
d
3
⇒
{
p
1
−
p
0
=
b
1
+
b
3
−
a
1
−
d
1
=
0
p
2
−
p
1
=
b
2
−
b
3
−
a
2
+
a
1
−
d
2
+
d
1
=
0
p
3
−
p
2
=
−
b
3
−
a
3
+
a
2
−
d
3
+
d
2
=
0
p
4
−
p
3
=
−
b
1
−
b
2
+
a
3
+
d
3
=
0
\begin{cases}p_1=b_1+b_3=a_1+d_1\\p_2-p_1=b_2-b_3=a_2-a_1+d_2-d_1\\ p_3-p_2=-b_3=a_3-a_2+d_3-d_2\\-p_3=-b_1-b_2=-a_3-d_3\end{cases}\Rightarrow \begin{cases}p_1-p_0=b_1+b_3-a_1-d_1=0\\p_2-p_1=b_2-b_3-a_2+a_1-d_2+d_1=0\\ p_3-p_2=-b_3-a_3+a_2-d_3+d_2=0\\p_4-p_3=-b_1-b_2+a_3+d_3=0 \end{cases}
⎩⎪⎪⎪⎨⎪⎪⎪⎧p1=b1+b3=a1+d1p2−p1=b2−b3=a2−a1+d2−d1p3−p2=−b3=a3−a2+d3−d2−p3=−b1−b2=−a3−d3⇒⎩⎪⎪⎪⎨⎪⎪⎪⎧p1−p0=b1+b3−a1−d1=0p2−p1=b2−b3−a2+a1−d2+d1=0p3−p2=−b3−a3+a2−d3+d2=0p4−p3=−b1−b2+a3+d3=0
网络流中除了源汇点,其余点都应满足流量平衡,即流入流量等于流出流量;若将流入记为正,流出记为负,则应满足流入流出流量的代数和为
0
0
0。
网络图中一条连接 x , y x,y x,y 的边,在 x , y x,y x,y 的流量平衡等式中各出现一次,且一次为正一次为负。
所以我们可以对上面最后化出的等式每个建立一个点,这个等式表示的就是这个点流量平衡。
再观察最后的等式:
observationⅠ. \text{observationⅠ.} observationⅠ. b i , d i b_i,d_i bi,di 都在恰好两个等式出现,且是一正一负。所以每一个变量 b i , d i b_i,d_i bi,di 都可以作为网络图中的一条边。
observationⅡ. \text{observationⅡ.} observationⅡ. 常量 a i a_i ai 也恰好在两个等式中出现,且是一正一负。为正时表示流入,可以和源点连边;为负时表示流出,可以和汇点连边。
根据作差规则, a i a_i ai 一定是出现在第 i , i + 1 i,i+1 i,i+1 两个等式中,且一定第 i i i 个等式为负,第 i + 1 i+1 i+1 个为正。
常量与源汇点连边,变量表示常量点之间的边跑平衡。
最后答案是 min ∑ b i ⋅ c i \min \sum b_i·c_i min∑bi⋅ci ,可以以“费用”的形式表示出来。
-
简述建图方式:
假设 a 0 = a n + 1 = 0 a_0=a_{n+1}=0 a0=an+1=0。
- 建立源汇点 s , t s,t s,t。
- 建立点 1 ∼ n + 1 1\sim n+1 1∼n+1,代表 n + 1 n+1 n+1 个等式。
- 第 i + 1 i+1 i+1 个点向第 i i i 个点连一条容量无穷,费用为零的边。对应 b i , d i b_i,d_i bi,di 的平衡。
- 第 i i i 类志愿者连边 s i → t i + 1 s_i\rightarrow t_i+1 si→ti+1,容量无穷,费用为 c i c_i ci。
- 对于第 i i i 个点,若 a i − a i − 1 a_i-a_{i-1} ai−ai−1 为正,连边 s → i s\rightarrow i s→i,容量 a i − a i − 1 a_i-a_{i-1} ai−ai−1 费用为零;若为负,连边 i → t i\rightarrow t i→t,容量 a i − 1 − a i a_{i-1}-a_i ai−1−ai 费用为零。相当于等式中的常数项。
实际理解上可以把常数项提到右边:
{
b
1
+
b
3
−
d
1
=
a
1
−
a
0
b
2
−
b
3
−
d
2
+
d
1
=
a
2
−
a
1
−
b
3
−
d
3
+
d
2
=
a
3
−
a
2
−
b
1
−
b
2
+
d
3
=
a
4
−
a
3
\begin{cases} b_1+b_3-d_1=a_1-a_0\\ b_2-b_3-d_2+d_1=a_2-a_1\\ -b_3-d_3+d_2=a_3-a_2\\ -b_1-b_2+d_3=a_4-a_3 \end{cases}
⎩⎪⎪⎪⎨⎪⎪⎪⎧b1+b3−d1=a1−a0b2−b3−d2+d1=a2−a1−b3−d3+d2=a3−a2−b1−b2+d3=a4−a3
把
a
i
−
a
i
−
1
a_i-a_{i-1}
ai−ai−1 当成第
i
i
i 个等式的盈亏量,这样你就能理解正负与源汇连边的意义了。
code
#include <bits/stdc++.h>
using namespace std;
#define maxn 2000
#define maxm 50000
#define int long long
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
int head[maxn], dis[maxn], lst[maxn], vis[maxn], a[maxn];
int cnt = -1, n, m, s, t;
queue < int > q;
void addedge( int u, int v, int w, int c ) {
E[++ cnt] = { v, head[u], w, c }; head[u] = cnt;
E[++ cnt] = { u, head[v], 0,-c }, head[v] = cnt;
}
bool SPFA() {
memset( lst, -1, sizeof( lst ) );
memset( dis, 0x3f, sizeof( dis ) );
q.push( dis[s] = 0 );
while( ! q.empty() ) {
int u = q.front(); q.pop(); vis[u] = 0;
for( int i = head[u];~ i;i = E[i].nxt ) {
int v = E[i].to;
if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
dis[v] = dis[u] + E[i].cost; lst[v] = i;
if( ! vis[v] ) vis[v] = 1, q.push( v );
}
}
}
return ~ lst[t];
}
int MCMF() {
int ans = 0;
while( SPFA() ) {
int flow = inf;
for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
flow = min( flow, E[i].flow );
for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
E[i ^ 1].flow += flow;
E[i].flow -= flow;
ans += flow * E[i].cost;
}
}
return ans;
}
signed main() {
memset( head, -1, sizeof( head ) );
scanf( "%lld %lld", &n, &m );
s = 0, t = n + 2;
for( int i = 1;i <= n;i ++ ) scanf( "%lld", &a[i] );
for( int i = 1;i <= n;i ++ ) addedge( i + 1, i, inf, 0 );
for( int i = 1, u, v, w;i <= m;i ++ ) {
scanf( "%lld %lld %lld", &u, &v, &w );
addedge( u, v + 1, inf, w );
}
for( int i = 1;i <= n + 1;i ++ )
if( a[i] - a[i - 1] > 0 ) addedge( s, i, a[i] - a[i - 1], 0 );
else addedge( i, t, a[i - 1] - a[i], 0 );
printf( "%lld\n", MCMF() );
return 0;
}
其实流量平衡的建边含义理解还有从线性规划对偶角度出发的。会在《防守战线》中详细说明。