题面
思路
引入:网络流?
看到这道题,第一想法是用一个dp来完成决策
但是,显然这道题的数据并不允许我们进行dp,尤其是有10000种志愿者的情况下
那么我们就要想别的办法来解决:
贪心?这道题做的决策除了价值以外还有覆盖面问题,而且更致命的是每一天的人数需求还不一样
那么题目限制如此多元化的题目,我们就一定要想到用那个传说中的万能算法——网络流
想到网络流以后,一个直观的想法就是源点连人,人连可以被雇佣的日子,日子连汇点,然后跑最小费用最大流
但是这样有一个问题:本题中的雇佣不是可以一天用一天不用的,而是你花了那么多钱,他就一次性帮你从Si做到Ti,不能按天购买
而我们的这个网络流模型显然可以让流量从每个“人点”的一部分出边流出,并不正确
“一面对多面”
这里的这个问题,我称之为“一面对多面”,也就是一个决策会影响到多个限制条件的改变,但是网络流中的流量是一对一的,也就是一点流量不能在某一个点“分”成很多流量,所以并不能解决这种“一面对多面”的问题
那我们就要考虑更换思路了
我们之前的想法是以“一个被雇佣的人”为“一点流量”,但是仔细思索,发现出现上述问题的本质在于:不同的日子可能会用同一个人,而代表同一个人的流量只有一
那我们就要想办法让这一个流量,去覆盖所有的Si到Ti的日子
这引导我们想到如下模型:
对于每一中志愿者(si,ti,ci),我们建一条跨过si到ti的所有点的边,费用为ci,来表示“这一个流量一直流完了这些区域”
但是问题在于,只是这样建图的话,并没有把每天的人数限制ai放到图里,也就是这个图缺少信息
补全信息
此时我们需要想办法把人数限制放到图里
我们考虑最大流算法:它会求出最大的流量
那我们既然用一点流量表示一个人,那么为什么我们不把这个“需要用人”的限制,放到另外几条边上呢?
我们在点(i,i+1)之间建边,设流量为-a[i],也就是负的当天需求数,费用自然是零的
然后,令上文中的志愿者(si,ti,ci),建边(si,ti+1),费用ci,流量无限
此时我们相当于是把第i天的决策放到了第i个点和第i+1个点之间的所有边上(就是把所有点排成一排,这两个点之间的那一条位置里的所有边,包括跨过这个区间的志愿者边)
需要志愿者?让它们从志愿者边上流过去,同时让人数限制边满流到-a[i],这样求一个1-n+1的最大流,流量为0的最小费用就是雇佣人的最小费用了
为了让这个限制起效,又因为网络流中流量非负,所以我们建立点SS和TT,连边(SS,1)(n+1,TT),限制为inf,费用为0
同时,我们把之前的人数限制边的流量改成(inf-a[i]),这样最终的SS-TT最大流一定是inf,而且限制依然成立
完成
总结
到这里,我们就完成了本题的建模,可以看到这个过程是复杂而十分深刻的,我们从最开始的暴力、dp,转化到网络流,又从“一面对多面”的问题中跳了出来,转化到了最后的方法,又增加了inf的流量来使整个图合法
可以看到,这道题里主要解决的就是两个矛盾:dp时间复杂度不够,以及“一面对多面”不可行
第一个矛盾是时间上的,我们发现无法进行优化以后,转换了算法
第二个矛盾在于建模之上,我们采取了研究算法本质、重新构建模型的方法,舍弃了常见的建图模型而建立了这道题的独有模型
由此可见,做题的时候思路一定要放开,要大胆;如果发现常用的“套路”不合适,就要果断放弃、选择新方法
同时,研究算法、建立模型要从算法本质出发;一定要先想好算法中的什么东西代表题目中的哪个东西,再放到算法框架中,针对性构建模型;不能生搬硬套旧套路
Code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define inf 1e9
using namespace std;
inline int read(){
int re=0,flag=1;char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-') flag=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') re=(re<<1)+(re<<3)+ch-'0',ch=getchar();
return re*flag;
}
int n,m,cnt=-1,ans,first[10010],dis[10010],vis[10010];
struct edge{
int to,next,w,cap;
}a[200010];
inline void add(int u,int v,int w,int cap){
a[++cnt]=(edge){v,first[u],w,cap};first[u]=cnt;
a[++cnt]=(edge){u,first[v],-w,0};first[v]=cnt;
}
int q[100010];
bool spfa(int s,int t){
int head=0,tail=1,i,u,v,w;
memset(dis,-1,sizeof(dis));memset(vis,0,sizeof(vis));
q[0]=t;vis[t]=1;dis[t]=0;
while(head<tail){
u=q[head++];vis[u]=0;
for(i=first[u];~i;i=a[i].next){
v=a[i].to;w=a[i].w;
if(a[i^1].cap&&((dis[v]==-1)||(dis[v]>dis[u]-w))){
dis[v]=dis[u]-w;
if(!vis[v]) q[tail++]=v,vis[v]=1;
}
}
}
return ~dis[s];
}
int dfs(int u,int t,int limit){
if(u==t||!limit){vis[u]=1;return limit;}
int i,v,f,flow=0,w;vis[u]=1;
for(i=first[u];~i;i=a[i].next){
v=a[i].to;w=a[i].w;
if(!vis[v]&&(dis[v]==dis[u]-w)&&(a[i].cap)){
if(!(f=dfs(v,t,min(limit,a[i].cap)))) continue;
a[i].cap-=f;a[i^1].cap+=f;ans+=f*w;
flow+=f;limit-=f;
if(!limit) return flow;
}
}
return flow;
}
int zkw(int s,int t){//zkw费用流
int re=0;
while(spfa(s,t)){
vis[t]=1;
while(vis[t]){
memset(vis,0,sizeof(vis));
re+=dfs(s,t,inf);
}
}
return re;
}
int main(){
int i,t1,t2,t3;memset(first,-1,sizeof(first));
n=read();m=read();
for(i=1;i<=n;i++) t1=read(),add(i,i+1,0,inf-t1);//构建“人数限制边”
add(0,1,0,inf);add(n+1,n+2,0,inf);
for(i=1;i<=m;i++){//构建“志愿者边”
t1=read();t2=read();t3=read();
add(t1,t2+1,t3,inf);
}
zkw(0,n+2);//最大流值一定是inf,费用就是答案
cout<<ans<<endl;
}