记录下网络流的学习,以及自己的一些理解
1.DAG最小路径覆盖
题目大意:DAG求最小路径覆盖,输出方案
思路:将n个点看成单独n条路径,接下来拆点,将每个点放在入点出点两个集合中,如果有边x->y,则x出指向y入,初始化时候所有点出点连源点,入点连汇点,最小路径数 = 总路径n - 最大流,最大流就是我们去匹配出入点的最大边数,实质就是匈牙利算法啊
建图示意 ,图中只有1->3一条路径可以从原始n条路径中拼接起来,即3-1=2,最小路径数,我们发现,连向汇点的点如果不和源点联通(点1,2),说明它会作为一条新路径的起点,即要新开一条路给它,因为输出方案时候要用,也就是说它连向汇点的边的流量此时为1(因为它未用过啊),edge[T^1].flow==1
注意点:(可能对于我来说),链式前向星从0或者2开始存图,因为要存反边,平时都从1开始存的,反边变成0了,调了两天才看出来
———————————————————————————————————————————————————————
回头又思烤了虾,说是拆成出点和入点有问题,就是进行边的匹配,尽可能的选多的边,才能使得覆盖路径最少,说拆点反而有误导倾向了 = = ||
如果拆点做的话,按照正常逻辑建图,点i的出入点连边,而每个点都可以作为路径的起点和终点,那最大流肯定为n了
#include <iostream>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <map>
#include <vector>
#include <queue>
using namespace std;
#define _for(i,a,b) for(int i=(a) ;i<=(b) ;i++)
#define _rep(i,a,b) for(int i=(a) ;i>=(b) ;i--)
#define mst(v,s) memset(v,s,sizeof(v))
#define pb push_back
#define IOS ios::sync_with_stdio(false)
//#define int long long
#define inf 0x3f3f3f3f
typedef long long ll;
const int N=1e4+10;
int n,np,nc,m;
int s,t;
int d[N];
int pre[N];
int nxt[N];
struct ty
{
int t,next,flow;
}edge[5010];
int tot=1,head[N];
void add(int x ,int y, int flow)
{
edge[++tot]={y,head[x],flow};
head[x]=tot;
edge[++tot]={x,head[y],0};
head[y]=tot;
}
void ini()
{
tot=1;
mst(head,-1);
}
bool bfs(int t)
{
mst(d,0);
queue <int> q;
q.push(0);
d[0]=1;
while( !q.empty() )
{
int x = q.front();
q.pop();
if( x==t ) return true;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( edge[i].flow > 0 && !d[y] )
{
d[y] = d[x] + 1;
q.push(y);
}
}
}
return false;
}
int dfs(int x ,int flow ,int t)
{
if( x==t ) return flow;
int sum=0;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( d[y] == d[x] +1 && edge[i].flow>0 )
{
int temp = dfs(y,min(edge[i].flow , flow-sum),t);
if( temp )//有流就记录路径
{
nxt[x] =y-n;//记录下一步
}
edge[i].flow -= temp;
edge[i^1].flow += temp;
sum += temp;
if( sum == flow ) return flow;
}
}
return sum;
}
signed main()
{
///!!!
// freopen("data.txt","r",stdin);
//!!!=
IOS;
ini();
cin>>n>>m;
s=0,t=2*n+1;
//实质是二分图匹配
_for(i,1,n)
{
add(0,i,1);add(i+n,2*n+1,1);
}
_for(i,1,m)
{
int x,y;
cin>>x>>y;
add(x,y+n,1);
}
int ans=0;
while( bfs(t) ) ans += dfs(s,inf,t);
for(int i=head[t] ;i!=-1 ;i=edge[i].next)
{
if( edge[i^1].flow ==1) //如果此点连到汇点
//说明是所在路径的起点,注意对于汇点来说它是反边
{
int y = edge[i].t - n;
while( y )
{
cout<<y<<" ";
y = nxt[y];
}
cout<<endl;
}
}
cout<<n-ans<<endl;
}
2 魔术球
题目大意:n个柱子,每个柱子上放数字,相邻数字的和必须为平方数,求n个柱子最多放几个数字
思路:既然数字的接龙有限制,若x+y为平方数,我们就对x,y连边,每个柱子肯定放的数字越多越好,柱子数也要尽可能的少,那不就转化为上一题dag最小路径覆盖了吗?
柱子数就是路径数量,建图和上面几乎一样
不过这里路径条数规定好了,所以我们不断往图里加点,如果路径条数仍小于n就继续加点,否则break。简单说就加一个点,跑一下网络流,直到路径数多于n。
注意:要顺序输出柱子的方案,而且卡格式,不能输出多余空格 = = ||
#include <iostream>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <map>
#include <vector>
#include <queue>
using namespace std;
#define _for(i,a,b) for(int i=(a) ;i<=(b) ;i++)
#define _rep(i,a,b) for(int i=(a) ;i>=(b) ;i--)
#define mst(v,s) memset(v,s,sizeof(v))
#define pb push_back
#define IOS ios::sync_with_stdio(false)
#define inf 0x3f3f3f3f
typedef long long ll;
const int N=2e5+10;
const int p = 100000;
int n,np,nc,m;
int s,t;
int d[N];
int pre[N];
int nxt[N];
int num[N],cnt;
struct ty
{
int t,next,flow;
}edge[N];
int tot=1,head[N];
void add(int x ,int y, int flow)
{
edge[++tot]={y,head[x],flow};
head[x]=tot;
edge[++tot]={x,head[y],0};
head[y]=tot;
}
void ini()
{
tot=1;
mst(head,-1);
}
bool bfs(int t)
{
mst(d,0);
queue <int> q;
q.push(s);
d[s]=1;
while( !q.empty() )
{
int x = q.front();
q.pop();
if( x==t ) return true;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( edge[i].flow > 0 && !d[y] )
{
d[y] = d[x] + 1;
q.push(y);
}
}
}
return false;
}
int dfs(int x ,int flow ,int t)
{
if( x==t ) return flow;
int sum=0;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( d[y] == d[x] +1 && edge[i].flow>0 )
{
int temp = dfs(y,min(edge[i].flow , flow-sum),t);
if( temp )//有流就记录路径
{
nxt[x] =y-p;//记录下一步
}
edge[i].flow -= temp;
edge[i^1].flow += temp;
sum += temp;
if( sum == flow ) return flow;
}
}
return sum;
}
bool check(int x ,int y)
{
int temp=x+y;
// cout<<" temp "<<temp<<" "<<(int) sqrt(temp)*(int) sqrt(temp)<<endl;
return (int) sqrt(temp)*(int) sqrt(temp) == temp;
}
signed main()
{
///!!!
// freopen("data.txt","r",stdin);
//!!!=
// IOS;
ini();
cin>>n;
s=0,t=2e5+1;
int ans=0;
for(int i=1;;i++)//枚举加入的数
{
add(s,i,1);add(i+p,t,1);
_rep(j,i-1,1)
{
// cout<<" i j "<<i<<" "<<j<<endl;
if( check(i,j) )
{
// cout<<" ok "<<j<<"-> "<<i<<endl;
add(j,i+p,1);
continue;
}
}
while( bfs(t) ) ans += dfs(s,inf,t);
if( i - ans > n )//柱子放不下了
{
cout<<i-1<<endl;
for(int j=head[t] ;j!=-1 ;j=edge[j].next)
{
if( edge[j^1].flow==1 && edge[j].t-p!=i )//如果没到汇点
//说明是路径起点
{
int y = edge[j].t-p;
num[++cnt] = y;
}
}
break;
}
}
sort(num+1,num+1+cnt);//对柱子起点排序
_for(i,1,cnt)
{
int y = num[i];
while(y)
{
if(nxt[y]!=0 )cout<<y<<" ";
else cout<<y;
y = nxt[y];
}
cout<<endl;
}
}
3最长递增子序列问题
n<=500
思路:(1)入门dp略了
(2)拆点——出点入点,跑完问题1的dp后,我们获得dp[i],以第i个数为结尾的最长上升子序列的长度。
如果求多条的话,就可以用网络流了。容量为1——每个数字只能用一次。两个数字a[i],a[j]要连边的话要符合dp[i] == dp[j] + 1&&a[i]>=a[j],因为要组成最多数的最长上升子序列的话,至少要遵照问题1跑出来的dp值相邻才能进行连边,然后dp值为1的连源点,dp值最大的连汇点,起点终点的选择应该挺好理解的。
(3)x1xn次数无限制,那就给1和n的出入点之间加容量为无穷的边,1和源点连inf的边,n和汇点的连边需要判断一下dp[n]是否为最大值(注意点),dp[1]因为肯定最小所以肯定为起点,dp[n]则不一定
#include <iostream>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <map>
#include <vector>
#include <queue>
using namespace std;
#define _for(i,a,b) for(int i=(a) ;i<=(b) ;i++)
#define _rep(i,a,b) for(int i=(a) ;i>=(b) ;i--)
#define mst(v,s) memset(v,s,sizeof(v))
#define pb push_back
#define IOS ios::sync_with_stdio(false)
//#define int long long
#define inf 0x3f3f3f3f
typedef long long ll;
const int N=1e4+10;
const int p = 100000;
int s,t,n;
int d[N];
int a[N],dp[N];
int nxt[N];
int ans1;
struct ty
{
int t,next,flow;
}edge[N];
int tot=1,head[N];
void add(int x ,int y, int flow)
{
edge[++tot]={y,head[x],flow};
head[x]=tot;
edge[++tot]={x,head[y],0};
head[y]=tot;
}
void ini()
{
tot=1;
mst(head,-1);
}
bool bfs(int t)
{
mst(d,0);
queue <int> q;
q.push(s);
d[s]=1;
while( !q.empty() )
{
int x = q.front();
q.pop();
if( x==t ) return true;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( edge[i].flow > 0 && !d[y] )
{
d[y] = d[x] + 1;
q.push(y);
}
}
}
return false;
}
int dfs(int x ,int flow ,int t)
{
if( x==t ) return flow;
int sum=0;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( d[y] == d[x] +1 && edge[i].flow>0 )
{
int temp = dfs(y,min(edge[i].flow , flow-sum),t);
edge[i].flow -= temp;
edge[i^1].flow += temp;
sum += temp;
if( sum == flow ) return flow;
}
}
return sum;
}
void solve()
{
//拆点
s=0,t=2*n+1;
_for(i,1,n)
{
if( dp[i] == 1 ) add(s,i,1);//dp值为1,向起点连边
if( dp[i] == ans1 ) add(i+n,t,1);//dp值为max,要作为路径终点,向汇点连边
_for(j,i+1,n)
{
if( dp[i] +1 == dp[j] && a[j]>=a[i] )//不仅要dp值相邻且a[i]值也要满足不下降
{
add(i+n,j,1);
}
}
add(i,i+n,1);//入点向出点连边
}
int ans=0;
while( bfs(t) ) ans += dfs(s,inf,t);-----
cout<<ans<<endl;
add(1,1+n,inf);add(s,1,inf);
add(n,n+n,inf);
if( dp[n] == ans1)
add(n+n,t,inf);
while( bfs(t) ) ans += dfs(s,inf,t);
cout<<ans<<endl;
}
signed main()
{
///!!!
// freopen("data.txt","r",stdin);
//!!!=
IOS;
ini();
cin>>n;_for(i,1,n) cin>>a[i];
_for(i,1,n) dp[i]=1;
_for(i,2,n)
{
_for(j,1,i-1)
{
if( a[i] >= a[j] ) dp[i] = max(dp[j]+1,dp[i]);
}
ans1 = max(ans1,dp[i]);
}
cout<<ans1<<endl;
solve();
}
4 方格取数
n,m<=100;
思路:之前训练赛做过一题数据范围是n,m<=20,当时是状压做的,现在压不了了
写下最小割做法。
明显这是张二分图(相邻数字不能同时取),我们将图染成黑白后,假定所有数全都取,然后存在互相限制的点相互连边,表示它们直接存在矛盾关系,不能共存,问题就转化成删去最小边权和的边使得源点到汇点没有流量。
如图所示,源点(汇点)到点之间的边权为方格内的值
又由最小割=最大流定理,答案即为sum - 最大流
#include <iostream>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <map>
#include <vector>
#include <queue>
using namespace std;
#define _for(i,a,b) for(int i=(a) ;i<=(b) ;i++)
#define _rep(i,a,b) for(int i=(a) ;i>=(b) ;i--)
#define mst(v,s) memset(v,s,sizeof(v))
#define pb push_back
#define IOS ios::sync_with_stdio(false)
#define int long long
#define inf 0x3f3f3f3f
typedef long long ll;
const int N=1e3+10;
int n,np,nc,m;
int s,t;
int d[N];
int mp[N][N];
struct ty
{
int t,next,flow;
}edge[2*6010];
int tot=1,head[N];
void add(int x ,int y, int flow)
{
edge[++tot]={y,head[x],flow};
head[x]=tot;
edge[++tot]={x,head[y],0};
head[y]=tot;
}
void ini()
{
tot=1;
mst(head,-1);
cin>>n>>m;
_for(i,1,n) _for(j,1,m) cin>>mp[i][j];
}
bool bfs(int t)
{
mst(d,0);
queue <int> q;
q.push(0);
d[0]=1;
while( !q.empty() )
{
int x = q.front();
q.pop();
// cout<<" "<<x<<endl;
if( x==t ) return true;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( edge[i].flow > 0 && !d[y] )
{
d[y] = d[x] + 1;
q.push(y);
}
}
}
return false;
}
int dfs(int x ,int flow ,int t)
{
if( x==t ) return flow;
int sum=0;
for(int i=head[x] ;i!=-1;i=edge[i].next)
{
int y = edge[i].t;
if( d[y] == d[x] +1 && edge[i].flow>0 )
{
int temp = dfs(y,min(edge[i].flow , flow-sum),t);
edge[i].flow -= temp;
edge[i^1].flow += temp;
sum += temp;
if( sum == flow ) return flow;
}
}
return sum;
}
signed main()
{
///!!!
// freopen("data.txt","r",stdin);
//!!!=
IOS;
ini();
s=0,t=n*m+1;
int sum=0;
_for(i,1,n)
{
_for(j,1,m)
{
sum += mp[i][j];
int num = (i-1)*m+j;
if( (i+j)%2==0 )
{
add(s,num,mp[i][j]);
if( j<m ) add(num,num+1,inf);
if( j>1 ) add(num,num-1,inf);
if( i<n ) add(num,num+m,inf);
if( i>1 ) add(num,num-m,inf);
}
else
{
add(num,t,mp[i][j]);
}
}
}
int ans=0;
while( bfs(t) ) ans += dfs(s,inf,t);
cout<<sum-ans;
}
5.圆桌问题
鸽了一周多了,速速补上。。
思路1:网络流,挺好想的,两个集合,单位和餐桌,限制分别转化为和源点汇点的边容量,单位到餐桌的边容量为1
思路2:贪心,我们先处理人多的单位,把人安置到空位多的座位上,口胡一下用优先队列维护方便点
6.星际转移
思路:枚举天数,然后拆点,将每个空间站拆成第一天的空间站x、第二天的空间站x…… ,然后匹配的点连边,容量为限制人数。
当最大流跑满k时输出天数
注意地球和月球无法走通的情况,我一开始用的并查集判的,不过其实是错的
可能存在联通但是无法到达的情况,因为飞船的移动是具有周期性的,不能断言如果存在联通则一定能到月球,存在死循环的情况,当天数足够大时break就行
Ps:发现并查集写错了,然后翻博客把以前的代码贴上去,又发现以前的代码是错的。。。翻了一下模板,模板的代码也是错的。。。。。
以下题非24题里的QAQ
abc231H
题意:H*W的矩阵,(ai,bi)的位置上有个花费为ci的物品,要求选择若干物品,覆盖每行和每列的最小费用是多少?(H,W<=1e3)
Solution:最大费用可行流
刚开始在最小割方向想,发现建图有点困难,又想了下最小路径覆盖,不过这里带权。二分图匹配做不了。。
首先容易想到分成行和列两个集合,然后先贪心地对每行每列用最小的费用去覆盖掉。再考虑能不能减少费用,也就是去找增广路。
设min[i]表示覆盖掉i点的最小费用
到这用最大费用可行流,对于(ai,bi,ci)我们连一条(ai,bi+H)权值为min[a[i]] +min[b[i]+H] - c[i]的边(边权要乘-1),所有容量都设为1。
因为对于每个点外卖都先贪心地单独覆盖掉了,剩下来的就是花更少的费用去匹配掉一些点,容量设为1,是去找每行能不能找一下增广路,对于i行,它只需要考虑自己(因为现在每行每列都被覆盖了),然后在考虑自己的过程中顺便就也顺便匹配掉了j列,因为是费用最大流,那费用优先,找到最大费用就ok了。
#define int long long
const int maxn=2e3+10;
bool vis[maxn];
int n,m,s,t,x,y,z,f,dis[maxn],pre[maxn],last[maxn],flow[maxn],maxflow,mincost;
struct Edge{
int to,next,flow,dis;//flow流量 dis花费
}edge[100000];
int head[maxn],num_edge;
queue <int> q;
void add_edge(int from,int to,int flow,int dis){
edge[++num_edge].next=head[from];
edge[num_edge].to=to;
edge[num_edge].flow=flow;
edge[num_edge].dis=dis;
head[from]=num_edge;
edge[++num_edge].next=head[to];
edge[num_edge].to=from;
edge[num_edge].flow=0;
edge[num_edge].dis=-dis;
head[to]=num_edge;
}
bool spfa(int s,int t){
fill(dis,dis+1+t,-inf);
fill(vis,vis+1+t,0);
fill(last,last+1+t,-1);
q.push(s); vis[s]=1; dis[s]=0;flow[s]=inf;
while (!q.empty()){
int now=q.front();
q.pop();
vis[now]=0;
for (int i=head[now]; i!=-1; i=edge[i].next){
if( edge[i].flow<=0 ) continue;
if ( dis[edge[i].to]<dis[now]+edge[i].dis){
// cout<<" "<<edge[i].to<<endl;
dis[edge[i].to]=dis[now]+edge[i].dis;
last[edge[i].to]=i;
flow[edge[i].to]=min(flow[now],edge[i].flow);
if (!vis[edge[i].to]){
vis[edge[i].to]=1;
q.push(edge[i].to);
}
}
}
}
return dis[t]>0;
}
void MCMF(){
while (spfa(s,t)){
int now=t;
maxflow+=flow[t];
mincost+=flow[t]*dis[t];
while (now!=s){//从源点一直回溯到汇点
edge[last[now]].flow-=flow[t];//flow和dis容易搞混
edge[last[now]^1].flow+=flow[t];
now = edge[last[now]^1].to;
}
}
}
int mi[maxn],a[maxn],b[maxn],c[maxn];
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
#endif
mst(head,-1);
num_edge=1;
int H,W;cin>>H>>W>>n;
_for(i,1,H+W) mi[i] = inf;
_for(i,1,n){
cin>>a[i]>>b[i]>>c[i];
mi[a[i]] = min(mi[a[i]],c[i]);
mi[b[i]+H] = min(mi[b[i]+H],c[i]);
}
int ans = 0;
s = 0 , t = H+W+1;
_for(i,1,H){
add_edge(s,i,1,0);
ans += mi[i];
}
_for(i,1,W){
add_edge(i+H,t,1,0);
ans += mi[i+H];
}
_for(i,1,n){
add_edge(a[i],b[i]+H,1,mi[a[i]]+mi[b[i]+H]-c[i]);
}
MCMF();
ans -= mincost;
cout<<ans<<endl;
}