省选集训 4/6 - 图论与网络流

[QOJ14718] Meeting for Meals

\(n\) 个点 \(m\) 条边的带权无向图,\(k\) 个人要去往 \(1\),设 \(mx\) 为所有人到 \(1\) 的最小时间.

问对于每个 \(i\),在 \(mx\) 时间内所有人到 \(1\) 的前提下,第 \(i\) 个人最多能有多长时间是与其他人一起走的。

首先能想到和某个人碰面后肯定会一直一起走,所以将题目转化为最小相遇时间。

直接算每个人不太好做,我们考虑在每条边上相遇的贡献,并将点相遇看作特殊的边相遇。

对于一条边 \((u,v,w)\),假设要让 \(i\)\(j\) 分别到达 \(u\)\(v\) 并在这条边相遇:

则需要满足 \(\min(dis_{i,u}+w+dis_{v,1},dis_{j,v}+w+dis_{u,1})\leq mx\)

即相遇后分别去到 \(u\)\(v\) 的总时间的较小值要 \(\leq mx\),此时经过这条边相遇的最小时间为 \(\frac{dis_{i,u}+w+dis_{v,j}}{2}\)

所以当我们确定了 \(i\)\(u\) 之后,就只需要知道 \(dis_{j,v}\) 最小的 \(j(j\neq i)\) 就可以了。

我们将到达每个点的最近的人及距离求出来过后,就只需要更新每条边两端点的答案就行了。

这样做不会漏掉 \(i\) 对于非 \(j\) 点的贡献吗?是不会的,因为假设这个非 \(j\) 点为 \(k\)

我们知道 \(i\)\(k\) 都不是最小的 \(dis_{v,j}\)\(j\),那么 \(i\)\(k\) 经过这条边相遇是不优于 \(j\)\(k\)\(v\) 相遇的。

\(i\) 就不可能经过这条边对 \(k\) 贡献答案,所以就不需要考虑了,编码直接跑多源最短路即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
bool vis[N];
double ans[N];
vector<pair<int,int>> v[N];
int t,n,m,k,mx,x[N],y[N],w[N],d[N],dt1[N],fr[N];
priority_queue<tuple<int,int,int>,vector<tuple<int,int,int>>,greater<>> q;
void dijkstra(vector<int> s,int *d){
	fill(d,d+n+1,INF),fill(vis,vis+n+1,0);
	for(auto x:s)  q.emplace(d[x]=0,x,fr[x]=x);
	while(!q.empty()){
		auto [sum,num,fst]=q.top();q.pop();
		if(vis[num])  continue;vis[num]=1;
		for(auto [x,w]:v[num])  if(d[x]>sum+w)
			q.emplace(d[x]=sum+w,x,fst),fr[x]=fst;
	}
}
void solve(){
	cin>>n>>m>>k,mx=0,fill(ans,ans+n+1,INF);
	vector<int> a(k,0);
	for(int i=0;i<k;i++)  cin>>a[i];
	for(int i=0;i<k;i++)  fr[a[i]]=a[i];
	for(int i=1;i<=n;i++)  v[i].clear();
	for(int i=1;i<=m;i++){
		cin>>x[i]>>y[i]>>w[i];
		v[x[i]].emplace_back(y[i],w[i]);
		v[y[i]].emplace_back(x[i],w[i]);
	}
	dijkstra({1},dt1),dijkstra(a,d);
	for(int i=0;i<k;i++)  mx=max(mx,dt1[a[i]]);
	for(int i=1;i<=m;i++){
		if(fr[x[i]]==fr[y[i]])  continue;
		int tmp1=d[x[i]]+w[i]+dt1[y[i]];
		int tmp2=d[y[i]]+w[i]+dt1[x[i]];
		if(min(tmp1,tmp2)>mx)  continue;
		double tmp=(d[x[i]]+w[i]+d[y[i]])/2.0;
		ans[fr[x[i]]]=min(ans[fr[x[i]]],tmp);
		ans[fr[y[i]]]=min(ans[fr[y[i]]],tmp);
	}
	for(int i=0;i<k;i++)  cout<<mx-ans[a[i]]<<" ";
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cout<<fixed<<setprecision(1);
	cin>>t;while(t--)  solve(),cout<<"\n";
}

[QOJ4218] Hidden Graph

交互题,有一个 \(n\) 个点的图,满足任意导出子图都存在一个点度数不超过 \(k\)

每次你可以询问一个集合,返回其中任意一条边或返回无,求出这个图的所有边,询问次数不能超过 \(2nk+n\)

对于这种题,一般考虑一个点一个点的加入,现在假设前 \(i-1\) 个点我们已经求出来了。

由于任意导出子图都存在一个点度数不超过 \(k\),所以我们可以将图 \(k+1\) 染色。

染色具体方法是:每次找到一个未染色度数最大的点,随便找一个不相邻颜色染上就行了。

为什么这样是正确的呢?因为一定存在一个点度数不超过 \(k\)

所以我们可以先不管度数最小的点,先将其它点全部染色完,最后再染这个度数最小的点。

由于这个点度数一定不超过 \(k\),那就一定能在 \(k+1\) 种颜色中找到一个不相邻的。

对于去除掉这个点的图,我们可以发现这是个完全相同的子问题,所以一定存在 \(k+1\) 染色方案。

将上述证明过程倒过来,于是从度数最大的点开始染就是正确的了。

编码的时候为了降低染色部分的复杂度用的线段树找最小未染的颜色,然而实际没有必要,复杂度瓶颈在输出。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 2005
#define INF 0x3f3f3f3f
vector<int> v[N],c[N];
int n,ans,s[N],d[N],col[N],vis[N];
struct Segment_tree{
	int tr[N<<1],tag[N<<1];
	void clear(int p,int l){tag[p]=1,tr[p]=l;}
	void pushdown(int p,int l,int mid){
		if(!tag[p])  return;
		tag[p]=0,clear(mid<<1,l),clear(mid<<1|1,mid+1);
	}
	void update(int x,int l=1,int r=n+1,int p=1){
		if(l==r)  return tr[p]=INF,void();
		int mid=(l+r)>>1,ls=mid<<1,rs=mid<<1|1;
		pushdown(p,l,mid);
		x<=mid?update(x,l,mid,ls):update(x,mid+1,r,rs);
		tr[p]=min(tr[ls],tr[rs]);
	}
	void build(int l=1,int r=n+1,int p=1){
		if(l==r)  return tr[p]=l,void();
		int mid=(l+r)>>1,ls=mid<<1,rs=mid<<1|1;
		build(l,mid,ls),build(mid+1,r,rs),tr[p]=tr[ls];
	}
}SGT[N];
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++)  SGT[i].build();
	for(int i=2,mx=0;i<=n;i++){
		vector<int> s(i,0);iota(s.begin(),s.end(),0);
		sort(s.begin()+1,s.end(),
			[&](int x,int y){return d[x]>d[y];});
		for(int j=1;j<=mx;j++)  c[j].clear();mx=0;
		for(int j=1;j<i;j++)  SGT[j].clear(1,1),vis[j]=0;
		for(int j=1;j<i;j++){
			int col=SGT[s[j]].tr[1];
			c[col].push_back(s[j]),mx=max(mx,col);
			for(auto x:v[s[j]])  SGT[x].update(col);
		}
		for(int j=1,cnt=0;j<=mx;j++,cnt=0){
			while(cnt<c[j].size()){
				cout<<"? "<<c[j].size()-cnt+1<<" ";
				for(auto x:c[j])  if(!vis[x])  cout<<x<<" ";
				cout<<i<<"\n",cout.flush();
				int x,y;cin>>x>>y,cnt++;
				if(x==-1&&y==-1)  break;
				v[x].push_back(y),v[y].push_back(x);
				d[x]++,d[y]++,ans++,vis[min(x,y)]=1;
			}
		}
	}
	cout<<"! "<<ans<<"\n",cout.flush();
	for(int i=1;i<=n;i++)  for(auto x:v[i])
		if(i<x)  cout<<i<<" "<<x<<"\n",cout.flush();
}

[CF1033E] Hidden Bipartite Graph

由于题目保证给出的图是连通图,我们可以考虑先找出图的一棵生成树。

这样做的好处是,我们可以通过深度划分便捷地得出二分图的左右部点。

同时因为是一棵生成树,找环会变得非常容易,接下来考虑怎么找。

首先有我们可以通过两次操作得到一个点是否与一个集合有连边。

只需要分别查询集合和点加集合看边数是否相同就可以了。

于是我们可以对于已经加进生成树的点,去找不在树内的点的连边。

方法也很简单,每次二分当前集合内是否有连边就可以了,查询次数 \(2n\log n\)

找到生成树后,分别查询奇深度集合和偶深度集合内部是否有连边。

如果没有就是二分图,有就在集合内二分找边,然后这个题就做完了。

实测按我的写法在 CF 数据下的最大查询次数为 12963 次,仍有较大容错空间。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 605
stack<int> st;
int n,num,dp[N],vis[N];vector<int> v,op[3],G[N];
bool ishave(int x,vector<int> &v){
	cout<<"? "<<v.size()+1<<" "<<x<<" ";
	for(auto y:v)  cout<<y<<" ";
	int num1,num2;
	cout<<"\n",cout.flush(),cin>>num1;
	cout<<"? "<<v.size()<<" ";
	for(auto y:v)  cout<<y<<" ";
	cout<<"\n",cout.flush(),cin>>num2;
	return num1!=num2;
}
int get(int x,vector<int> &v,bool f){
	if(!f&&!ishave(x,v))  return -1;
	if(v.size()==1)  return v[0];
	int mid=(v.size()+1)>>1;
	vector<int> down(0,0);
	for(int i=0;i<mid;i++)  down.push_back(v[i]);
	if(ishave(x,down))  return get(x,down,1);
	down.clear();
	for(int i=mid;i<v.size();i++)  down.push_back(v[i]);
	return get(x,down,1);
}
void dfs(int x,int f){
	dp[x]=f,op[f].push_back(x);
	for(auto y:G[x])  if(!dp[y])  dfs(y,3-f);
}
void findpath(int x,int fa,int to){
	st.push(x);
	if(x==to){
		cout<<"N "<<st.size()<<"\n";
		while(!st.empty())
			cout<<st.top()<<" ",st.pop();
		cout<<"\n",cout.flush(),exit(0);
	}
	for(auto y:G[x])  if(y!=fa)  findpath(y,x,to);
	return st.pop(),void();
}
void findcircle(vector<int> &v){
	for(auto x:v){
		vector<int> chk(0,0);
		for(auto y:v)  if(x!=y)  chk.push_back(y);
		int tmp=get(x,chk,0);
		if(tmp!=-1)  return findpath(x,0,tmp);
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n,v.push_back(1),vis[1]=1;
	if(n==1)  return cout<<"Y 1\n1\n",0;
	for(int i=0;v.size()<n;i++){
		while(v.size()<n){
			vector<int> chk(0,0);
			for(int j=1;j<=n;j++)
				if(!vis[j])  chk.push_back(j);
			int tmp=get(v[i],chk,0);
			if(tmp==-1)  break;
			G[tmp].push_back(v[i]),vis[tmp]=1;
			G[v[i]].push_back(tmp),v.push_back(tmp);
		}
	}
	dfs(1,1),cout<<"? "<<op[1].size()<<" ";
	for(auto x:op[1])  cout<<x<<" ";
	cout<<"\n",cout.flush(),cin>>num;
	if(num)  return findcircle(op[1]),0;
	cout<<"? "<<op[2].size()<<" ";
	for(auto x:op[2])  cout<<x<<" ";
	cout<<"\n",cout.flush(),cin>>num;
	if(num)  return findcircle(op[2]),0;
	cout<<"Y "<<op[1].size()<<"\n";
	for(auto x:op[1])  cout<<x<<" ";
	return cout<<"\n",cout.flush(),0;
}

[CF555E] Case of Computer Network

先将原图边双缩点,然后发现如果 \(s\)\(t\) 在同一点双内就一定可以到达。

如果不在同一点双内就是将 \(s\rightarrow t\) 的所有边定向。

我们用树上差分维护定向的信息,最后判断是否存在边被双向定向即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 200005
stack<int> st;
vector<int> vc[N];
vector<pair<int,int>> v[N];
int n,m,q,cnt,dfn,d[N],dep[N],bin[N],dfl[N],low[N],def1[N],def2[N],fa[N][18];
void dfs1(int x,int num,int rt){
    d[x]=rt,low[x]=dfl[x]=++dfn,st.push(x);
    for(auto [y,i]:v[x]){
        if(!dfl[y])  dfs1(y,i,rt),low[x]=min(low[x],low[y]);
        else if(i!=num)  low[x]=min(low[x],dfl[y]);
    }
    if(low[x]==dfl[x]){
        cnt++;
        while(1){int y=st.top();st.pop(),bin[y]=cnt;if(x==y)  break;}
    }
}
void dfs2(int x){
    for(int i=1;i<18;i++)  fa[x][i]=fa[fa[x][i-1]][i-1];
    for(auto y:vc[x])  if(y!=fa[x][0])  dep[y]=dep[x]+1,fa[y][0]=x,dfs2(y);
}
int LCA(int x,int y){
    if(dep[x]>dep[y])  swap(x,y);
    for(int i=17;i>=0;i--)  if(dep[fa[y][i]]>=dep[x])  y=fa[y][i];
    if(x==y)  return x;
    for(int i=17;i>=0;i--)  if(fa[x][i]!=fa[y][i])  x=fa[x][i],y=fa[y][i];
    return fa[x][0];
}
void dfs3(int x){
    for(auto y:vc[x])  if(y!=fa[x][0])  dfs3(y),def1[x]+=def1[y],def2[x]+=def2[y];
    if(def1[x]&&def2[x])  printf("No\n"),exit(0);
}
int main(){
	scanf("%d%d%d",&n,&m,&q);
    for(int i=1,x,y;i<=m;i++){
        scanf("%d%d",&x,&y);
        v[x].emplace_back(y,i),v[y].emplace_back(x,i);
    }
    for(int i=1;i<=n;i++)  if(!dfl[i])  dfs1(i,0,i);
    for(int i=1;i<=n;i++)
        for(auto [x,j]:v[i])
            if(bin[i]!=bin[x])  vc[bin[i]].push_back(bin[x]);
    for(int i=1;i<=cnt;i++)  if(!dep[i])  dep[i]=1,dfs2(i);
    for(int i=1,x,y;i<=q;i++){
        scanf("%d%d",&x,&y);
        if(d[x]!=d[y])  return printf("No\n"),0;
        x=bin[x],y=bin[y];if(x==y)  continue;
        int lca=LCA(x,y);def1[x]++,def2[y]++,def1[lca]--,def2[lca]--;
    }
    for(int i=1;i<=cnt;i++)  if(!fa[i][0])  dfs3(i);printf("Yes\n");
}

[SWTR-8] 地地铁铁

先考虑单个点双内,显然只能经过点双内的点,若颜色均相同则显然不计入答案。

否则最多只有一对点不计入答案:只有两个点连着两种不同的边时的这两个点。

对于不在同一点双内的两个点,只有中间有异色点双即可计入答案,建圆方树并查集即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 800005
char ch[N];
long long ans;
stack<int> st;
vector<int> vc[N];
bool fl[N][2],all[2];
vector<pair<int,int>> v[N];
stack<tuple<int,int,int>> stk;
int calc(bool fl0,bool fl1){return !fl1?0:(!fl0?1:2);}
int s,n,m,dfn,cnt,dfl[N],low[N],sum[N],col[N],sz[N],d[N];
int find(int x){return d[x]==x?x:d[x]=find(d[x]);}
void merge(int x,int y){x=find(x),y=find(y);if(x!=y)  sz[x]+=sz[y],d[y]=x;}
void dfs(int x,int fa){
	st.push(x),dfl[x]=low[x]=++dfn;
	for(auto [y,w]:v[x]){
		if(!dfl[y]){
			stk.emplace(x,y,w),dfs(y,x),low[x]=min(low[x],low[y]);
			if(low[y]>=dfl[x]){
				sum[++cnt]=1,fl[x][0]=fl[x][1]=all[0]=all[1]=0;
				while(1){
					int z=st.top();st.pop(),fl[z][0]=fl[z][1]=0;
					sum[cnt]++,vc[cnt].emplace_back(z);
					if(y==z)  break;
				}
				while(1){
					auto [u,v,w]=stk.top();stk.pop(),fl[u][w]=fl[v][w]=all[w]=1;
					if(u==x&&v==y)  break;
				}
				col[cnt]=calc(all[0],all[1]),vc[cnt].emplace_back(x),sz[cnt]=sum[cnt];
				if(col[cnt]==2){
					int tmp=0;
					for(auto z:vc[cnt])  tmp+=(fl[z][0]&&fl[z][1]);
					if(tmp==2)  ans+=2;
				}
			}
		}
		else{
			low[x]=min(low[x],dfl[y]);
			if(dfl[x]>dfl[y])  stk.emplace(x,y,w);
		}
	}
}
int main(){
	scanf("%d%d%d",&s,&n,&m),iota(d,d+n+1,0),fill(sz,sz+n+1,1),cnt=n;
	for(int i=1,x,y;i<=m;i++){
		scanf("%d%d%s",&x,&y,ch);
		v[x].emplace_back(y,ch[0]=='D'),v[y].emplace_back(x,ch[0]=='D');
	}
	dfs(1,0);
	for(int i=n+1;i<=cnt;i++){
		if(col[i]!=0)  continue;
		for(int j=1;j<vc[i].size();j++)  merge(vc[i][j],vc[i][j-1]);
	}
	for(int i=1;i<=n;i++)  if(find(i)==i)  ans+=sz[i]*(sz[i]-1ll);
	iota(d,d+n+1,0),fill(sz,sz+n+1,1);
	for(int i=n+1;i<=cnt;i++){
		if(col[i]!=1)  continue;
		for(int j=1;j<vc[i].size();j++)  merge(vc[i][j],vc[i][j-1]);
	}
	for(int i=1;i<=n;i++)  if(find(i)==i)  ans+=sz[i]*(sz[i]-1ll);
	printf("%lld\n",(n*(n-1ll)-ans)/2);
}

[USACO07OPEN] Dining G

将奶牛拆成入点和出点建流量 1 的边,前面由食物连边,后面向饮料连边即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 405
#define M 105*105*5
int n,m,k,s,t;
struct Max_flow{
	queue<int> q;
	vector<int> G[N];
	int cnt=0,v[M],w[M],dep[N],cur[N];
	bool bfs(){
		while(!q.empty())  q.pop();
		q.push(s),memset(dep,0,sizeof dep),dep[s]=1;
		while(!q.empty()){
			int x=q.front();q.pop();
			if(x==t) return 1;
			for(auto e:G[x])
				if(!dep[v[e]]&&w[e])
					dep[v[e]]=dep[x]+1,q.push(v[e]);
		}
		return 0;
	}
	int dfs(int x,int flow,int res=0){
		if(x==t)  return flow;
		for(int i=cur[x];i<G[x].size()&&flow;i++){
			cur[x]=i;int e=G[x][i];
			if(dep[v[e]]==dep[x]+1&&w[e]){
				int f=dfs(v[e],min(flow,w[e]));
				w[e]-=f,w[e^1]+=f,flow-=f,res+=f;
			}
		}
		if(res==0)  dep[x]=0;
		return res;
	}
	void clear(){for(int i=0;i<N;i++)  G[i].clear();cnt=0;}
	void add(int x,int y,int z){v[cnt]=y,w[cnt]=z,G[x].push_back(cnt++);}
	int work(int x,int y,int z){add(x,y,z),add(y,x,0);return cnt-2;}
	int max_flow(int ans=0){while(bfs())  memset(cur,0,sizeof cur),ans+=dfs(s,INT_MAX);return ans;}
}Dinic;
int main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m>>k,s=0,t=N-1;
	for(int i=1;i<=m;i++)  Dinic.work(s,i+n+n,1);
	for(int i=1;i<=k;i++)  Dinic.work(i+n+n+m,t,1);
	for(int i=1,u,v;i<=n;i++){
		cin>>u>>v,Dinic.work(i,i+n,1);
		for(int j=1,x;j<=u;j++)  cin>>x,Dinic.work(x+n+n,i,1);
		for(int j=1,x;j<=v;j++)  cin>>x,Dinic.work(i+n,x+n+n+m,1);
	}
	cout<<Dinic.max_flow()<<"\n";
}

最长不下降子序列问题

问题一就是最基础的最长不下降子序列问题,用 \(O(n^2)\)\(dp\) 解决即可。

问题二开始拆点,在不降的两个下标间连边即可,注意起点只向 \(dp_i=1\) 连边,也只有 \(dp_i=s\) 向终点连边。

问题三在问题二基础上将 \(1\)\(n\) 的流量改为 \(INF\) 即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1005
#define M 505*505*3
#define INF 0x3f3f3f3f
int n,s,t,ans,flag,a[N],dp[N];
struct Max_flow{
	queue<int> q;
	vector<int> G[N];
	int cnt=0,ans=0,v[M],w[M],dep[N],cur[N];
	bool bfs(){
		while(!q.empty())  q.pop();
		q.push(s),memset(dep,0,sizeof dep),dep[s]=1;
		while(!q.empty()){
			int x=q.front();q.pop();
			if(x==t) return 1;
			for(auto e:G[x])
				if(!dep[v[e]]&&w[e])
					dep[v[e]]=dep[x]+1,q.push(v[e]);
		}
		return 0;
	}
	int dfs(int x,int flow,int res=0){
		if(x==t)  return flow;
		for(int i=cur[x];i<G[x].size()&&flow;i++){
			cur[x]=i;int e=G[x][i];
			if(dep[v[e]]==dep[x]+1&&w[e]){
				int f=dfs(v[e],min(flow,w[e]));
				w[e]-=f,w[e^1]+=f,flow-=f,res+=f;
			}
		}
		if(res==0)  dep[x]=0;
		return res;
	}
	void clear(){for(int i=0;i<N;i++)  G[i].clear();cnt=ans=0;}
	void add(int x,int y,int z){v[cnt]=y,w[cnt]=z,G[x].push_back(cnt++);}
	int work(int x,int y,int z){add(x,y,z),add(y,x,0);return cnt-2;}
	int max_flow(){while(bfs())  memset(cur,0,sizeof cur),ans+=dfs(s,INT_MAX);return ans;}
}Dinic;
int main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n,s=0,t=N-1;
	for(int i=1;i<=n;i++)  cin>>a[i];
	for(int i=1;i<=n;i++)
		for(int j=i;j>0;j--)
			if(a[i]>=a[j])
				ans=max(ans,dp[i]=max(dp[i],dp[j]+1));
	for(int i=1;i<=n;i++){
		Dinic.work(i,i+n,1);
		if(dp[i]==1)  Dinic.work(s,i,1);
		if(dp[i]==ans)  Dinic.work(i+n,t,1);
		for(int j=1;j<i;j++)
			if(dp[j]+1==dp[i]&&a[i]>=a[j])  Dinic.work(j+n,i,1);
	}
	flag=(dp[n]==ans),cout<<ans<<"\n";
	cout<<Dinic.max_flow()<<"\n",Dinic.work(n,n+n,0x3f3f3f3f);
	Dinic.work(s,1,0x3f3f3f3f),Dinic.work(1,n+1,0x3f3f3f3f);
	if(flag&&n>=2)  Dinic.work(n+n,t,0x3f3f3f3f);
	cout<<Dinic.max_flow()<<"\n";
}

[USACO05NOV] Asteroids G

在第 \(i\) 行第 \(j\) 列有颗小行星相当于第 \(i\) 行和第 \(j\) 列至少要有一个要使用武器。

转换为了最小割问题,起点向每行连 \(1\) 边,每列向终点连 \(1\) 边,第 \(i\) 行向第 \(j\) 列连 \(INF\) 边即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1005
#define M 505*505*3
#define INF 0x3f3f3f3f
int n,m,s,t;
struct Max_flow{
	queue<int> q;
	vector<int> G[N];
	int cnt=0,ans=0,v[M],w[M],dep[N],cur[N];
	bool bfs(){
		while(!q.empty())  q.pop();
		q.push(s),memset(dep,0,sizeof dep),dep[s]=1;
		while(!q.empty()){
			int x=q.front();q.pop();
			if(x==t) return 1;
			for(auto e:G[x])
				if(!dep[v[e]]&&w[e])
					dep[v[e]]=dep[x]+1,q.push(v[e]);
		}
		return 0;
	}
	int dfs(int x,int flow,int res=0){
		if(x==t)  return flow;
		for(int i=cur[x];i<G[x].size()&&flow;i++){
			cur[x]=i;int e=G[x][i];
			if(dep[v[e]]==dep[x]+1&&w[e]){
				int f=dfs(v[e],min(flow,w[e]));
				w[e]-=f,w[e^1]+=f,flow-=f,res+=f;
			}
		}
		if(res==0)  dep[x]=0;
		return res;
	}
	void clear(){for(int i=0;i<N;i++)  G[i].clear();cnt=ans=0;}
	void add(int x,int y,int z){v[cnt]=y,w[cnt]=z,G[x].push_back(cnt++);}
	int work(int x,int y,int z){add(x,y,z),add(y,x,0);return cnt-2;}
	int max_flow(){while(bfs())  memset(cur,0,sizeof cur),ans+=dfs(s,INT_MAX);return ans;}
}Dinic;
int main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m,s=0,t=N-1;
	for(int i=1;i<=n;i++)  Dinic.work(s,i,1);
	for(int i=1;i<=n;i++)  Dinic.work(i+n,t,1);
	for(int i=1,x,y;i<=m;i++)  cin>>x>>y,Dinic.work(x,y+n,INF);
	cout<<Dinic.max_flow()<<"\n";
}

[SP4063] MPIGS - Sell Pigs

每次调整只需要将所有猪圈向一个超级点连 \(INF\) 边,再给每个猪圈开个新点连回去就行。

统计答案只需要在每个人对应的超级点向终点连 \(b\) 的边就可以了。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define M 2005*2005*3
#define INF 0x3f3f3f3f
int n,m,s,t,cnt,d[N];
struct Max_flow{
	queue<int> q;
	vector<int> G[N];
	int cnt=0,ans=0,v[M],w[M],dep[N],cur[N];
	bool bfs(){
		while(!q.empty())  q.pop();
		q.push(s),memset(dep,0,sizeof dep),dep[s]=1;
		while(!q.empty()){
			int x=q.front();q.pop();
			if(x==t) return 1;
			for(auto e:G[x])
				if(!dep[v[e]]&&w[e])
					dep[v[e]]=dep[x]+1,q.push(v[e]);
		}
		return 0;
	}
	int dfs(int x,int flow,int res=0){
		if(x==t)  return flow;
		for(int i=cur[x];i<G[x].size()&&flow;i++){
			cur[x]=i;int e=G[x][i];
			if(dep[v[e]]==dep[x]+1&&w[e]){
				int f=dfs(v[e],min(flow,w[e]));
				w[e]-=f,w[e^1]+=f,flow-=f,res+=f;
			}
		}
		if(res==0)  dep[x]=0;
		return res;
	}
	void clear(){for(int i=0;i<N;i++)  G[i].clear();cnt=ans=0;}
	void add(int x,int y,int z){v[cnt]=y,w[cnt]=z,G[x].push_back(cnt++);}
	int work(int x,int y,int z){add(x,y,z),add(y,x,0);return cnt-2;}
	int max_flow(){while(bfs())  memset(cur,0,sizeof cur),ans+=dfs(s,INT_MAX);return ans;}
}Dinic;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m,s=0,t=N-1,iota(d,d+n+1,0),cnt=n;
	for(int i=1,x;i<=n;i++)  cin>>x,Dinic.work(s,i,x);
	for(int i=1,a,b,now;i<=m;i++){
		cin>>a,now=++cnt;
		for(int j=1,x;j<=a;j++){
			cin>>x;
			Dinic.work(d[x],now,INF);
			Dinic.work(now,d[x]=++cnt,INF);
		}
		cin>>b,Dinic.work(now,t,b);
	}
	cout<<Dinic.max_flow()<<"\n";
}

[ARC161F] Everywhere is Sparser than Whole (Judge)

考虑将导出子图换一个描述:若一条边在子图内,则它的两个端点一定在子图内,将边向点连边从而转化为闭合子图问题。

\(\frac{E}{V}\geq D\) 写作 \(E-VD\geq 0\),转化为闭合子图问题,当最大权 \(>0\) 时一定不合法。

当最大权为 \(0\) 时图不一定不合法,因为存在最大流满流即选全图的情况权也为 \(0\)

但这时由于最大流满流,每个点恰好会受到来自 \(D\) 条边的流量,我们可以据此给边定向。

这样定向后,存在密度等于 \(D\) 的真导出子图当且仅当新定向的图不止一个强连通分量。

考虑证明:如果存在多个强连通分量,那没有出边的强连通分量的所有边一定指向内部,密度恰好为 \(D\),得到充分性。

如果存在一个真导出子图密度等于 \(D\),这个点集无论怎么定向都会至少形成一个没有出边的强连通分量,得到必要性。

至此我们将这个题分成了两部分解决,编码时先跑 Dinic 再跑 Tarjan 即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 100005
#define M 50005*10
#define INF 0x3f3f3f3f
stack<int> st;
vector<int> v[N];
int cnt,dfn,dfl[N],low[N],scc[N];
int T,n,d,s,t,x[N],y[N],nx[N],ny[N];
struct Max_flow{
	queue<int> q;
	vector<int> G[N];
	int cnt=0,ans=0,v[M],w[M],dep[N],cur[N];
	bool bfs(){
		while(!q.empty())  q.pop();
		q.push(s),memset(dep,0,sizeof dep),dep[s]=1;
		while(!q.empty()){
			int x=q.front();q.pop();
			if(x==t) return 1;
			for(auto e:G[x])
				if(!dep[v[e]]&&w[e])
					dep[v[e]]=dep[x]+1,q.push(v[e]);
		}
		return 0;
	}
	int dfs(int x,int flow,int res=0){
		if(x==t)  return flow;
		for(int i=cur[x];i<G[x].size()&&flow;i++){
			cur[x]=i;int e=G[x][i];
			if(dep[v[e]]==dep[x]+1&&w[e]){
				int f=dfs(v[e],min(flow,w[e]));
				w[e]-=f,w[e^1]+=f,flow-=f,res+=f;
			}
		}
		if(res==0)  dep[x]=0;
		return res;
	}
	void clear(int n){for(int i=0;i<=n;i++)  G[i].clear();cnt=ans=0;}
	void add(int x,int y,int z){v[cnt]=y,w[cnt]=z,G[x].push_back(cnt++);}
	int work(int x,int y,int z){add(x,y,z),add(y,x,0);return cnt-2;}
	int max_flow(){while(bfs())  memset(cur,0,sizeof cur),ans+=dfs(s,INT_MAX);return ans;}
}Dinic;
void dfs(int x){
	st.push(x),low[x]=dfl[x]=++dfn;
	for(auto y:v[x]){
		if(!dfl[y])  dfs(y),low[x]=min(low[x],low[y]);
		else if(!scc[y])  low[x]=min(low[x],dfl[y]);
	}
	if(dfl[x]==low[x]){
		cnt++;int y=0;
		while(y!=x)  y=st.top(),st.pop(),scc[y]=cnt;
	}
}
void solve(){
	cin>>n>>d,s=0,t=n*(d+1)+1,cnt=dfn=0;
	for(int i=1;i<=n;i++)  dfl[i]=scc[i]=0,v[i].clear();
	for(int i=1;i<=n*d;i++){
		cin>>x[i]>>y[i],Dinic.work(s,i,1);
		nx[i]=Dinic.work(i,x[i]+n*d,INF);
		ny[i]=Dinic.work(i,y[i]+n*d,INF);
	}
	for(int i=1;i<=n;i++)  Dinic.work(n*d+i,t,d);
	Dinic.max_flow();
	for(int i=1;i<=n*d;i++){
		if(Dinic.w[nx[i]^1])  v[x[i]].push_back(y[i]);
		if(Dinic.w[ny[i]^1])  v[y[i]].push_back(x[i]);
	}
	for(int i=1;i<=n;i++)  if(!dfl[i])  dfs(i);
	Dinic.clear(n*(d+1)+1);
	return cout<<(cnt==1?"Yes":"No")<<"\n",void();
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>T;while(T--)  solve();
}

[AGC038F] Two Permutations

\(i\)\(p_i\) 连边,所以对于 \(a\) 序列而言,建出来的每个环上所有点要么都选自己要么都选 \(p_i\)

我们将环上所有点都选自己定义为不操作这个环,都选 \(p_i\) 定义为操作这个环,另一个序列同理。

\(p_i\)\(q_i\) 分为 \(5\) 种情况讨论:

\(p_i=q_i=i\),此时无论如何 \(a_i\) 都会等于 \(b_i\)

\(p_i=i\)\(q_i\neq i\),此时若不操作 \(q_i\) 所在环则 \(a_i\) 等于 \(b_i\)

\(p_i\neq i\)\(q_i=i\),此时若不操作 \(p_i\) 所在环则 \(a_i\) 等于 \(b_i\)

\(p_i\neq i\)\(q_i\neq i\)\(p_i \neq q_i\),此时若 \(p_i\)\(q_i\) 所在环均不操作则 \(a_i\) 等于 \(b_i\)

\(p_i\neq i\)\(q_i\neq i\)\(p_i=q_i\),此时若 \(p_i\)\(q_i\) 所在环均不操作或均操作则 \(a_i\) 等于 \(b_i\)

当发生 \(a_i=b_i\) 时会产生 \(1\) 的代价,考虑转化为最小割。

\(p\) 序列的环与汇点相连为选,与源点相连为不选,\(q\) 序列的环与源点相连为选,与汇点相连为不选。

对于第一种情况直接减 \(1\) 的代价。

对于第二种情况从源点到 \(q\) 环连 \(1\) 边,表示如果不选就要产生 \(1\) 的代价,选则无此代价。

对于第三种情况从 \(p\) 环到汇点连 \(1\) 边,表示如果不选就要产生 \(1\) 的代价,选则无此代价。

对于第四种情况从 \(p\) 环到 \(q\) 环连 \(1\) 边,表示都不选就要产生 \(1\) 的代价,有选则无此代价。

对于第五种情况在 \(p\) 环和 \(q\) 环连双向边,\(p\rightarrow q\) 表示都不选就要产生 \(1\) 的代价,\(q\rightarrow p\) 表示都选就要产生 \(1\) 的代价。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=200005,M=400005;
int n,s,t,cnt,ans,p[N],q[N],blp[N],blq[N];
struct Max_flow{
	queue<int> q;
	vector<int> G[N];
	int cnt=0,ans=0,v[M],w[M],dep[N],cur[N];
	bool bfs(){
		while(!q.empty())  q.pop();
		q.push(s),memset(dep,0,sizeof dep),dep[s]=1;
		while(!q.empty()){
			int x=q.front();q.pop();
			if(x==t) return 1;
			for(auto e:G[x])
				if(!dep[v[e]]&&w[e])
					dep[v[e]]=dep[x]+1,q.push(v[e]);
		}
		return 0;
	}
	int dfs(int x,int flow,int res=0){
		if(x==t)  return flow;
		for(int i=cur[x];i<G[x].size()&&flow;i++){
			cur[x]=i;int e=G[x][i];
			if(dep[v[e]]==dep[x]+1&&w[e]){
				int f=dfs(v[e],min(flow,w[e]));
				w[e]-=f,w[e^1]+=f,flow-=f,res+=f;
			}
		}
		if(res==0)  dep[x]=0;
		return res;
	}
	void clear(){for(int i=0;i<N;i++)  G[i].clear();cnt=ans=0;}
	void add(int x,int y,int z){v[cnt]=y,w[cnt]=z,G[x].push_back(cnt++);}
	int work(int x,int y,int z){add(x,y,z),add(y,x,0);return cnt-2;}
	int max_flow(){while(bfs())  memset(cur,0,sizeof cur),ans+=dfs(s,INT_MAX);return ans;}
}Dinic;
int main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n,s=0,t=N-1;
	for(int i=1;i<=n;i++)  cin>>p[i],p[i]++;
	for(int i=1;i<=n;i++)  cin>>q[i],q[i]++;
	for(int i=1,j;i<=n;i++){
		if(blp[i])  continue;
		for(j=i,cnt++;!blp[j];)  blp[j]=cnt,j=p[j];
	}
	for(int i=1,j;i<=n;i++){
		if(blq[i])  continue;
		for(j=i,cnt++;!blq[j];)  blq[j]=cnt,j=q[j];
	}
	for(int i=1;i<=n;i++){
		if(p[i]==i&&q[i]==i)  ans++;
		else if(p[i]==i)  Dinic.work(s,blq[i],1);
		else if(q[i]==i)  Dinic.work(blp[i],t,1);
		else if(p[i]!=q[i])  Dinic.work(blp[i],blq[i],1);
		else  Dinic.work(blp[i],blq[i],1),Dinic.work(blq[i],blp[i],1);
	}
	cout<<n-ans-Dinic.max_flow()<<"\n";
}
posted @ 2025-12-29 22:21  tkdqmx  阅读(68)  评论(4)    收藏  举报