网络流学习笔记——难题
这里是网络流难题集合。
VI.[NOI2009]植物大战僵尸
一眼看出拓扑排序。因为对于每个点,只有所有保护着和在右边的植物全挂掉之后,植物才能够被攻击。这样只要建出图来,在上面拓扑排序,对每个排序到的点统计权值和即可。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,head[610],val[610],cnt,in[610],res;
struct node{
int to,next;
}edge[400100];
void ae(int u,int v){
edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
}
queue<int>q;
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
for(int i=0,t1,t2,t3;i<n*m;i++){
scanf("%d%d",&val[i],&t1);
while(t1--){
scanf("%d%d",&t2,&t3);
ae(i,t2*m+t3),in[t2*m+t3]++;
}
}
for(int i=0;i<n;i++)for(int j=1;j<m;j++)ae(i*m+j,i*m+j-1),in[i*m+j-1]++;
for(int i=0;i<n*m;i++)if(!in[i])q.push(i);
while(!q.empty()){
int x=q.front();q.pop();
res+=val[x];
for(int i=head[x];i!=-1;i=edge[i].next){
in[edge[i].to]--;
if(!in[edge[i].to])q.push(edge[i].to);
}
}
printf("%d\n",res);
}
但是,如果你把它用本题的样例跑一下的话,你会发现,结果跑出来是而不是答案!!!
为什么呢?
看一下样例。我们发现里面存在负权点。
负权点就意味着,贪心地吃掉每一个能吃到的植物并不是最优的。我们仍需要权衡是放弃这个点还是吃掉它。
咋办呢?
这时候,就是网络流的登场。
我们引出闭合子图概念。
闭合子图是这样一个,使得:
如果点,那么对于所有的边,都有和。
换句话说,如果一个点在子图里,那么从出发爆搜,所有到达得了的点和边都在这个子图里。
这时候,我们回过来看一下这道题,就会发现,如果我们建反边,即一个点向保护着该点的所有点连边,那么,一个正确的解法,必定是一张闭合子图。(不然就有点被吃了,但是保护着它的点中还有活着的,违背了题意)。
显然,我们要求一个最大权闭合子图(字面意思)。
如何建图呢?我们首先仍然要跑拓扑排序,只保留拓扑排序能够排序到的节点。剩余的部分出现了环,是不能被吃掉的。
然后,开始建图。
1.对于原图中的边,在新图中连一条边。
2.对于原图中的点,如果有,则连一条边;如果有,则连一条边;如果有,两条边中随便选一条连。
最后答案即为(所有正权点的权值和-最小割)。
证明:
令集合为最小割意义下所能到达的所有点,即为最终我们要攻击的所有点。因为原图中的所有边边权都是,我们只能割断新加入的边。
则如果一个点,那么对于所有能到达的点,都有,因为原图中的边不会被截断。显然,如果一个点已经在中,那么边一定不会被割断(不割的流量比割了小)。
则我们证明了一个割方案下的一定是一张闭合子图。
那为什么最小割就对应着最大权呢?
因为如果一个正点被割了,就意味着我们不选这个点,要从正权点的权值和中减除;一个负权点被割了,就意为着,因此才要在汇点处把它割掉。所以我们要将正权点的权值和中加上,即减掉,就是的边权。
所以最小割,就是放弃最少的正点,选择最少的负点。
然后就OK了。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,head[610],val[610],cnt,in[610],dep[610],cur[610],res,S,T,sum;
struct node{
int to,next,val;
}edge[400100];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
bool vis[610];
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
if(!vis[x])continue;
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1||!vis[edge[i].to])continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,0x3f3f3f3f);
}
}
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n*m,T=n*m+1,vis[S]=vis[T]=true;
for(int i=0,t1,t2,t3;i<n*m;i++){
scanf("%d%d",&val[i],&t1);
while(t1--){
scanf("%d%d",&t2,&t3);
ae(t2*m+t3,i,0x3f3f3f3f),in[t2*m+t3]++;
}
}
for(int i=0;i<n;i++)for(int j=1;j<m;j++)ae(i*m+j-1,i*m+j,0x3f3f3f3f),in[i*m+j-1]++;
for(int i=0;i<n*m;i++)if(!in[i])q.push(i);
while(!q.empty()){
int x=q.front();q.pop();
vis[x]=true;
for(int i=head[x];i!=-1;i=edge[i].next){
if(edge[i].val)continue;
in[edge[i].to]--;
if(!in[edge[i].to])q.push(edge[i].to);
}
if(val[x]>=0)ae(S,x,val[x]),sum+=val[x];
else ae(x,T,-val[x]);
}
Dinic();
printf("%d\n",sum-res);
}
XXII.最长k可重区间集问题
这个建图比较神仙orz...
一上来默认需要离散化。设离散化后共有个位置。然后呢?
这里我们这样建图:
1.对于每个位置,连一条边。
2.连边与。
3.对于每一条从到,长度为的线段,连一条边。
答案即为最大费用。
为什么呢?
让我们看看一张典型的图:
水流从出发,流到了。
在处,每有一股水流离开主干道,就能获得对应的收益。但是,直到这股水流重新归队,这一点流量是回不来的。
例如,如果有一股水流流入了路径,那么,流经的流量就会少。但是,这股水流对的流量并无影响,毕竟是开线段,在端点处没有影响。
因此,我们就能看出这种建图的正确性。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,k,l[1010],r[1010],len[1010],cnt,head[1010],id[1010],fr[1010],dis[1010],lim,S,T,cost;
vector<int>v;
struct node{
int to,next,val,cost;
}edge[101000];
void ae(int u,int v,int w,int c){
edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
bool in[1100];
bool SPFA(){
memset(dis,-1,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
while(!q.empty()){
int x=q.front();q.pop(),in[x]=false;
for(int i=head[x];i!=-1;i=edge[i].next){
if(!edge[i].val)continue;
if(dis[edge[i].to]<dis[x]+edge[i].cost){
dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
}
}
}
if(dis[T]==-1)return false;
int x=T,mn=0x3f3f3f3f;
while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
cost+=mn*dis[T],x=T;
while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
return true;
}
int main(){
scanf("%d%d",&n,&k),memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)scanf("%d%d",&l[i],&r[i]),len[i]=r[i]-l[i],v.push_back(l[i]),v.push_back(r[i]);
sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin()),lim=v.size(),S=lim+1,T=lim+2;
for(int i=1;i<lim;i++)ae(i,i+1,k,0);
ae(S,1,k,0),ae(lim,T,k,0);
for(int i=1;i<=n;i++){
if(l[i]>r[i])swap(l[i],r[i]);
l[i]=lower_bound(v.begin(),v.end(),l[i])-v.begin()+1;
r[i]=lower_bound(v.begin(),v.end(),r[i])-v.begin()+1;
ae(l[i],r[i],1,len[i]);
}
while(SPFA());
printf("%d\n",cost);
return 0;
}
XXX.[CQOI2016]不同的最小割
这里介绍一种新的神奇玩意儿:最小割树。
首先,这题不是让你跑个网络流,绝对不是。
我们观察得,一组最小割必定将原图分割成两个连通块,而在一棵树上删去一条边也会将这棵树分成两个连通块。既然最短路有最短路径树,我们是否能建出一棵最小割树来呢?
我们先任选两个点,假设是和,当作源点和汇点。然后,我们跑出最小割,在另一张新图上连边。
然后,原图肯定被分割成两部分。我们在两个部分内递归着跑最小割,直到某部分内只剩一个节点。
如果看不懂,没关系,我们上图!
初始时,所有节点都在同一个集合里。
第一步,我们选择,。跑得最小割为边,值为。之后它分为两个集合与。
第二步,我们在集合内跑网络流。选择。跑出最小割为,我们选择割边。集合被分成集合与集合。
第三步,我们在集合中选择,跑出最小割为,割边。
之后我们在集合中做相同操作。
最终得到这样的图:
那这最小割树有什么性质呢?
1.它一定是一棵树。
不要笑,我们还没有证明它是一颗树呢!万一在某一步时,我们跑出了一个最小割,却发现所有割边都在集合外怎么办?
这种情况不可能发生。假设我们删去了该集合外的所有边,但这个集合一定仍然联通,我们一定还要至少删去集合内部的一条边,将这个集合真正地分成两个集合。
2.原图中任意两点的最小割,是最小割树中对应节点之间路径上的最小权值。
因为路径上任意一条边,将它割断一定是这两点的一组割。则最小割即为两点间最小的权值。
则本题的答案就呼之欲出了:就是最小割树上不同权值的种数。
另外,注意是无向边,在建图时正向反向边都有权值。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,head[1010],cnt,dep[1010],cur[1010],ord[1010],S,T,res,pos[1010];
struct node{
int to,next,val,ini;
}edge[400100];
void ae(int u,int v,int w){
// printf("%d %d %d\n",u,v,w);
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=edge[cnt].ini=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=edge[cnt].ini=w,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep));
q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,0x3f3f3f3f);
}
}
inline void initialize(){
for(int i=0;i<cnt;i++)edge[i].val=edge[i].ini;
}
set<int>s;
bool cmp(int x,int y){
return dep[x]<dep[y];
}
void work(int l,int r){
if(r<=l)return;
// printf("%d %d\n",l,r);
S=ord[l],T=ord[r];
res=0;
Dinic(),s.insert(res),initialize();
sort(ord+l,ord+r+1,cmp);
// for(int i=l;i<=r;i++)printf("%d ",dep[ord[i]]);puts("");
int mid=upper_bound(ord+l,ord+r+1,0,cmp)-ord;
// printf("%d\n",mid);
work(l,mid-1),work(mid,r);
}
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
for(int i=1;i<=n;i++)ord[i]=i;
work(1,n);
printf("%d\n",s.size());
// for(set<int>::iterator it=s.begin();it!=s.end();it++)printf("%d\n",*it);
return 0;
}
XXXVI.[NOI2012]美食节
我要举报,这里有人虐菜
真·菜
一眼看去,这很明显是[SCOI2007]修车的增强版。按照那题的思路,我果断敲了一发:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
int n,m,head[80100],cnt,dis[80100],fr[80100],id[80100],S,T,cost,p[50],s;
struct node{
int to,next,val,cost;
}edge[10001000];
inline void ae(int u,int v,int w,int c){
edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
bool in[80100];
inline bool SPFA(){
memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
while(!q.empty()){
register int x=q.front();q.pop(),in[x]=false;
// printf("%d\n",x);
for(register int i=head[x];i!=-1;i=edge[i].next){
if(!edge[i].val)continue;
if(dis[edge[i].to]>dis[x]+edge[i].cost){
dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
}
}
}
if(dis[T]==dis[0])return false;
register int x=T,mn=0x3f3f3f3f;
while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
cost+=dis[T]*mn,x=T;
while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
return true;
}
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
for(register int i=1;i<=n;i++)scanf("%d",&p[i]),s+=p[i];
S=s*m+n+1,T=s*m+n+2;
for(register int i=1;i<=n;i++)ae(S,s*m+i,p[i],0);
for(register int i=1;i<=n;i++)for(register int j=1,x;j<=m;j++){
scanf("%d",&x);
for(register int k=1;k<=s;k++)ae(s*m+i,s*(j-1)+k,1,x*k);
}
for(register int i=1;i<=m;i++)for(register int j=1;j<=s;j++)ae((i-1)*s+j,T,1,0);
while(SPFA());
printf("%d\n",cost);
return 0;
}
结果光荣地T掉了。吸臭氧都没用。
看了题解
首先,如果在残量网络上添加新边的话,是可以在之前残量的基础之上不加修改地继续跑的。因此,我们就想着不一次性把所有点全加完,万一有些点从头到尾都没有被用到过怎么办?
因为这道题的建模是分层的,第一层是源点,第二层全是菜(比如我),第三层全是虐菜的,最后一层是汇点,而SPFA费用流的特殊性,就在于它一次只能找到一条增广路。因此,每跑一边SPFA,就意为着将一个菜和一个虐菜的绑在了一起。
考虑一个模型。首先,对于每道菜,我们不如让最快的人去虐它(尽管这是错的,但我们先不管它)。这样,只有当一个人虐完了一道菜,他才有可能去虐下一道菜(好像有些不对劲)。
这样,当我们找出一条增广路后,增广路所涉及到的那个虐菜的就可以解锁下一道菜。当然咯,其它菜也有可能被虐的更好(丧 心 病 狂),因此我们要让所有菜都有一个被虐的机会(那菜还怎么活)。
总结一下:
1.在初始时,每一个人的首个虐菜位都是开放的:即,所有的菜连到所有第一时刻的人。同时,所有第一时刻的人连到汇点,源点连到所有菜。
2.当跑出一条增广路后,这个人开放下一个虐菜位,所有的菜连到这个虐菜位,同时这个虐菜位连到汇点。
当什么时候再也找不到新的增广路后,算法结束。
代码:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
int n,m,head[80100],cnt,dis[80100],fr[80100],id[80100],S,T,cost,p[50],s,tim[50][110];
struct node{
int to,next,val,cost;
}edge[10001000];
inline void ae(int u,int v,int w,int c){
edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
bool in[80100];
inline bool SPFA(){
memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
while(!q.empty()){
register int x=q.front();q.pop(),in[x]=false;
// printf("%d\n",x);
for(register int i=head[x];i!=-1;i=edge[i].next){
if(!edge[i].val)continue;
if(dis[edge[i].to]>dis[x]+edge[i].cost){
dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
}
}
}
if(dis[T]==0x3f3f3f3f)return false;
register int x=T,mn=0x3f3f3f3f;
while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
cost+=dis[T]*mn,x=T;
while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
return true;
}
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
for(register int i=0;i<n;i++)scanf("%d",&p[i]),s+=p[i];
S=s*m+n+1,T=s*m+n+2;
for(register int i=0;i<n;i++)ae(S,s*m+i,p[i],0);
for(register int i=0;i<n;i++)for(register int j=0;j<m;j++)scanf("%d",&tim[i][j]),ae(s*m+i,s*j,1,tim[i][j]);
for(register int i=0;i<m;i++)ae(i*s,T,1,0);
while(SPFA()){
int x=fr[T]+1;
ae(x,T,1,0);
for(int i=0;i<n;i++)ae(s*m+i,x,1,tim[i][x/s]*(x%s+1));
}
printf("%d\n",cost);
return 0;
}
XXXIX.[CQOI2012]交换棋子
魔鬼题orz……
首先这题我一点进去就懵了:这么个鬼畜的交换,怎么建模?我是一点思路也没有我太菜了。
题解的做法就很骇人了:
首先,我们把一个点拆成个点。
What?三个点?为什么?
首先,我们观察一条移动路径就会发现,路径两端的格子都只移动了一次,但是路径中间的格子都移动了两次!再加上移入棋子和移出棋子的区别,只拆两个点肯定是不可以的不信您可以试试。
决定了拆点,我们就会发现这个方法可以非常轻松地解决这两个问题。
移入棋子,我们统统都从入;移出棋子,我们统统都从出(和普通的拆点一样);统计答案,我们从统计。
这样子,我们只需要限制边和的流量,就能够达到区分路径两端和路径中间格子的目的(路径两边的格子,两条边的流量都被占去了;路径中间的格子,只有一条边的流量被占去了)。
设始图为,终图为,格子最多交换次数为,那么:
如果,连边,表示从点有一枚棋子出发了,目标是星辰大海。
如果,连边,表示点可以是某颗棋子的终点。
如果有,则连边。因为格子收支平衡了,所以在一组合法的方案中,这个格子流入的棋子肯定同流出的棋子相同,流量直接下取整(可能多余的那一点流量直接扔掉)。
否则,如果,则说明这个点移出的棋子比移入的棋子要多,故连。
否则,即,连边。
然后,对于每一个八连通的点对,连边。这条边可以被调用无限多次(但是点不可以),并且每调用一次就相当于带来一点费用。
则答案即为最小费用。
(如果始图中的数量和终图中的数量不等,则无解;如果跑出来最大流与始图中的数量不等,也无解)
代码:
#include<bits/stdc++.h>
using namespace std;
#define left(x,y) (x)*m+(y)
#define mid(x,y) (x)*m+(y)+n*m
#define right(x,y) (x)*m+(y)+2*n*m
int n,m,dx[8]={-1,0,1,1,1,0,-1,-1},dy[8]={1,1,1,0,-1,-1,-1,0},s1,s2;
namespace MCMF{
const int N=2000,M=2000000;
int head[N],cnt,dis[N],fr[N],id[N],S,T,cost,flow;
struct node{
int to,next,val,cost;
}edge[M];
void ae(int u,int v,int w,int c){
// printf("%d %d %d %d\n",u,v,w,c);
edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
bool in[N];
bool SPFA(){
memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
while(!q.empty()){
int x=q.front();q.pop(),in[x]=false;
// printf("%d\n",x);
for(int i=head[x];i!=-1;i=edge[i].next){
if(!edge[i].val)continue;
if(dis[edge[i].to]>dis[x]+edge[i].cost){
dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
}
}
}
if(dis[T]==0x3f3f3f3f)return false;
int x=T,mn=0x3f3f3f3f;
while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
flow+=mn,cost+=dis[T]*mn,x=T;
while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
return true;
}
}
using namespace MCMF;
char s[30][30],t[30][30],c[30][30];
bool ok(int x,int y){
return (x>=0&&x<n&&y>=0&&y<m);
}
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=3*n*m+1,T=3*n*m+2;
for(int i=0;i<n;i++)scanf("%s",s[i]);
for(int i=0;i<n;i++)scanf("%s",t[i]);
for(int i=0;i<n;i++)scanf("%s",c[i]);
for(int i=0;i<n;i++)for(int j=0;j<m;j++){
s1+=(s[i][j]=='0'),s2+=(t[i][j]=='0');
if(s[i][j]=='0')ae(S,mid(i,j),1,0);
if(t[i][j]=='0')ae(mid(i,j),T,1,0);
if(s[i][j]==t[i][j])ae(left(i,j),mid(i,j),(c[i][j]-'0')/2,0),ae(mid(i,j),right(i,j),(c[i][j]-'0')/2,0);
else{
if(s[i][j]=='0')ae(left(i,j),mid(i,j),(c[i][j]-'0')/2,0),ae(mid(i,j),right(i,j),(c[i][j]-'0'+1)/2,0);
if(t[i][j]=='0')ae(left(i,j),mid(i,j),(c[i][j]-'0'+1)/2,0),ae(mid(i,j),right(i,j),(c[i][j]-'0')/2,0);
}
for(int k=0;k<8;k++)if(ok(i+dx[k],j+dy[k]))ae(right(i,j),left(i+dx[k],j+dy[k]),0x3f3f3f3f,1);
}
if(s1!=s2){puts("-1");return 0;}
while(SPFA());
if(flow==s1)printf("%d\n",cost);
else puts("-1");
return 0;
}
LII.[ZJOI2010]贪吃的老鼠
神题,我写了题解。
LIX.[SDOI2014]LIS
多测不清空,爆零两行泪
因为一个没有清空我调了一下午QaQ……
首先,这道题与IV.最长不下降子序列问题极像,也是一样的分层建图。第一问就是直接跑最小割。但是接下来我们要输出字典序最小的最小割,怎么办呢?
我一开始想了非常暴力的方法:枚举一条边,断开它,看(新最小割+这条边的容量)等不等于(原本的最小割)。当然,复杂度过大,只有分连臭氧都救不了。
然后看题解,发现这道题就是求最小割的可行边。
最小割的可行边是这样的边,在某个残量网络中:
我们可以仍然按照递增的顺序枚举边。如果这条边不是可行边,直接跳过;否则,考虑删去这条边,并抹去它的一切影响。
抹去影响的方法就是:
不妨设在原本的残量网络中,而。那么,以为而为,跑最大流,就相当于把流量还给了;以为而为,跑最大流,就相当于把流量还给了。之后,断去边,即彻底地把这条边从图上删去,并使得这张图仍然是一条合法的残量网络。这种手法被称作退流。
代码:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int INF=0x3f3f3f3f3f3f3f3f;
int T_T,n,a[1010],b[1010],c[1010],f[1010],mx,ord[1010],id[1010];
namespace MaxFlow{
const int N=2000,M=2000000;
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{
int to,next,val;
}edge[M];
inline void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,INF);
}
}
}
using namespace MaxFlow;
inline bool cmp(int x,int y){
return c[x]<c[y];
}
bool vis[2000];
inline bool che(int u,int v){
while(!q.empty())q.pop();
memset(vis,false,sizeof(vis));
q.push(u),vis[u]=true;
while(!q.empty()){
int x=q.front();q.pop();
if(x==v)return true;
for(int i=head[x];i!=-1;i=edge[i].next)if(!vis[edge[i].to]&&edge[i].val)q.push(edge[i].to),vis[edge[i].to]=true;
}
return false;
}
vector<int>v;
signed main(){
scanf("%lld",&T_T);
while(T_T--){
scanf("%lld",&n),S=2*n+1,T=2*n+2,mx=0,v.clear();
for(register int i=1;i<=n;i++)scanf("%lld",&a[i]),ord[i]=i;
for(register int i=1;i<=n;i++)scanf("%lld",&b[i]);
for(register int i=1;i<=n;i++)scanf("%lld",&c[i]);
for(register int i=1;i<=n;i++){
f[i]=1;
for(register int j=1;j<i;j++)if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
mx=max(mx,f[i]);
}
memset(head,-1,sizeof(head)),cnt=res=0;
for(register int i=1;i<=n;i++){
id[i]=cnt,ae(i,i+n,b[i]);
if(f[i]==1)ae(S,i,INF);
if(f[i]==mx)ae(i+n,T,INF);
for(register int j=1;j<i;j++)if(f[j]+1==f[i]&&a[j]<a[i])ae(j+n,i,INF);
}
Dinic();
printf("%lld ",res);
sort(ord+1,ord+n+1,cmp);
for(register int i=1;i<=n;i++){
if(che(ord[i],ord[i]+n))continue;
v.push_back(ord[i]);
S=ord[i],T=2*n+1,Dinic();
S=2*n+2,T=ord[i]+n,Dinic();
edge[id[ord[i]]].val=edge[id[ord[i]]^1].val=0;
}
sort(v.begin(),v.end());
printf("%d\n",v.size());
for(int i=0;i<v.size();i++)printf("%lld ",v[i]);puts("");
}
return 0;
}
LXI.[AHOI2009]最小割
这里就是我们在LIX.[SDOI2014]LIS里面提到的最小割的可行边与必须边的应用。
复习一下,一条边是最小割的可行边,当且仅当
而一条边是最小割的必须边,当且仅当
但是,这道题中,如果你像LIX一样,爆搜判连通的话,会取得30分的好成绩;如果你聪明点,会预处理出点对间相互到达的关系的话,能够拿到70分。
这就启发我们必须要优化判连通的复杂度。
这个时候,我们就可以用算法求出点双连通分量(),判连通。
如果你真就这么写了个交上去,会取得20分的好成绩。你满眼都会是红红火火。
为什么呢?
哦,我们忘记了一条重要限制:
然后就过了。
代码:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
int n,m,dfn[4010],low[4010],col[4010],C,tot;
namespace MF_ISAP{
const int N=5000,M=200000;
int head[N],cur[N],dep[N],gap[N],cnt,S,T,res;
struct node{
int to,next,val;
}edge[M];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline void bfs(){
memset(dep,-1,sizeof(dep)),memset(gap,0,sizeof(gap)),q.push(T),dep[T]=0;
while(!q.empty()){
register int x=q.front();q.pop();
gap[dep[x]]++;
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(dep[edge[i].to]==-1)dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
}
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]+1!=dep[x])continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
}
if(used==flow)return used;
}
gap[dep[x]]--;
if(!gap[dep[x]])dep[S]=n+1;
dep[x]++;
gap[dep[x]]++;
return used;
}
inline void ISAP(){
bfs();
while(dep[S]<n)memcpy(cur,head,sizeof(head)),dfs(S,0x3f3f3f3f);
}
}
using namespace MF_ISAP;
struct EDGE{
int u,v,w,id;
}e[60010];
stack<int>s;
void tarjan(int x){
s.push(x);
dfn[x]=low[x]=++tot;
for(int i=head[x];i!=-1;i=edge[i].next){
if(!edge[i].val)continue;
if(!dfn[edge[i].to])tarjan(edge[i].to),low[x]=min(low[x],low[edge[i].to]);
else if(!col[edge[i].to])low[x]=min(low[x],dfn[edge[i].to]);
}
if(low[x]!=dfn[x])return;
C++;
while(s.top()!=x)col[s.top()]=C,s.pop();
col[s.top()]=C,s.pop();
}
int main(){
scanf("%d%d%d%d",&n,&m,&S,&T),memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w),e[i].id=cnt,ae(e[i].u,e[i].v,e[i].w);
ISAP();
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
for(int i=1;i<=m;i++){
if(edge[e[i].id].val||col[e[i].u]==col[e[i].v]){puts("0 0");continue;}
printf("1 ");
puts(col[S]==col[e[i].u]&&col[e[i].v]==col[T]?"1":"0");
}
return 0;
}
LXVII.CF343E Pumping Stations
这是一套最小割树的神题。
我居然想着用费用流瞎搞
首先,最小割树肯定要建。然后呢?
考虑树中权值最小的一条边。则这条边一定会被统计入最终答案中至少一次,因为只要有排列中相邻的两个数一个在左侧,一个在右侧,这条边就会是最小割。而这种情况必定发生至少一次。
我们考虑分治。对于左侧,我们设至少有两个节点;对于右侧,我们设至少有两个节点。
不妨设这里面有一些数是相邻的。
假设排列为,则本段收益为,其中为最小割大小。
又有 ,
则
而另一种排列,收益。
则显然,(每次选完这条边一端所有的数)的方案一定不劣于其它方案。
因为断开一条边后,剩下的两端都仍是树,因此我们可以递归。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,cut[210][210];
namespace MF{
const int N=2100,M=20100;
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{
int to,next,val,org;
}edge[M];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=edge[cnt].org=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=edge[cnt].org=w,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
res=0;
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,0x3f3f3f3f);
}
}
void initialize(){
for(int i=0;i<cnt;i++)edge[i].val=edge[i].org;
}
}
namespace GMT{
int ord[2100],cnt,head[2100],p,q,id,mn,res;
struct node{
int to,next,val;
bool vis;
}edge[4100];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,edge[cnt].vis=false,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,edge[cnt].vis=false,head[v]=cnt++;
}
bool cmp(int x,int y){
return MF::dep[x]<MF::dep[y];
}
void work(int l,int r){
if(l==r)return;
MF::S=ord[l],MF::T=ord[r];
MF::Dinic(),ae(ord[l],ord[r],MF::res),MF::initialize();
sort(ord+l,ord+r+1,cmp);
int mid=upper_bound(ord+l,ord+r+1,0,cmp)-ord;
work(l,mid-1),work(mid,r);
}
bool vis[2100];
vector<int>v;
void dfs(int x,int fa){
for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa&&!edge[i].vis){
if(edge[i].val<mn)mn=edge[i].val,p=x,q=edge[i].to,id=i;
dfs(edge[i].to,x);
}
}
void solve(int x){
mn=0x3f3f3f3f,dfs(x,0);
if(mn==0x3f3f3f3f){v.push_back(x);return;}
edge[id].vis=edge[id^1].vis=true,res+=mn;
int u=p,v=q;
solve(u),solve(v);
}
}
int main(){
scanf("%d%d",&n,&m),memset(MF::head,-1,sizeof(MF::head)),memset(GMT::head,-1,sizeof(GMT::head));
for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),MF::ae(x,y,z);
for(int i=1;i<=n;i++)GMT::ord[i]=i;
GMT::work(1,n);
GMT::solve(1);
printf("%d\n",GMT::res);
for(int i=0;i<GMT::v.size();i++)printf("%d ",GMT::v[i]);
return 0;
}
LXIX.LOJ#115. 无源汇有上下界可行流
(在这种题中,原汇点和源点用小写字母和表示;新汇点和新源点用大写字母和表示,中文称作伪汇点或新汇点)
首先,我们要限制住流量。建一张新图,对于每条原图中的边,在新图中连边。这样子,在新图中跑出的任何流都肯定符合原图中的流量限制,每条边的流量为(流量下界+新图中对应边的流量)。
但是,满足了流量限制,我们还要满足流入和流出的流量限制。
设立数组。对于每条边,。
这样子,最终的就是每个节点的初始流量。为正,意味着可以向其它点流流量,新图中连边;为负,意为着要靠其它点补贴它,连边。这样子就保证流入和流出的流量相等。
则如果最终新图中流量跑满,就有一组可行流。每条边的流量为(流量下界+新图中对应边的流量)。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,d[210],sum,low[21000],id[21000];
namespace MaxFlow{
const int N=1000,M=200000;
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{
int to,next,val;
}edge[M];
inline void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,0x3f3f3f3f);
}
}
}
using namespace MaxFlow;
signed main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n+1,T=n+2;
for(register int i=1,x,y,l,r;i<=m;i++)scanf("%d%d%d%d",&x,&y,&l,&r),id[i]=cnt,low[i]=l,ae(x,y,r-l),d[x]-=l,d[y]+=l;
for(register int i=1;i<=n;i++)if(d[i]>0)ae(S,i,d[i]),sum+=d[i];else if(d[i]<0)ae(i,T,-d[i]);
Dinic();
if(sum!=res){puts("NO");return 0;}
puts("YES");
for(register int i=1;i<=m;i++)printf("%d\n",low[i]+edge[id[i]^1].val);
return 0;
}
LXXI.LOJ#116. 有源汇有上下界最大流
首先,有源汇的情况,我们已经知道,是连一条边来解决。这题也是。
首先,先跑一遍有源汇可行流。因为这条边是为了平衡源汇点的流量而设,因此这条边在跑完从到的可行流后的流量,便是当前对的流量。
然后,我们拆去和图中所有与伪源点和伪汇点(即与相连的那两个点)有关的边,并在到再跑一遍最大流。则答案为(第一遍可行流时上的流量+第二遍的最大流)。
理解:第一遍是帮我们试探出了一组合法的解。之后,在断掉所有新加的边后,再跑最大流,就是正常网络流的操作(在残量网络上瞎搞,看能不能使最大流增大)。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,s,t,degree[310],sum;
namespace MaxFlow{
const int N=1000,M=200000;
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{
int to,next,val;
}edge[M];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,0x3f3f3f3f);
}
}
}
using namespace MaxFlow;
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t),memset(head,-1,sizeof(head)),S=n+1,T=n+2;
for(int i=1,x,y,l,r;i<=m;i++)scanf("%d%d%d%d",&x,&y,&l,&r),degree[y]+=l,degree[x]-=l,ae(x,y,r-l);
ae(t,s,0x3f3f3f3f);
for(int i=1;i<=n;i++)if(degree[i]>0)ae(S,i,degree[i]),sum+=degree[i];else ae(i,T,-degree[i]);
Dinic();
if(sum!=res){puts("please go home to sleep");return 0;}
for(int i=head[t];i!=-1;i=edge[i].next)if(edge[i].to==s)res=edge[i^1].val,edge[i].val=edge[i^1].val=0;
for(int i=head[S];i!=-1;i=edge[i].next)edge[i].val=edge[i^1].val=0;
for(int i=head[T];i!=-1;i=edge[i].next)edge[i].val=edge[i^1].val=0;
S=s,T=t;
Dinic();
printf("%d\n",res);
return 0;
}
LXXII.LOJ#117. 有源汇有上下界最小流
首先先像无源汇可行流一样建图跑,然后连边,然后再跑。答案即为的流量。
理解:第一遍时,所有流量都被尽可能地压榨出去,这保证需要经过的流量最小。然后连边后再跑,就保证了这是一组合法解。第一遍保证最优,第二遍保证可行。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,s,t,degree[100010],sum;
namespace MaxFlow{
const int N=50100,M=2000000;
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{
int to,next,val;
}edge[M];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,0x3f3f3f3f);
}
}
}
using namespace MaxFlow;
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t),memset(head,-1,sizeof(head)),S=n+1,T=n+2;
for(int i=1,x,y,l,r;i<=m;i++)scanf("%d%d%d%d",&x,&y,&l,&r),degree[y]+=l,degree[x]-=l,ae(x,y,r-l);
for(int i=1;i<=n;i++)if(degree[i]>0)ae(S,i,degree[i]),sum+=degree[i];else ae(i,T,-degree[i]);
Dinic();
ae(t,s,0x3f3f3f3f);
Dinic();
if(sum!=res){puts("please go home to sleep");return 0;}
for(int i=head[t];i!=-1;i=edge[i].next)if(edge[i].to==s)res=edge[i^1].val;
printf("%d\n",res);
return 0;
}
LXXIV.[WC2007]剪刀石头布
神题。
标签上写着“网络流”和“差分”,可是我怎么想也想不出它和网络流有什么关系。直到我看了题解。
首先,这道题可以反面考虑,即最小化非三元环的三元组数。我们来观察一下一个典型的非三元环的出入度信息:一定是一个入度为,一个出度为,一个出入度都为。这是唯一的情形,因为这个竞赛图肯定是完全图,任何一个三元组之间三条边肯定都连上了。它们要么是三元环,要么只有一条边反向了,因此这是唯一情形。
我们以入度为例。则每有一个入度为的点,就会拆散个三元环。那么一个入度为的点呢?显然,从三个指向它的点中任选两个点,再加上它自己,一定构成了一个非三元环。因此,一个入度为的点拆散了 个三元环。
更一般地说,一个入度为的点,它共拆散了个三元环,其中。
我们在分配一条未标明方向(即胜负未明)的边后,肯定有一个点的入度增加了。考虑在后的新拆散三元环数量:
也就是说,入度每增加,就会拆散(入度)个三元环。
考虑用网络流解决这个问题。我们给每个未标明方向的边一个编号,并连边,表示这条边产生的效果是必须在和两个点之间只选择一个点,并让它的入度加一。
然后,对于,连边,表示这个点第一次入度加一会拆散个三元环,第二次会拆散个三元环,第三次……
则答案为(总三元环数-最小费用),即。
哦,另外,每个点一开始的基础入度已经拆散了一些点。因此答案还要再减去。
即。
至于输出方案,就看每个究竟把流量流给了还是。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,g[110][110],deg[110],ord[110][110];
namespace MCMF{
const int N=100000,M=2000000;
int head[N],cnt,dis[N],fr[N],id[N],S,T,cost;
struct node{
int to,next,val,cost;
}edge[M];
void ae(int u,int v,int w,int c){
edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
bool in[N];
bool SPFA(){
memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
while(!q.empty()){
int x=q.front();q.pop(),in[x]=false;
// printf("%d\n",x);
for(int i=head[x];i!=-1;i=edge[i].next){
if(!edge[i].val)continue;
if(dis[edge[i].to]>dis[x]+edge[i].cost){
dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
}
}
}
if(dis[T]==dis[0])return false;
int x=T,mn=0x3f3f3f3f;
while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
cost+=dis[T]*mn,x=T;
while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
return true;
}
}
using namespace MCMF;
int main(){
scanf("%d",&n),memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
scanf("%d",&g[i][j]);
if(i==j)continue;
if(g[i][j]==0)deg[i]++;
}
S=n;
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(g[i][j]==2)ord[i][j]=++S;
S++,T=S+1;
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(g[i][j]==2)ae(S,ord[i][j],1,0),ae(ord[i][j],i,1,0),ae(ord[i][j],j,1,0);
for(int i=1;i<=n;i++){
cost+=(deg[i]-1)*deg[i]/2;
for(int j=deg[i];j<=n;j++)ae(i,T,1,j);
}
while(SPFA());
printf("%d\n",n*(n-1)*(n-2)/6-cost);
for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++){
if(g[i][j]!=2)continue;
for(int k=head[ord[i][j]];k!=-1;k=edge[k].next)if(edge[k].to>=1&&edge[k].to<=n&&!edge[k].val)g[i][j]=edge[k].to;
if(g[i][j]==i)g[i][j]=0,g[j][i]=1;
else g[i][j]=1,g[j][i]=0;
}
for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",g[i][j]);puts("");}
return 0;
}
LXXVI.无限之环
这道题太恶心了……它超过了我以前不知道怎么想的去用treap去写的疫情控制,荣膺我有生以来所写过的最长的代码……
这题也要拆点,并且还是拆成个点!!!分别设为,表示中,上,右,下,左五个方向。
首先,明显,可以奇偶建图。奇点从源点连流量,偶点连向汇点。
接下来我们只分析奇点操作,偶点就是将奇点操作反向我都是直接复制粘贴的。
我们要分情况讨论。
首先,我们都要连边,从中点分配流量。
1.O形,即。
以为例,显然,有一条免费的边是。
如果逆向或正向旋转的话,费用为,连边和。
如果旋转的话,费用为,连边。
2.形,即。以为例,显然,有两条免费的边和。
如果旋转的话,可能是不变,转到或不变,转到(自己画图理解一下),因此连边和。
如果旋转的话,就是两个操作同时进行,即转到的同时转到,费用为,刚好是前面两条边同时走的效果。
3.T形,即。以为例,显然,有条免费的边。
同时,如果或空出来,费用为;如果空出来,费用为;因此连边。
4.其它。这些要么转不了,要么转了跟没转一样,直接连。
然后我们就做完了这道大毒瘤。
代码:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
#define O(x,y) (x)*m+(y)
#define A(x,y) (x)*m+(y)+n*m
#define B(x,y) (x)*m+(y)+n*m*2
#define C(x,y) (x)*m+(y)+n*m*3
#define D(x,y) (x)*m+(y)+n*m*4
int n,m,dx[4]={-1,0,1,0},dy[4]={0,1,0,-1},sum;
namespace MCMF{
const int N=100000,M=2000000;
int head[N],cnt,dis[N],fr[N],id[N],S,T,cost,res;
struct node{
int to,next,val,cost;
}edge[M];
void ae(int u,int v,int w,int c){
edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
bool in[N];
bool SPFA(){
memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
while(!q.empty()){
int x=q.front();q.pop(),in[x]=false;
// printf("%d\n",x);
for(int i=head[x];i!=-1;i=edge[i].next){
if(!edge[i].val)continue;
if(dis[edge[i].to]>dis[x]+edge[i].cost){
dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
}
}
}
if(dis[T]==0x3f3f3f3f)return false;
int x=T,mn=0x3f3f3f3f;
while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
res+=mn,cost+=dis[T]*mn,x=T;
while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
return true;
}
}
using namespace MCMF;
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n*m*5,T=n*m*5+1;
for(int i=0;i<n;i++)for(int j=0,x;j<m;j++){
scanf("%d",&x);
if((i+j)&1){
sum+=__builtin_popcount(x);
ae(S,O(i,j),0x3f3f3f3f,0);
if(i>0)ae(A(i,j),C(i-1,j),1,0);
if(i<n-1)ae(C(i,j),A(i+1,j),1,0);
if(j>0)ae(D(i,j),B(i,j-1),1,0);
if(j<m-1)ae(B(i,j),D(i,j+1),1,0);
if(x==0)continue;
if(x==1){//^
ae(O(i,j),A(i,j),1,0);
ae(A(i,j),B(i,j),1,1);
ae(A(i,j),C(i,j),1,2);
ae(A(i,j),D(i,j),1,1);
}
if(x==2){//>
ae(B(i,j),A(i,j),1,1);
ae(O(i,j),B(i,j),1,0);
ae(B(i,j),C(i,j),1,1);
ae(B(i,j),D(i,j),1,2);
}
if(x==3){//^>
ae(O(i,j),A(i,j),1,0);
ae(O(i,j),B(i,j),1,0);
ae(A(i,j),C(i,j),1,1);
ae(B(i,j),D(i,j),1,1);
}
if(x==4){//_
ae(C(i,j),A(i,j),1,2);
ae(C(i,j),B(i,j),1,1);
ae(O(i,j),C(i,j),1,0);
ae(C(i,j),D(i,j),1,1);
}
if(x==5){//|
ae(O(i,j),A(i,j),1,0);
ae(O(i,j),C(i,j),1,0);
}
if(x==6){//_>
ae(C(i,j),A(i,j),1,1);
ae(O(i,j),B(i,j),1,0);
ae(O(i,j),C(i,j),1,0);
ae(B(i,j),D(i,j),1,1);
}
if(x==7){//^>_
ae(O(i,j),A(i,j),1,0);
ae(O(i,j),B(i,j),1,0);
ae(O(i,j),C(i,j),1,0);
ae(A(i,j),D(i,j),1,1);
ae(B(i,j),D(i,j),1,2);
ae(C(i,j),D(i,j),1,1);
}
if(x==8){//<
ae(D(i,j),A(i,j),1,1);
ae(D(i,j),B(i,j),1,2);
ae(D(i,j),C(i,j),1,1);
ae(O(i,j),D(i,j),1,0);
}
if(x==9){//<^
ae(O(i,j),A(i,j),1,0);
ae(D(i,j),B(i,j),1,1);
ae(A(i,j),C(i,j),1,1);
ae(O(i,j),D(i,j),1,0);
}
if(x==10){//-
ae(O(i,j),B(i,j),1,0);
ae(O(i,j),D(i,j),1,0);
}
if(x==11){//<^>
ae(O(i,j),A(i,j),1,0);
ae(O(i,j),B(i,j),1,0);
ae(A(i,j),C(i,j),1,2);
ae(B(i,j),C(i,j),1,1);
ae(D(i,j),C(i,j),1,1);
ae(O(i,j),D(i,j),1,0);
}
if(x==12){//<_
ae(C(i,j),A(i,j),1,1);
ae(D(i,j),B(i,j),1,1);
ae(O(i,j),C(i,j),1,0);
ae(O(i,j),D(i,j),1,0);
}
if(x==13){//<^_
ae(O(i,j),A(i,j),1,0);
ae(A(i,j),B(i,j),1,1);
ae(C(i,j),B(i,j),1,1);
ae(D(i,j),B(i,j),1,2);
ae(O(i,j),C(i,j),1,0);
ae(O(i,j),D(i,j),1,0);
}
if(x==14){//<_>
ae(B(i,j),A(i,j),1,1);
ae(C(i,j),A(i,j),1,2);
ae(D(i,j),A(i,j),1,1);
ae(O(i,j),B(i,j),1,0);
ae(O(i,j),C(i,j),1,0);
ae(O(i,j),D(i,j),1,0);
}
if(x==15){//+
ae(O(i,j),A(i,j),1,0);
ae(O(i,j),B(i,j),1,0);
ae(O(i,j),C(i,j),1,0);
ae(O(i,j),D(i,j),1,0);
}
}else{
ae(O(i,j),T,0x3f3f3f3f,0);
if(x==0)continue;
if(x==1){//^
ae(A(i,j),O(i,j),1,0);
ae(B(i,j),A(i,j),1,1);
ae(C(i,j),A(i,j),1,2);
ae(D(i,j),A(i,j),1,1);
}
if(x==2){//>
ae(A(i,j),B(i,j),1,1);
ae(B(i,j),O(i,j),1,0);
ae(C(i,j),B(i,j),1,1);
ae(D(i,j),B(i,j),1,2);
}
if(x==3){//^>
ae(A(i,j),O(i,j),1,0);
ae(B(i,j),O(i,j),1,0);
ae(C(i,j),A(i,j),1,1);
ae(D(i,j),B(i,j),1,1);
}
if(x==4){//_
ae(A(i,j),C(i,j),1,2);
ae(B(i,j),C(i,j),1,1);
ae(C(i,j),O(i,j),1,0);
ae(D(i,j),C(i,j),1,1);
}
if(x==5){//|
ae(A(i,j),O(i,j),1,0);
ae(C(i,j),O(i,j),1,0);
}
if(x==6){//_>
ae(A(i,j),C(i,j),1,1);
ae(B(i,j),O(i,j),1,0);
ae(C(i,j),O(i,j),1,0);
ae(D(i,j),B(i,j),1,1);
}
if(x==7){//^>_
ae(A(i,j),O(i,j),1,0);
ae(B(i,j),O(i,j),1,0);
ae(C(i,j),O(i,j),1,0);
ae(D(i,j),A(i,j),1,1);
ae(D(i,j),B(i,j),1,2);
ae(D(i,j),C(i,j),1,1);
}
if(x==8){//<
ae(A(i,j),D(i,j),1,1);
ae(B(i,j),D(i,j),1,2);
ae(C(i,j),D(i,j),1,1);
ae(D(i,j),O(i,j),1,0);
}
if(x==9){//<^
ae(A(i,j),O(i,j),1,0);
ae(B(i,j),D(i,j),1,1);
ae(C(i,j),A(i,j),1,1);
ae(D(i,j),O(i,j),1,0);
}
if(x==10){//-
ae(B(i,j),O(i,j),1,0);
ae(D(i,j),O(i,j),1,0);
}
if(x==11){//<^>
ae(A(i,j),O(i,j),1,0);
ae(B(i,j),O(i,j),1,0);
ae(C(i,j),A(i,j),1,2);
ae(C(i,j),B(i,j),1,1);
ae(C(i,j),D(i,j),1,1);
ae(D(i,j),O(i,j),1,0);
}
if(x==12){//<_
ae(A(i,j),C(i,j),1,1);
ae(B(i,j),D(i,j),1,1);
ae(C(i,j),O(i,j),1,0);
ae(D(i,j),O(i,j),1,0);
}
if(x==13){//<^_
ae(A(i,j),O(i,j),1,0);
ae(B(i,j),A(i,j),1,1);
ae(B(i,j),C(i,j),1,1);
ae(B(i,j),D(i,j),1,2);
ae(C(i,j),O(i,j),1,0);
ae(D(i,j),O(i,j),1,0);
}
if(x==14){//<_>
ae(A(i,j),B(i,j),1,1);
ae(A(i,j),C(i,j),1,2);
ae(A(i,j),D(i,j),1,1);
ae(B(i,j),O(i,j),1,0);
ae(C(i,j),O(i,j),1,0);
ae(D(i,j),O(i,j),1,0);
}
if(x==15){//+
ae(A(i,j),O(i,j),1,0);
ae(B(i,j),O(i,j),1,0);
ae(C(i,j),O(i,j),1,0);
ae(D(i,j),O(i,j),1,0);
}
}
}
while(SPFA());
if(res!=sum)puts("-1");
else printf("%d\n",cost);
return 0;
}
LXXXVI.CF132E Bits of merry old England
XCIV.CF1009G Allowed Letters
网络流各种玄学残量网络的代表,题解
C.[Topcoder12158]SurroundingGame
最后一题,我们将在这题中探索如下东西的本质:最大权闭合子图问题&对偶建图法。
这两种东西,都可以被抽象成如下模型:
有 件物品,每件物品要分到两个集合 之一。一件物品分到集合 的代价是 ,分到集合 的代价是 ;同时,若对于两件物品 ,都分到 中会有额外费用 ,都到 中则会有 , 则有 , 则有 。所有代价都可能为正或负。求费用最小的分配方案。(当然,实际应用中也有求最大的,此时就全部取反权即可)
例如,在“最大权闭合子图”问题中,我们会将所有东西分到两个集合中:选择的集合 和不选择的集合 。而若一个点 被选择,它所连向的节点 却未被选择,此时方案是不合法的,即相当于 ,而其它东西(除了 )的值都为 。
我们总是可以把问题抽象成一张长这样的图,通过上面的最小割——在最小割后,令 ,——来解决问题。
如图,当 时,代价为 ,而反映到图上则是 。
同理,通过把 分配到不同集合,我们可以得到四组方程:
因为对于不同的 对, 等东西在所有情形中只能够被计算一次,可以在最后一次性加到对应的 边或 边上,所以我们这里就先不考虑它们,只考虑 这四个。
于是现在方程便变为了
显然,在网络流中,任意边的边权都应该为正,不能出现负边,故应有 ;但是,观察到在任意一组最小割中, 两边中选且仅被选了一条边, 两边中选且仅被选了一条边,这意味着我们可以将 两条边的权值同时加上某个数,最后在跑完最小割后再把加上的这个东西减去即可。例如,若 ,则原本的 这条边,可以被替换成 这条边,然后答案减去 。这就是最大权闭合子图中,对点的权值正负分别判断是连到 或是连到 的原因。有了这个trick,我们便不需要求 ,只需保证 即可。
显然,六个未知数,四个方程,一般来说没有唯一解;但是,本题特殊的地方在于通过加加减减,我们可以得出
设此式结果为 。则明显,若 ,则不可能存在任何一组合法的 解。而当 时,明显至少存在一组解,为图方便,直接令 即可,此时 两条有向边便可合并成一条无向边。
的值一旦确定,则 也可直接通过解方程(明显现在已经被化成了有唯一解的四元四式方程组)解出。
但是,在大多数题中, 这四个东西不是全非零,所以大多数时候不需要解方程。
我们发现,当 时,我们无法找到一组合法的分配方式;但是,如果原本应用的图满足二分图性质(即,所有的 可以被分作两个集合,集合内部的 全部为 ,只有集合间的值才非零)的话,我们可以通过翻转一个集合(即,令对于一个集合中的点来说,,;而对于另一个集合来说,,)来使得 。但是,若原图不是二分图,则本方法就不再适用了。
幸运的是,大部分此类题中,要么直接有 (例如最大权闭合子图),要么是二分图(例如本题)。二分图的常见场景即为网格图。
在本题中,我们可以通过拆点来抽象出模型。我们定义 表示图中某个位置 是否放石头的状态,另外定义 表示是否周围全都有石头的状态。设 表示放石头的代价, 表示被占据的收益。现在考虑需要连的边。
显然,当 同时成立的情形,位置 的收益不能被计算两次,所以此处有额外的代价 。
设 在棋盘上有一个相邻位置 。则,若 被选择,但 却没有被选择,显然这是不成立的。故此处有代价 。
其它的代价就只是一个点被分到某个集合中产生的代价(可能为负,此时就是收益)了,这部分是容易的。
现在正式考虑建图。事实上,本题的建图中并不需要解方程。我们发现, 和 应该是反向(即一个是 选 不选,一个是 选 不选)的,因为它们是同侧有代价;而 和 应该是同向的,因为它们是异侧有代价。于是,我们得到 和 是异侧的。这可能吗?
可能,因为原图是网格图,可以被黑白染色。
所以,我们不妨设 是 不选 选的,而 则是 选 不选的。
当 选时,其与 的边应该被割去,代价是 。所以连边 。
当 不选时,其没有代价,故其与 间无边。
当 选时,有边 (负权因为是收益);当 不选时,无代价,与 无边。
以及其对应的 的连边与 的相反。
现在考虑连接两点间的边。 间的边,若画出图来,会发现是 ; 间的边,则是 。
在建图的时候,注意使用我们上文提到的将边权化正的trick。
(均是有向边)
这样,我们便得到了需要的图;求其最小割即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=810;
const int M=2001000;
int dx[4]={1,0,-1,0},dy[4]={0,1,0,-1};
namespace MaxFlow{
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{int to,next,val;}edge[M];
void ae(int u,int v,int w){
// printf("%d %d %d\n",u,v,w);
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){
res+=flow;
reach=true;
return flow;
}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){
edge[i].val-=ff;
edge[i^1].val+=ff;
used+=ff;
if(used==flow)break;
}
}
return used;
}
inline void Dinic(){
while(bfs()){
reach=true;
while(reach)reach=false,dfs(S,0x3f3f3f3f);
}
}
}
using namespace MaxFlow;
class SurroundingGame{
private:
int a[30][30],b[30][30],n,m;
int trans(char ip){
if('0'<=ip&&ip<='9')return ip-'0';
if('a'<=ip&&ip<='z')return ip-'a'+10;
if('A'<=ip&&ip<='Z')return ip-'A'+36;
}
public:
int maxScore(vector<string>c,vector<string>w){
n=c.size(),m=c[0].size(),S=2*n*m,T=S+1,memset(head,-1,sizeof(head));
for(int i=0;i<n;i++)for(int j=0;j<m;j++)a[i][j]=trans(c[i][j])-trans(w[i][j]),b[i][j]=trans(w[i][j]);
// for(int i=0;i<n;i++,puts(""))for(int j=0;j<m;j++)printf("%d ",a[i][j]);puts("");
// for(int i=0;i<n;i++,puts(""))for(int j=0;j<m;j++)printf("%d ",b[i][j]);puts("");
int sum=0;
for(int i=0;i<n;i++)for(int j=0;j<m;j++){
if(a[i][j]>0){
if((i+j)&1)ae(S,i*m+j,a[i][j]);
else ae(i*m+j,T,a[i][j]);
}
if(a[i][j]<0){
if((i+j)&1)ae(i*m+j,T,-a[i][j]);
else ae(S,i*m+j,-a[i][j]);
sum+=-a[i][j];
}
sum+=b[i][j];
if((i+j)&1)ae(n*m+i*m+j,i*m+j,b[i][j]),ae(S,n*m+i*m+j,b[i][j]);
else ae(i*m+j,n*m+i*m+j,b[i][j]),ae(n*m+i*m+j,T,b[i][j]);
for(int k=0;k<4;k++){
int ii=i+dx[k],jj=j+dy[k];
if(ii>=n||ii<0||jj>=m||jj<0)continue;
if((i+j)&1)ae(n*m+i*m+j,ii*m+jj,0x3f3f3f3f);
else ae(ii*m+jj,n*m+i*m+j,0x3f3f3f3f);
}
}
Dinic();
// printf("%d %d\n",sum,res);
return sum-res;
}
}my;
CVII.[CTSC2008]祭祀
定义“偏序集”为一个集合 以及一种运算 构成的二元组 ,满足如下性质:
-
均成立
-
,则 成立
-
,则 成立
典型的例子是DAG上是否可达的关系,例如本题,当对所有的边跑完传递闭包后,得到的转移矩阵 即是一组偏序关系。另一个例子是集合间的包含关系()。
定义一组偏序集上的链是一组有序序列 满足 ,均有 。
定义一组偏序集上的反链是一组无序序列 满足 ,均有 。换句话说,两两集合不相互包含。
Dilworth定理:对于任何一组偏序集,其最长链长度等于其最小反链划分大小,其最长反链长度等于其最小链划分大小,其中一组划分指的是将一个集合划分成若干集合,使得两两集合的交集为空,所有集合的并集为全集。
最长链长度一定等于最小反链划分大小,因为我们每次均可挑出所有极小元素组成一条反链;第一次挑选一定挑出了最长链上最小元素(因为其不可能有后继元素,不然最长链还能更长),第二次挑选一定挑出了最长链上次小元素(因为其所有的后继元素一定都是极小元素,不然最长链还能更长),以此类推,最后一次挑选一定挑出最长链上最大元素。
关于最长反链长度的证明类似。
回到本题。明显,本题我们需要找出传递闭包后的偏序集的最长反链长度,并输出方案。最长反链长度等于最小链划分;最小链划分,其与最小路径覆盖等价,因为DAG的偏序集仍是DAG,所以可以通过经典的拆点后建二分图,并求出最大独立集的套路来求出最小链划分大小。
但是,求出最小链划分还不够,我们还要构造出一条最长反链。但首先先让我们找出求最大独立集的方法。
最大独立集等于点数减最小点覆盖,因为对于最小点覆盖外的任意一对点对间都不可能有边(不然就违背了点覆盖的定义)。而最长反链,就由拆点后两端均在独立集内的点构成。具体而言,设独立集大小为 ,反链大小为 ,最小点覆盖大小为 ,则应有 。考虑 ,应为只有一端在 内的点数,必定是 的。故必有 ,即 。而 ,就是最小路径覆盖问题的答案,也即最小链划分大小,即最长反链长度。则 即为最长反链。
于是问题转变为求出最大独立集,也即最小点覆盖的补集。故我们只需求出最小点覆盖。
考虑求出最大匹配。考虑从右侧所有非匹配点出发遍历图,当从右到左时只走非匹配边,从左往右时只走匹配边,则最终一定得到众多两端均在右侧的增广路,因为右侧的非匹配点不可能连到左侧的非匹配点(不然可以直接连接两者,与最大匹配前提相悖),则从遍历到的左侧点一定可以沿某条匹配边回到右侧。我们在左侧选取所有被遍历到的点,以及右侧所有未被遍历到的点,它们构成一组最小点覆盖。
显然,最小点覆盖的大小等于最大匹配大小,因为对于一个左侧遍历到的点,与其匹配的右侧点也一定被遍历到了,故此处只有左侧点被选择;对于一个右侧未被遍历到的点,因为其必定是一个被匹配的点(所有未被匹配的点都被作为起点进行了遍历),而其对应的左侧点又没有被选择,故此处只有右端点被选择;二者综合来看,即是所有匹配边选且仅被选一次,故上述结论成立。
找到最小点覆盖后,取补集即得到了左侧未被遍历到的点和右侧遍历到的点,它们构成最大独立集。
将上述结论结合后,便得到了求最长反链的方法。
下面我们考虑什么点可能出现在最长反链内。因为我们上述解法,若二分图匹配采用 的Dinic法的话,单次是 的,所以对于每个点都跑一次匹配,复杂度也不过 ,对于 是轻松通过的;于是我们考虑每个点,强制令其在反链内(这意味着所有它能到达或能到达它的点全部不可选择),并求出新图中的最长反链判断其是否比原图中长度只减小 即可。
总复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
namespace MaxFlow{
const int N=210,M=200000;
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{int to,next,val;}edge[M];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){res+=flow,reach=true;return flow;}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;}
}
return used;
}
inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}}
}
using namespace MaxFlow;
bool trans[110][110],on[210],vis[210],vld[110];
int mat[210];
char s[110],t[110];
void findpath(int x){
if(vis[x]==true)return;vis[x]=true;
for(int y=1;y<=n;y++)if(y!=mat[x]&&vld[y]&&trans[y][x-n]&&!vis[y])vis[y]=true,findpath(mat[y]);
}
int MaximalAntiChain(int ban){
memset(head,-1,sizeof(head)),cnt=res=0;
int all=0;
for(int i=1;i<=n;i++)vld[i]=!trans[ban][i]&&!trans[i][ban]&&i!=ban,all+=vld[i];
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(vld[i]&&vld[j]&&trans[i][j])ae(i,j+n,1);
for(int i=1;i<=n;i++)if(vld[i])ae(S,i,1),ae(i+n,T,1);
Dinic();
return all-res;
}
void GetScheme(){
memset(mat,0,sizeof(mat));
for(int x=1;x<=n;x++)for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to>n&&edge[i].to<=2*n&&!edge[i].val)mat[x]=edge[i].to,mat[edge[i].to]=x;
// for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
// for(int i=1;i<=n;i++)printf("%d ",mat[i+n]);puts("");
memset(vis,false,sizeof(vis));
for(int i=1;i<=n;i++)if(vld[i]&&!mat[i+n])findpath(i+n);
for(int i=1;i<=n;i++)if(vld[i]&&!vis[i]&&vis[i+n])s[i]='1';else s[i]='0';
}
int main(){
scanf("%d%d",&n,&m),S=2*n+1,T=2*n+2;
for(int i=1,u,v;i<=m;i++)scanf("%d%d",&u,&v),trans[u][v]=true;
for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)trans[i][j]|=trans[i][k]&&trans[k][j];
// for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",trans[i][j]);puts("");}puts("");
int mx=MaximalAntiChain(0);printf("%d\n",mx);
GetScheme(),printf("%s\n",s+1);
for(int i=1;i<=n;i++)t[i]=(MaximalAntiChain(i)==mx-1?'1':'0');
printf("%s\n",t+1);
return 0;
}
CXIII.[北京省选集训2019]图的难题/[UOJ#168][UR #11]元旦老人与丛林
两题是重题,第二题数据范围更大。
首先,我们发现,如果原图的 ,显然是不合法的;
再进一步思考,如果原图存在一个导出子图(包含一些点及所有两端均在该点集内的边的子图) 使得 ,则也一定不合法。
我们称“不存在上述导出子图”为“符合性质”。那么,如果原图 符合性质,是否就合法了呢?我们考虑归纳证明。
首先,当 时显然成立。否则,假设对于所有 均成立,考虑构造出 时的分配方案。
考虑 个节点中度数最少的那个,不妨设为 。当度数 时,显然总边数不小于 ,则此时整张图不符合上述 的前提条件,可以舍去。
则此度数只能为 。前三者时,我们一定可以把任一条边分到 条边时的白图里,另一条分到黑图里,并且两张图均仍是森林。
于是现在就只剩下度数为 的情形。
因为 符合性质,所以其任意子图 均有 。故我们定义一张子图 是“满”的当且仅当 。
则若两张子图 都是满的,则它们的并也是满的。因为 ,而因为 符合性质,所以 ;但是因为在上式中其前面是负号,所以有 。但是 也是 的子图,所以两者结合起来就只能有 ,即二者之并是满的。
有了上述结论,我们再设上述度数为 的点连到了 三个点。则在删去 及其连边后,在剩余的 个点的图中,同时包含 的满子图、同时包含 的满子图、同时包含 的满子图,这三者不可能同时出现,不然就可以把它们求并得到同时包含 的满子图。得到这张图后,再加入 及其连边,便得到了一张 的不合条件的图,与所有子图都符合条件矛盾。
于是便证明了至少有一张上述满子图不存在,不妨设为 的满子图不存在。而这时,如果我们往 个点的图中添加一条额外的 边,显然这时整张图依旧符合性质,且点数为 ,存在至少一种划分方案。在任一方案中,总有一棵森林包含 ,于是删去 ,连接 ,,同时把 加入另一棵森林。
于是便构造出一种合法的分配方案,则 符合性质是充要条件。
要判断是否存在不符条件的导出子图,等价于求 的 ,并观察其是否有 。我们发现,若我们选择了一条边,则其两端的节点也必须被选择——这看上去像一个最大权闭合子图问题。我们化边为点,其中边的权值为 ,点的权值为 ,然后求其最大权导出子图即可。
需要注意的是,该导出子图必须非空。所以,我们必须枚举一个点,强制其在导出子图中(即将其权值设作 ,然后最终求出来的最大权再减去 )。
对于第一题,枚举点并暴力建图是可以通过的;然而对于第二题,暴力建图会喜获TLE,需要手动退流以保证复杂度。
因为没退好流,所以最终还是T掉了。
T掉的UOJ#168的代码:
#include<bits/stdc++.h>
using namespace std;
int T,n,m;
namespace MaxFlow{
const int N=6010,M=2000000;
int head[N],cur[N],dep[N],cnt,S,T,res;
struct node{int to,next,val;}edge[M];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
}
queue<int>q;
inline bool bfs(){
memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
while(!q.empty()){
register int x=q.front();q.pop();
for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
}
return dep[T]>0;
}
bool reach;
inline int dfs(int x,int flow){
if(x==T){res+=flow,reach=true;return flow;}
int used=0;
for(register int &i=cur[x];i!=-1;i=edge[i].next){
if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;}
}
return used;
}
inline int Dinic(int s,int t){S=s,T=t,res=0;while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}return res;}
}
using MaxFlow::Dinic;
using MaxFlow::edge;
using MaxFlow::ae;
using MaxFlow::cnt;
using MaxFlow::head;
int tmp[2010],id[2010];
void read(int &x){
x=0;
char c=getchar();
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
int main(){
read(n),read(m),memset(head,-1,sizeof(head));
int S=n+m+1,T=n+m+2,sum=0;
for(int i=1,x,y;i<=m;i++)read(x),read(y),ae(S,n+i,1),ae(n+i,x,0x3f3f3f3f),ae(n+i,y,0x3f3f3f3f),sum+=1;
for(int i=1;i<=n;i++)id[i]=cnt,ae(i,T,2);
for(int i=1;i<=n;i++){
edge[id[i]].val=edge[id[i]^1].val=0;
sum+=Dinic(i,S);
sum-=Dinic(S,T);
if(sum>0){puts("No");return 0;}
edge[id[i]].val=2,edge[id[i]^1].val=0;
}
puts("Yes");
return 0;
}
CXXIII.[NOI2019] 序列
最近的NOI咋都喜欢玩模拟费用流这种阴间玩意
首先,这题不难看出是个费用流模型。但是我想的多数奇奇怪怪的网络流都是带上下界的,因此没有任何优化前景
正解是说,这个东西可以被看作是二分图最大权匹配的模型,其中任意匹配的点对不超过 对。任意匹配,可以通过建一个中间点,然后左部点全部连到该点,该点再连到全部右部节点,这样就能处理;而现在有了匹配上界的限制,就把该中间点拆点,使得只有一条流量为 的边。
因为这张图比较别致,所以可以考虑用模拟费用流优化。
什么是模拟费用流呢?就是用人类智慧手动跑费用流。
首先,二分图匹配问题要手动实现不是不可能的,只需要找到你匹配的点对即可。
于是我们考虑一点点地往图上添加流量,直到流量到达 。
我们记“任意流”为经由前述 的边的流量,并设 表示该边目前剩余的流量;再记“绑定流”为左部右部的同一位置配对的流量。
显然,任意时刻,任意流总是不小于绑定流。于是,若 非零,就流任意流即可。流任意流的方法也很简单,找到左右部权值最大的点,连一块即可。
否则,就只能流绑定流了。绑定流有如下流法:
-
直接流 (令带 的表示右部节点)。显然此时取所有双方都未配对的点对中最大的那个即可。
-
考虑现行有一条任意流 。我们可以找到任意一个未配对节点 ,并将其修改成 ,这样子就让 这两个点参与了匹配。
-
同上,只不过这里是找到 ,然后修改为 。
每次流绑定流就从三者中找到最优的那种流即可。
可以发现,二三两种操作本质上是先退流再增广,因此替代了费用流的操作,也因此成功模拟了费用流。
我们考虑要维护什么才能支持查询。
首先,左右部未匹配的点显然是要维护的。我们各开一个大根堆维护其中权值最大的。
然后, 这种直接流的也要开一个堆维护。
再后,因为 中, 与 两个点都分别在二、三两种方案中参与了匹配,所以我们还需要分别维护 的最大值。准确地说, 就是所有自身尚未匹配,但其左部对应节点已经匹配的右部节点,而 同理是所有自身尚未匹配,右部对应节点已经匹配的左部节点。继续开堆维护即可。
至于 和 ,因为也没有特别的限制,所以就选择左右部未匹配的点中权值最大的即可,我们上文已经维护过了。
现在,就是一些具体的细节了。举例来说,因为随着某些操作的执行,另一些堆中的某些点可能已经无效了,但我们又不太能把它从堆里掏出来,因此在堆顶出堆的时候,先判断一下堆顶的元素是否合法即可,如果不合法就一直弹,直到堆顶合法,或是堆空掉。
再举例来说,当我们流任意流时,可能会出现任意流流成了绑定流,也即两端位置相同的情形。这时候,要记得把它算成绑定流,也即回退给 一点流量。
还有一个例子,就是若我们有任意流匹配 ,完全可以把它修改成 ,使得 凭空多了一点流量,且每个节点的匹配与否的状态无变化。因此,每当连边 时,都要判断 是否已经匹配,或者 是否已经匹配,如果是的话就可以调整了。
并且,调整可能还不是一次性的。还是上文的例子,当连边 时,仍有可能出现 匹配或是 匹配的情形,也要递归下去处理;甚至,还会出现 任意流流成了绑定流的例子,也要判掉。
于是综上起来,这里的 Adjust
函数就可以递归地处理上述操作,其中 mat,mbt
分别是左部的匹配对象,右部的匹配对象。
void Adjust(int x,int y){//try to adjust the FREE-FLOW (x,y)
if(x==y){fre++;return;}//no need to adjust.
if(mbt[x]){mat[mbt[x]]=y,mbt[y]=mbt[x],mat[x]=mbt[x]=x,fre++,Adjust(mbt[y],y);return;}
if(mat[y]){mbt[mat[y]]=x,mat[x]=mat[y],mat[y]=mbt[y]=y,fre++,Adjust(x,mat[x]);return;}
}
最后的一个例子,是某些堆空掉的情形。此时,对应的方案就不合法,不能予以选择。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T,n,m,fre,a[200100],b[200100],mat[200100],mbt[200100];
ll res;
priority_queue<pair<int,int> >as,bs,am,bm,o;//s:spare nodes; m:parterner matched,self spare nodes; o:pairs.
void Adjust(int x,int y){//try to adjust the FREE-FLOW (x,y)
if(x==y){fre++;return;}//no need to adjust.
if(mbt[x]){mat[mbt[x]]=y,mbt[y]=mbt[x],mat[x]=mbt[x]=x,fre++,Adjust(mbt[y],y);return;}
if(mat[y]){mbt[mat[y]]=x,mat[x]=mat[y],mat[y]=mbt[y]=y,fre++,Adjust(x,mat[x]);return;}
}
void clear(priority_queue<pair<int,int> >&q){while(!q.empty())q.pop();}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d%d",&n,&m,&fre),fre=m-fre,res=0;
clear(as),clear(bs),clear(am),clear(bm),clear(o);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),as.push(make_pair(a[i],i)),mat[i]=0;
for(int i=1;i<=n;i++)scanf("%d",&b[i]),bs.push(make_pair(b[i],i)),mbt[i]=0;
for(int i=1;i<=n;i++)o.push(make_pair(a[i]+b[i],i));
// printf("%d %d %d %d %d\n",as.size(),bs.size(),am.size(),bm.size(),o.size());
while(m--){
// printf("%d:%d\n",m,fre);
// for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
// for(int i=1;i<=n;i++)printf("%d ",mbt[i]);puts("");
while(!o.empty()&&(mat[o.top().second]||mbt[o.top().second]))o.pop();
while(mat[as.top().second])as.pop();
while(mbt[bs.top().second])bs.pop();
while(!am.empty()&&(mat[am.top().second]||!mbt[am.top().second]))am.pop();
while(!bm.empty()&&(!mat[bm.top().second]||mbt[bm.top().second]))bm.pop();
if(fre){
int x=as.top().second,y=bs.top().second;as.pop(),bs.pop();
res+=a[x]+b[y];
mat[x]=y,mbt[y]=x,fre--;
if(!mbt[x])bm.push(make_pair(b[x],x));
if(!mat[y])am.push(make_pair(a[y],y));
// printf("FRE:%d %d\n",x,y);
Adjust(x,y);
continue;
}else{
int t1=0;if(!o.empty())t1=o.top().first;
int t2=0;if(!am.empty())t2=am.top().first+bs.top().first;
int t3=0;if(!bm.empty())t3=bm.top().first+as.top().first;
// printf("SIT:%d %d %d\n",t1,t2,t3);
if(t1>=t2&&t1>=t3){
// printf("OO:%d\n",o.top().second);
res+=t1;
mat[o.top().second]=mbt[o.top().second]=o.top().second;
o.pop();
continue;
}
if(t2>=t3){
int x=am.top().second,y=bs.top().second;am.pop(),bs.pop();
// printf("AM:%d %d\n",x,y);
res+=t2;
int z=mbt[x];
mat[z]=y,mbt[y]=z;
mat[x]=mbt[x]=x;
if(!mat[y])am.push(make_pair(a[y],y));
Adjust(z,y);
}else{
int x=as.top().second,y=bm.top().second;as.pop(),bm.pop();
// printf("BM:%d %d\n",x,y);
res+=t3;
int z=mat[y];
mbt[z]=x,mat[x]=z;
mat[y]=mbt[y]=y;
if(!mbt[x])bm.push(make_pair(b[x],x));
Adjust(x,z);
}
}
}
// for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
// for(int i=1;i<=n;i++)printf("%d ",mbt[i]);puts("");
printf("%lld\n",res);
}
return 0;
}
CXXV.UOJ#455. 【UER #8】雪灾与外卖
题意:有 只老鼠和 个洞,老鼠分别位于 ,洞分别位于 ,最多能塞下 只老鼠,且塞一只老鼠的代价是 。一只老鼠从位置 移动到位置 需要花费额外的代价 。求使所有老鼠有洞进的最小代价。
考虑从左往右遍历每只老鼠和每个洞,计算匹配。
对于老鼠:
- 与之前的一个洞匹配。
- 抢走之前某只老鼠的洞。
自然,老鼠也可以留着匹配以后的洞。但是为了避免出现老鼠配不上洞的情形,我们强制老鼠必须配上一个洞。但万一真的没有剩的洞了 呢?那就让它配上一个代价为 的虚洞,这样子就一定会被接下来的某个洞抢走。
对于洞:
- 抢走之前某只洞的老鼠。
- 等待后面的某只老鼠与其匹配。
这里发现洞的操作中没有直接与老鼠匹配的选项,因为老鼠都已经有匹配了(可能匹配的是虚洞)。
具体而言,我们分别对老鼠和洞各维护一个小根堆。
对于老鼠操作1和2,都直接与洞堆顶匹配即可。若洞堆顶的元素是 ,则答案增加 ,然后洞堆 pop
。同时,为了方便退流,要同时再往老鼠堆中扔入一个 。退流操作支持了洞操作1。
可能有人会问,对于老鼠操作2,那个被抢走洞的老鼠不需要配对吗?但是我们发现,有一个显然的性质是老鼠与洞间的连线不可能交叉(不然可以把叉打开使得答案严格不增),因此一对老鼠与洞不会同时被抢走。故这个情形只需在洞的操作中处理即可。
对于洞操作1,就直接与老鼠堆顶匹配即可。若老鼠堆顶的元素是 ,则答案增加 。但是需要注意的是,当 时,抢了还不如不抢,因此此时就可以直接跳过操作1,执行操作2。同时,为了方便退流,还要往洞堆里扔入 。退流操作支持了老鼠操作2。
同时,洞操作1还要兼顾我们上文没有进行的操作,即,那个被抢走老鼠的洞还需要配对。我们不选择在抢的洞处统计,而选择在被抢的洞处统计。因此,每执行一个洞操作1,我们同时还要往老鼠堆中扔入 ,用来表示这个洞的老鼠被抢走了。
对于洞操作2,就直接往洞堆里扔入 即可。洞操作2支持了老鼠操作1。
但是我们发现这样搞总操作数是 级别的,不可能通过。但是,发现因为有着“一对老鼠与洞不会同时被抢走”的条件,所以如果能够使得每对只会被处理一次,复杂度就是正确的 。
于是,我们发现对于 的项与 的项,其对于同一个洞的所有位置都是相等的。于是我们堆中元素需要两维,一维是值,一维是出现次数,即可将复杂度优化到均摊 。总复杂度 。
如果要修改堆顶的元素的值可以使用 mutable
关键字。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,px[100100],py[100100],fod[100100],cst[100100],all;
ll res;
struct dat{ll v;mutable int t;dat(ll V,int T){v=V,t=T;}friend bool operator<(const dat&x,const dat&y){return x.v>y.v;}};
priority_queue<dat>mou,hol;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&px[i]);
for(int i=1;i<=m;i++)scanf("%d%d%d",&py[i],&cst[i],&fod[i]),fod[i]=min(fod[i],n),all+=fod[i],all=min(all,n);
if(all<n){puts("-1");return 0;}
hol.emplace(0x3f3f3f3f3f,0x3f3f3f3f);
for(int i=1,j=1;i<=n||j<=m;){
if(i<=n&&(px[i]<=py[j]||j>m)){
ll c=hol.top().v+px[i];
if(!--hol.top().t)hol.pop();
res+=c,mou.emplace(-c-px[i],1);
i++;
}else{
int tot=0;
while(fod[j]&&!mou.empty()){
ll c=(mou.top().v+cst[j]+py[j]);
if(c>=0)break;
int mn=min(mou.top().t,fod[j]);
res+=c*mn,hol.emplace(-c+cst[j]-py[j],mn);
fod[j]-=mn,tot+=mn;
if(!(mou.top().t-=mn))mou.pop();
}
if(fod[j])hol.emplace(cst[j]-py[j],fod[j]);
if(tot)mou.emplace(-cst[j]-py[j],tot);
j++;
}
}
printf("%lld\n",res);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?