从多种角度看[BZOJ 1061] [NOI 2008]志愿者招募(费用流)

题面

申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管。布布刚上任就遇到了一个难题:为即将启动的奥运新项目招募一批短期志愿者。经过估算,这个项目需要N 天才能完成,其中第i 天至少需要Ai 个人。 布布通过了解得知,一共有M 类志愿者可以招募。其中第i 类可以从第Si 天工作到第Ti 天,招募费用是每人Ci 元。新官上任三把火,为了出色地完成自己的工作,布布希望用尽量少的费用招募足够的志愿者,但这并不是他的特长!于是布布找到了你,希望你帮他设计一种最优的招募方案。

分析

感觉这个问题已经成为一个经典套路了。先把问题形式化:

给出一条直线上的\(n\)个点,\(m\)个区间\([l_i,r_i]\)。选一些区间覆盖这条直线,使得每个点至少被覆盖\(a_i\)次,每个区间可以被用来覆盖多次,每覆盖一次的代价为\(c_i\).求最小代价。

接下来我们将从多个角度建图:

从流量守恒的角度理解

这里先给出建图方式,记\((u,v,w,c)\)表示从\(u\)到\(v\)连一条容量为\(w\),费用为\(c\)的有向边:

  1. 建立源汇点\(s,t\),连边\((s,1,+\infty,0),\ (n+1,t,+\infty,0)\)
  2. 对于每个区间\([l_i,r_i]\),连边\((l_i,r_i+1,+\infty,c_i)\)
  3. 对于每个点\(i\),连边\((i,i+1,\infty-a_i)\)

对于每个区间\([l_i,r_i]\),连边\((l_i,r_i+1,+\infty,c_i)\)不难理解,+1是为了把覆盖点转化成覆盖边。比如\(l_i=r_i\)时区间\([l_i,r_i]\)正好就覆盖了\(a_{l_i}\)所在的那条边。

如何理解\(\infty-a_i\)?我们考虑到覆盖点\(i\)的每个区间,每个区间的使用次数加起来\(\geq a_i\).也就是说,跨过边\((i,i+1)\)的那些边的流量之和至少为\(a_i\).

现在我们要保证上面的条件。注意到我们从源点流出的流量为\(+\infty\),流入汇点的流量也是\(+\infty\)。对于一条边\((i,i+1,\infty-a_i,0)\),它最多只能流掉\(\infty-a_i\)的流量。而为了保证流量守恒,跨过它的边至少要流掉剩下\(a_i\)的流量,否则总流量就不是\(\infty\)了。这样的话,跨过\((i,i+1)\)的边的流量之和就至少为\(a_i\)了。满足了上面的条件。

如果不好理解可以看图,假如a[2]=3,那么(2,3)上面的两条边就需要流掉3的流量,否则流量就不守恒了。


从线性规划的角度理解

对于一些不太好直接想到建图的问题,我们可以数学建模,列出方程然后用线性规划求解。这样的好处是思维量较小,只要做代数变换就可以建图,而不用考虑建图的实际意义。

对于每个区间,我们设它的覆盖次数为\(x_i\),那么对于每个点\(i\),我们有:

\[p_i=\sum_{l_j \leq i \leq r_j} x_j \geq a_i
\]

把不等式转化为等式,添加辅助变量\(y_i(y_i \geq 0)\)

\[p_i=\sum_{l_j \leq i \leq r_j} x_j -y_i = a_i
\]

用\(p_i\)减去\(p_{i-1}\)(特别地,我们规定\(p_0=0,p_{n+1}=0\)),得到:

\(p_i-p_{i-1}=\sum_{l_j \leq i \leq r_j} x_j - \sum_{l_j \leq i-1 \leq r_j}x_j -y_i+y_{i-1}=a_i-a_{i-1}\)

我们会发现,\(x_j\)的系数为正当且仅当区间\([l_j,r_j]\)的左端点在\(i\).因为这样第二个和式里一定没有\(x_j\)

​ \(x_j\)的系数为负当且仅当\([l_j,r_j]\)的右端点在\(i-1\),因为这样第一个和式里一定没有\(x_j\)

​ 其他的\(x_j\)均被消掉

这样的话,每个变量都只在两个式子中出现了,而且一次为正,一次为负。所有等式右边和为0 .

我们举个例子来理解

例子来自byvoid巨佬的博客

一共需要4天,四天需要的人数依次是4,2,5,3。有5类志愿者,如下表所示:

种类 1 2 3 4 5
时间 [1,2] [1,1] [2,3] [3,3] [3,4]
费用 3 4 3 5 6

根据我们上面的描述,可以列出式子

\[\begin{cases} p_1= x_1 + x_2 - y_1 = 4 \\ p_2 = x_1 + x_3 - y_2 = 2\\ p_3 = x_3 + x_4 +x_5 - y_3 =5\\ p_4 = x_5 - y_4 = 3 \end{cases}
\]

然后做差

\[\begin{cases} p_1 -p_0 = x_1 + x_2 - y_1 = 4 \\ p_2 - p_1 = x_3 - x_2 -y_2 +y_1 = -2 \\ p_3 - p_2 = x_4 + x_5 - x_1 - y_3 + y_2 =3 \\p_4 - p_3 = - x_3 - x_4 + y_3 - y_4 = -2 \\p_5 - p_4 = - x_5 + y_4 = -3 \\ \end{cases}
\]

容易发现,每个变量都只在两个式子中出现了,而且一次系数为正,一次系数为负。所有等式右边的常数之和为0 .

我们最终的目的是在上面方程组的约束条件下最优化目标函数 \(\sum_{j=1}^m x_j c_j\)。这里当然可以用单纯形算法解决,但是本人太弱不会,于是考虑建图跑网路流解决。

根据网络流中每个点流量平衡的思想,我们可以把\(-x_i\)看成从点\(i\)流出\(x_i\)的流量,\(+x_i\)看成流入\(x_i\)的流量。等式为0就代表流量平衡。

  • 每个等式为图中一个顶点,添加源点S和汇点T。

  • 如果一个等式右边为非负整数c,从源点S向该等式对应的顶点连接一条容量为c,权值为0的有向边;如果一个等式右边为负整数c,从该等式对应的顶点向汇点T连接一条容量为c,权值为0的有向边。

  • 如果一个变量\(x_i\)在第j个等式中出现为\(x_i\),在第k个等式中出现为\(-x_i\),且在目标函数里的系数为\(c_i\),从顶点j向顶点k连接一条容量为\(+\infin\),费用为\(c_i\)的有向边。

  • 如果一个变量\(y_i\)在第j个等式中出现为\(y_i\),在第k个等式中出现为\(-y_i\),且在目标函数里没有出现,从顶点j向顶点k连接一条容量为\(+\infin\),权值为0的有向边。

具体到这个问题上,最终的建图方案是:

  1. 建立源点S=0,汇点T=n+2

  2. 若\(a_i-a_{i-1}>0\) 连边\((S,i,a_i-a_{i-1},0)\),否则连边\((i,T,a_{i-1}-a_{i},0)\).(\(i \in[1,n+1],a_0=a_{n+1}=0\))

  3. 对于每个区间\([l_j,r_j]\),连边\((l_j,r_j+1,+\infin,c_i)\) (对应方程中的\(x\))

  4. 对于\(i \in [1,n]\) 连边\([i+1,i, +\infty,0]\) (对应方程中的\(y\))

代码

第一种建图:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define INF 0x3f3f3f3f
#define maxn 50000
#define maxm 500000
using namespace std;
inline void qread(int &x){
x=0;
int sign=1;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') sign=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
x=x*10+c-'0';
c=getchar();
}
x=x*sign;
} int n,m;
struct edge {
int from;
int to;
int next;
int flow;
int cost;
} E[maxm*2+5];
int sz=1;
int head[maxn+5];
int pre[maxn+5];
int minf[maxn+5];
int dist[maxn+5];
int inq[maxn+5];
void adde(int u,int v,int w,int c) {
sz++;
E[sz].from=u;
E[sz].to=v;
E[sz].flow=w;
E[sz].cost=c;
E[sz].next=head[u];
head[u]=sz;
}
void add_edge(int u,int v,int w,int c){
// printf("%d->%d vol=%d cost=%d\n",u,v,w,c);
adde(u,v,w,c);
adde(v,u,0,-c);
} int spfa(int s,int t){
queue<int>q;
memset(dist,0x3f,sizeof(dist));
memset(inq,0,sizeof(q));
q.push(s);
dist[s]=0;
inq[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
inq[x]=0;
for(int i=head[x];i;i=E[i].next){
int y=E[i].to;
if(E[i].flow){
// printf("%d %d\n",x,y);
if(dist[y]>dist[x]+E[i].cost){
dist[y]=dist[x]+E[i].cost;
minf[y]=min(minf[x],E[i].flow);
pre[y]=i;
if(!inq[y]){
inq[y]=1;
q.push(y);
}
}
}
}
}
if(dist[t]==INF) return 0;
else return 1;
} void update(int s,int t){
int x=t;
while(x!=s){
int i=pre[x];
E[i].flow-=minf[t];
E[i^1].flow+=minf[t];
x=E[i^1].to;
}
} int mcmf(int s,int t){
int maxcost=0,maxflow=0;
memset(minf,0x3f,sizeof(minf));
while(spfa(s,t)){
update(s,t);
maxcost+=minf[t]*dist[t];
maxflow+=minf[t];
}
return maxcost;
} int a[maxn+5];
int main(){
int u,v,w;
qread(n);
qread(m);
for(int i=1;i<=n;i++){
qread(a[i]);
}
for(int i=1;i<=m;i++){
qread(u);
qread(v);
qread(w);
add_edge(u,v+1,INF,w);
}
int s=0,t=n+2;
add_edge(0,1,INF,0);
add_edge(n+1,t,INF,0);
for(int i=1;i<=n;i++){
add_edge(i,i+1,INF-a[i],0);
}
printf("%d\n",mcmf(s,t));
}

第二种建图:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define INF 0x3f3f3f3f
#define maxn 50000
#define maxm 500000
using namespace std;
inline void qread(int &x){
x=0;
int sign=1;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') sign=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
x=x*10+c-'0';
c=getchar();
}
x=x*sign;
} int n,m;
struct edge {
int from;
int to;
int next;
int flow;
int cost;
} E[maxm*2+5];
int sz=1;
int head[maxn+5];
int pre[maxn+5];
int minf[maxn+5];
int dist[maxn+5];
int inq[maxn+5];
void adde(int u,int v,int w,int c) {
sz++;
E[sz].from=u;
E[sz].to=v;
E[sz].flow=w;
E[sz].cost=c;
E[sz].next=head[u];
head[u]=sz;
}
void add_edge(int u,int v,int w,int c){
// printf("%d->%d vol=%d cost=%d\n",u,v,w,c);
adde(u,v,w,c);
adde(v,u,0,-c);
} int spfa(int s,int t){
queue<int>q;
memset(dist,0x3f,sizeof(dist));
memset(inq,0,sizeof(q));
q.push(s);
dist[s]=0;
inq[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
inq[x]=0;
for(int i=head[x];i;i=E[i].next){
int y=E[i].to;
if(E[i].flow){
// printf("%d %d\n",x,y);
if(dist[y]>dist[x]+E[i].cost){
dist[y]=dist[x]+E[i].cost;
minf[y]=min(minf[x],E[i].flow);
pre[y]=i;
if(!inq[y]){
inq[y]=1;
q.push(y);
}
}
}
}
}
if(dist[t]==INF) return 0;
else return 1;
} void update(int s,int t){
int x=t;
while(x!=s){
int i=pre[x];
E[i].flow-=minf[t];
E[i^1].flow+=minf[t];
x=E[i^1].to;
}
} int mcmf(int s,int t){
int maxcost=0,maxflow=0;
memset(minf,0x3f,sizeof(minf));
while(spfa(s,t)){
update(s,t);
maxcost+=minf[t]*dist[t];
maxflow+=minf[t];
}
return maxcost;
} int a[maxn+5];
int main(){
int u,v,w;
qread(n);
qread(m);
for(int i=1;i<=n;i++){
qread(a[i]);
}
for(int i=1;i<=m;i++){
qread(u);
qread(v);
qread(w);
add_edge(u,v+1,INF,w);
}
int s=0,t=n+2;
for(int i=1;i<=n+1;i++){
int c=a[i]-a[i-1];
if(c>=0){
add_edge(s,i,c,0);
}else{
add_edge(i,t,-c,0);
}
}
for(int i=1;i<=n;i++) add_edge(i+1,i,INF,0);
printf("%d\n",mcmf(s,t));
}

从多种角度看[BZOJ 1061] [NOI 2008]志愿者招募(费用流)的更多相关文章

  1. NOI 2008 志愿者招募

    NOI 2008 志愿者招募 考虑用 $ p_i $ 表示第 $ i $ 天实际招收的人数,我们假设我们有三种志愿者,分别是 $ 1\to 2,1 \to 3 , 2\to 3 $ ,我们招手的人数分 ...

  2. BZOJ 1061: [Noi2008]志愿者招募 费用流

    1061: [Noi2008]志愿者招募 题目连接: http://www.lydsy.com/JudgeOnline/problem.php?id=1061 Description 申奥成功后,布布 ...

  3. [BZOJ1061][Noi 2008]志愿者招募(网络流)

    题目:http://www.lydsy.com:808/JudgeOnline/problem.php?id=1061 分析: 神题不解释,只能欣赏:https://www.byvoid.com/bl ...

  4. bzoj 1061 志愿者招募 费用流

    详见BYV的博客,写的非常全面https://www.byvoid.com/blog/noi-2008-employee /************************************** ...

  5. BZOJ 3876 支线剧情 | 有下界费用流

    BZOJ 3876 支线剧情 | 有下界费用流 题意 这题题面搞得我看了半天没看懂--是这样的,原题中的"剧情"指的是边,"剧情点"指的才是点. 题面翻译过来大 ...

  6. [BZOJ 1221] [HNOI2001] 软件开发 【费用流 || 三分】

    题目链接:BZOJ - 1221 题目分析 算法一:最小费用最大流 首先这是一道经典的网络流问题.每天建立两个节点,一个 i 表示使用毛巾,一个 i' 表示这天用过的毛巾. 然后 i 向 T 连 Ai ...

  7. 【BZOJ】1070: [SCOI2007]修车(费用流+特殊的技巧)

    http://www.lydsy.com/JudgeOnline/problem.php?id=1070 好神的题!!!orz 首先我是sb不会拆点..... 首先,每一个技术人员维修车辆都有一个先后 ...

  8. BZOJ.2324.[ZJOI2011]营救皮卡丘(费用流 Floyd)

    BZOJ 洛谷 首先预处理出\(dis[i][j]\),表示从\(i\)到\(j\)的最短路.可以用\(Floyd\)处理. 注意\(i,j\)是没有大小关系限制的(\(i>j\)的\(dis[ ...

  9. BZOJ 1877: [SDOI2009]晨跑(费用流)

    看到要求两个量就下意识的想到了费用流= =,先把一个点拆成两个点就能够解决一个的只经过一次的限制 CODE: #include<cstdio>#include<iostream> ...

随机推荐

  1. 面试题:MySQL索引为什么用B+树?

    面试题:MySQL索引为什么用B+树? 前言 讲到索引,第一反应肯定是能提高查询效率.例如书的目录,想要查找某一章节,会先从目录中定位.如果没有目录,那么就需要将所有内容都看一遍才能找到. 索引的设计 ...

  2. 关于项目在网页中运行部分jsp出现乱码(由request.getRequestDispatcher("XXX.jsp").forward(request, response)造成)的解决方法

    在写jsp的时候发现部分的jsp在浏览器预览时出现乱码,为一堆问号,如图: 当时问了同学,只有部分jsp会出现乱码,因为重新建一个jsp在运行就没有错误,可以显示出来,所以发现是jsp头部的错误,当新 ...

  3. Lucas(卢卡斯)定理

    Lucas定理 对于C(m,n)%P(P是质数)这样的问题,可以通过预处理阶乘和阶乘的逆元,来快速计算.但是当m,n大于P时,就不能保证m,n与P互质了,但不互质的情况下,乘法逆元不存在,此时就需要卢 ...

  4. php 调用python接口出现的一系列问题(原)

    调用示例代码(python写的一个谷歌翻译接口): $name = '中国'; exec("/mob360/EditImage/venv/bin/python /EditImage/fany ...

  5. Centos-Redhat下远程桌面的方法 & Redhat改Centos源

    折腾了好几天才搞定,Redhat下远程桌面的方法,首先保证本身已经装了桌面,并且可以ssh访问 由于系统中自带python2环境,装了anaconda以及它带的python3环境,这个必须存在(前提) ...

  6. 关于SpringBoot跨域的问题

    直接在启动类里面加这一段代码就行: @Bean public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource sourc ...

  7. jupyter notebook + MobaXterm Linux端远程部署

    $jupyter notebook --generate-config $python In []: from notebook.auth import passwd In []: passwd() ...

  8. CSS效果——绝对居中

    实现效果 不论窗口尺寸,都可以垂直和水平居中. 代码 <!DOCTYPE html> <html> <head> <meta charset="ut ...

  9. leetcode73矩阵置零

    https://leetcode-cn.com/problems/set-matrix-zeroes/ 解答: 两种方法时间复杂度都为O(mn) O(m+n)空间方法: 用两个容器储存为0的行和列 c ...

  10. Throwable 源码阅读

    Throwable 属性说明 /** * Java 语言中所有错误和异常的基类,此类及其子类才能通过 throws 被 JVM 虚拟机抛出. * @since 1.0 */ public class ...