模拟网络流

模拟网络流

对于一些题目,我们或许可以发现可以用网络流建模,但是一看数据范围网络流又跑不了的时候。就可以思考是否有特殊性质,用别的算法模拟网络流。

模拟最大流

最常用的套路就是最大流与最小割的互相转化。常用来模拟的算法有 \(dp\) 贪心甚至可以直接枚举。

CF724E Goods transportation

网络流建模是简单的,\(s\) 向城市连 \(p_i\) ,城市与城市之间连 \(c\) ,城市与 \(t\)\(s_i\) ,最大流即是答案。

然后考虑模拟最大流,先转为最小割,我们发现如果一个点与 \(t\) 相连花费的代价 \(p_i+c\times j\) (\(j\) 是前面与 \(s\) 相连的城市数目),因为我们要割掉与 \(s\) 的边同时割掉与前面连着 \(s\) 的点直接相连的边,与 \(s\) 相连的代价是就是 \(s_i\) 。我们发现代价至于 \(i,j\) 有关,所以考虑用 dp 把 \(i,j\) 记下来。

\(f_{i,j}\) 表示处理到第 \(i\) 个点前面有 \(j\) 个点与 \(s\) 相连的答案,转移就是 \(f_{i,j}=min(f_{i,j-1}+s_i,f_{i,j}+p_i+c\times j)\) 。时间复杂度是 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10;
int n,c,dp[2][N],p[N],s[N],ans; 
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>c;
	for(int i=1;i<=n;i++)cin>>p[i];
	for(int i=1;i<=n;i++)cin>>s[i];
	memset(dp,0x3f,sizeof(dp));
	ans=dp[1][0];
	dp[0][0]=0;
	for(int i=1;i<=n;i++)
	{
		int op=i&1;
		for(int j=0;j<=i;j++) 
		{
			dp[op][j]=ans;//赋inf 
			if(j>=1)dp[op][j]=min(dp[op][j],dp[1-op][j-1]+s[i]);
			if(j<i)dp[op][j]=min(dp[op][j],dp[1-op][j]+p[i]+c*j);
		}	
	}
	for(int j=0;j<=n;j++)
		ans=min(ans,dp[n&1][j]); 
	cout<<ans<<'\n';
	return 0; 
} 

还有时间复杂度更优秀 \(O(n\log n)\) 的贪心做法。

P9902 『PG2』模拟最大流

可以发现与上一道题大同小异。唯一的不同是不只与前面和 \(s\) 相连的个数有关,与具体是那几个点有关,还告诉我们从小向大连最多差 \(7\) ,明示我们状压。设 \(f_{i,j}\) 表示处理前 \(i\) 个数前面的 \(k+1\) (包括自己)个数的状态是 \(j\)\(0\) 表示与 \(t\) 连,\(1\) 表示与 \(s\) 连)的答案,直接转移即可。我的写法比较劣有一点细节需要注意。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10;
int n,m,k,dp[2][N],jz[N][10],p[N],s[N],ans; 
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>k;k++;
	for(int i=1;i<=m;i++)
	{
		int u,v,w;cin>>u>>v>>w;
		if(u==v)continue;
		if(u==1)p[v]+=w;
		if(v==n)s[u]+=w; 
		jz[v][v-u]+=w;
	}
	memset(dp,0x3f,sizeof(dp));
	ans=dp[1][0];
	dp[1][0]=0;
	for(int i=2;i<n;i++)//特判1-n直接相连 
	{
		int op=i&1;
		for(int j=0;j<(1<<k);j++)dp[op][j]=ans;//赋inf 
		for(int j=0;j<(1<<k);j++) 
		{
			dp[op][((j<<1)|1)&((1<<k)-1)]=min(dp[op][((j<<1)|1)&((1<<k)-1)],dp[1-op][j]+s[i]);
			int jzz=0;
			for(int z=1;z<k;z++)if((1<<(z-1))&j)jzz+=jz[i][z];
			dp[op][(j<<1)&((1<<k)-1)]=min(dp[op][(j<<1)&((1<<k)-1)],dp[1-op][j]+p[i]+jzz);
		}	
	}
	for(int j=0;j<(1<<k);j++)
		ans=min(ans,dp[(n-1)&1][j]); 
	cout<<ans+s[1]<<'\n';
	return 0; 
} 

[ARC125E] Snack

建模同样简单,\(s\) 向糖果 \(i\)\(a_i\) , 糖果 \(i\) 向小孩 \(j\) 之间连 \(b_j\) ,小孩 \(j\)\(t\)\(c_j\) ,最大流即是答案。

与前两题相比,我们边的类型多了,不能简单的判断与一个划给 \(s\) 还是划给 \(t\) 。但糖果 \(i\) 向小孩 \(j\) 之j间的边首先任意一对都有,同时只跟 \(j\) 有关,所以我们考虑对每个小孩考虑。发现糖果有用的只有多少点与 \(s\) 的边没被割,这个可以枚举个数 \(k\) ,然后贪心地选 \(a_i\) 最小的 \(k\) 个。然后对每个小孩的代价就是 \(\min(c_j,k\times b_j)\) ,对小孩按 \({c_j\over b_j}\) 排序即可双指针快速做。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+10; 
int n,m,a[N],sumc,sumb,sum;
struct node
{
	int b,c;
}ch[N];
bool cmp(node x,node y)
{
	return x.c*y.b<x.b*y.c;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=m;i++)cin>>ch[i].b;
	for(int i=1;i<=m;i++)cin>>ch[i].c,sumc+=ch[i].c;
	sort(a+1,a+n+1);
	sort(ch+1,ch+m+1,cmp);
	int j=m,ans=1e18;
	for(int i=n;i>=0;i--)//有多少没割 
	{
		sum+=a[n-i];
		for(;j>=1&&i*ch[j].b<=ch[j].c;j--)sumb+=ch[j].b,sumc-=ch[j].c;
		ans=min(ans,sum+sumb*i+sumc);
	}
	cout<<ans<<'\n';
	return 0;
}

[ABC332G] Not Too Many Balls

其实与上一个题非常像。我们发现中间的边只与没割掉的小球序号和有关,因为 \(n\le500\) 所以序号和 \(k\) 是可以接受直接记录下来的,用 dp 我们就可快速剩序列和为 \(k\) 的最小代价(处理掉最左边割的情况)。中间和右边割的情况是由 \(min(kj,b_j)\) 决定的,前一段是 \(kj\) 后一段 \(b_j\) ,用 \({b_i\over i}\) 算出分界点就可以枚举 \(k\) 快速算出答案。

有一种理解模拟最小割的方法。

\[\begin{aligned} &=&\min_{P\subseteq X}\min_{Q\subseteq Y}(\sum_{i\notin P}a_i+\sum_{i\notin Q}b_i+\sum_{i\in P,j\in Q}ij)\\ &=&\min_{P\subseteq X}\min_{Q\subseteq Y}(\sum_{i\notin P}a_i+\sum_{i\notin Q}b_i+\sum_{i\in P}i\sum_{j\in Q}j)\\ &=&\min_{k=0}^{{n(n+1)\over 2}}\min_{P\subseteq X\wedge\sum_{i\in k}i=k}\min_{Q\subseteq Y}(\sum_{i\notin P}a_i+\sum_{i\notin Q}b_i+\sum_{i\in P}i\sum_{j\in Q}j)\\ &=&\min_{k=0}^{{n(n+1)\over 2}}(\min_{P\subseteq X\wedge\sum_{i\in k}i=k}\sum_{i\notin P}a_i+\min_{Q\subseteq Y}(\sum_{i\notin Q}b_i+k\sum_{j\in Q}j))\\ \end{aligned} \]

#include <bits/stdc++.h>
using namespace std;
const int N=510,M=5e5+10;
#define int long long 
int n,m,a[N],b[M],dp[N*N],prek[N*N],preb[N*N];
signed main()
{
	freopen("railroad.in","r",stdin);
	freopen("railroad.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	memset(dp,0x3f,sizeof(dp));dp[n*(n+1)/2]=0;
	for(int i=1;i<=n;i++)
		for(int k=0;k+i<=n*(n+1)/2;k++)
		 	dp[k]=min(dp[k],dp[k+i]+a[i]);
	for(int i=1;i<=m;i++)
	{
		cin>>b[i];
		int z=b[i]/i;
		prek[1]+=i;//区间修改,差分成单点
		if(z+1<=n*(n+1)/2)prek[z+1]-=i,preb[z+1]+=b[i]; 
	}
	int ans=1e18;
	for(int k=0;k<=n*(n+1);k++)
	{
		prek[k]+=prek[k-1];preb[k]+=preb[k-1];
		ans=min(ans,dp[k]+k*prek[k]+preb[k]);
	}
	cout<<ans<<'\n';
	return 0;
}

CF1408H Rainbow Triples

只能说难度飙升。

首先有一个显然但是不好想到的性质如果有 \(c\)\(0\) 那么最多凑成 \({c\over 2}\) 组,那么如果一个位置左边有大于等于 \({c\over 2}\) 种,那么不论什么情况左边一定可以出一个 \(0\) 给它,同理右边,而且每个点要么被左边限制要么被右边限制,我们只管限制它的一边。又因为 \(P_{b_i}\) 各不相同,所以我们只要限制最松的两个即可。

有了上面的分析建图就简单起来了。 \(s\) 向每个不为 \(0\) 的值连流量 \(1\) ,不为 \(1\) 的值指向限制最松的 \(0\) 的位置流量为 \(1\) ,然后左边的 \(0\) 向前一个连流量为 \(inf\) 的边,右边的 \(0\) 向后面连流量为 \(inf\) 的边(前 \({c\over 2}\)\(0\) 属于左边,否则属于右边),然后 \(0\) 的位置向 \(t\) 连边流量为 \(1\) ,最大流即是答案。

模拟最大流,先转最小割,然后观察性质。

  1. 只会割 \(0\)\(t\) ,\(s\) 到不为 \(0\) 的值的边。
  2. \(0\)\(t\) 的边,一定左边割一段前缀,右边割一段后缀。

所以考虑 dp ,首先枚举左边割了多少,然后维护 \(f_i\) 表示右边割了 \(i\) 条边时的要割的值的数量和右边割的数量的和。用线段树即可快速转移。

#include <bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct node
{
	int mn,tag;
}sh[N<<2];
int col,cl[N],cr[N],a[N],n,pre[N],tot,ans;
vector<int> L,R;
void build(int x,int l,int r)
{
	sh[x].tag=0;
	if(l==r)
	{
		sh[x].mn=col+l;//割掉所有颜色和后缀的边数 
		return;
	}
	int mid=(l+r)>>1;
	build(x<<1,l,mid);build(x<<1|1,mid+1,r);
	sh[x].mn=min(sh[x<<1].mn,sh[x<<1|1].mn); 
}
void pushdown(int x)
{
	if(!sh[x].tag)return;
	sh[x<<1].tag+=sh[x].tag;sh[x<<1].mn+=sh[x].tag;
	sh[x<<1|1].tag+=sh[x].tag;sh[x<<1|1].mn+=sh[x].tag;
	sh[x].tag=0;
	return ; 
}
void modify(int x,int l,int r,int lt,int rt,int z)
{
	if(lt<=l&&rt>=r)
	{
		sh[x].tag+=z;
		sh[x].mn+=z;
		return;
	}
	pushdown(x);
	int mid=(l+r)>>1;
	if(lt<=mid)modify(x<<1,l,mid,lt,rt,z);
	if(rt>mid)modify(x<<1|1,mid+1,r,lt,rt,z);
	sh[x].mn=min(sh[x<<1].mn,sh[x<<1|1].mn);
	return ; 
}
void solve()
{
	cin>>n;
	for(int i=0;i<=n;i++)cl[i]=cr[i]=0; 
	col=0;L.clear();R.clear();
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		pre[i]=pre[i-1]+(a[i]==0);
	}
	int fj=pre[n]/2;tot=pre[n]-fj;
	for(int i=1;i<=n;i++)
	{
		if(a[i]==0)continue;//为0就不管
		if(pre[i]<=fj)L.push_back(i);//被左边限制 
		else R.push_back(i); 
	} 
	int lenl=L.size(),lenr=R.size();
	//cout<<lenl<<" "<<lenr<<'\n';
	for(int i=0;i<lenl;i++)cl[a[L[i]]]=L[i];
	for(int i=lenr-1;i>=0;i--)cr[a[R[i]]]=R[i];
	for(int i=1;i<=n;i++)if(cl[i]||cr[i])col++;//统计有多少种颜色出现
	//cout<<col<<" "<<fj<<'\n';
	build(1,0,tot);//范围?
	for(int i=1;i<=n;i++)if(!cl[i]&&cr[i])modify(1,0,tot,pre[n]-pre[cr[i]-1],tot,-1);//只在R中出现与L无关
	ans=min(sh[1].mn,fj); 
	//cout<<ans<<'\n';
	for(int i=1;i<=n;i++)
	{
		if(pre[i]>fj)break;
		if(cl[a[i]]^i)continue; //不是做贡献的位置
		if(cr[a[i]]) modify(1,0,tot,pre[n]-pre[cr[a[i]]-1],tot,-1);//在R中出现过 
		else  modify(1,0,tot,0,tot,-1);//只在L中出现过 
		ans=min(ans,sh[1].mn+pre[i]);
		//cout<<ans<<'\n';
	}
	cout<<ans<<'\n';
	return ;
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int t;cin>>t;
	while(t--)solve();
	return 0;
}
posted @ 2025-06-25 19:58  exCat  阅读(25)  评论(0)    收藏  举报