「图论 2022/02 」学习记录

[SDOI2019]世界地图

给一个网格图,相邻的点有边,边权随机。

左右联通上下不联通,多次询问,删掉 \([l, r]\) 列的点,剩余点最小生成树,询问独立。

其中 \(n \le 100,m \le 10^4,q \le 10^4\)

每次询问相当于合并一个最小生成树前缀和后缀,预处理前缀和后缀的过程也是合并的过程。考虑优秀的合并。

实际上合并只要合并最左和最右两列。而每次合并的过程都是删除一些边,加入一些边保证是一棵树。

于是可以发现,加入的边一定是这两列之间的边,删除的边也是由这两列的点连接的边。

于是考虑对这些点建出虚树,边权为这两点在原来最小生成树边权的最大值,并且记录原来 MST 的值。

因为虚数的点是 \(\mathcal{O}(n)\) 的,考虑对这虚树暴力跑 kruskal 合并。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const ll N=100+5,M=1e4+5;
struct Edge{
	ll v,w,nx;
}e2[M];
struct edge{
	ll u,v,w;
	friend bool operator < (edge x,edge y){
		return x.w<y.w;
	}
};
ll n,m,q,lim,ne,ans,f[M],fa[M],use[M],row[M][N],col[M][N];
vector<edge> e;
unsigned int SA,SB,SC;
struct MST{
	ll tot,sum;
	vector<edge>e;
	MST(){}
	MST(ll *x)
	{	tot=n,sum=0;
		for(ll i=1;i<n;i++)e.push_back((edge){i,i+1,x[i]});
	}
	ll query()
	{	ll res=sum;
		for(ll i=0;i<e.size();i++)res+=e[i].w;
		return res;
	}
}pre[M],suf[M];
ll getw()
{	SA^=SA<<16,SA^=SA>>5,SA^=SA<<1;
	unsigned int t=SA;
	SA=SB,SB=SC,SC^=t^SA;
	return SC%lim+1;
}
ll find(ll x)
{	if(fa[x]==x)return x;
	return fa[x]=find(fa[x]);
}
void read(ll u,ll v,ll w)
{	e2[++ne].v=v;
	e2[ne].nx=f[u];
	e2[ne].w=w;
	f[u]=ne;
//	printf("%lld %lld %lld\n",u,v,w);
}
bool dfs1(ll u,ll ffa)
{	ll sum=0;//printf("fff%lld %lld\n",u,ffa);
	for(ll i=f[u];i;i=e2[i].nx)
	{	ll v=e2[i].v;
		if(v==ffa)continue;
		sum+=dfs1(v,u);
	}
	if(sum>=2)use[u]=1;
	sum+=use[u];
	return sum;
}
void dfs2(ll u,ll ffa,ll lst,ll val)
{	if(use[u])
	{	if(lst)e.push_back((edge){use[u],lst,val});
		lst=use[u],ans-=val;val=0;
	}
	for(ll i=f[u];i;i=e2[i].nx)
	{	ll v=e2[i].v;
		if(v==ffa)continue;
		dfs2(v,u,lst,max(val,e2[i].w));
	}
}
MST merge(MST x,MST y,ll *z)
{	ll tot=x.tot+y.tot;ans=0,ne=0;
	e.clear();MST res;//printf("%lld %lld %lld\n",tot,x.tot,y.tot);
	for(ll i=0;i<x.e.size();i++)e.push_back(x.e[i]);
	for(ll i=0;i<y.e.size();i++)e.push_back((edge){y.e[i].u+x.tot,y.e[i].v+x.tot,y.e[i].w});
	for(ll i=1;i<=n;i++)e.push_back((edge){x.tot-n+i,x.tot+i,z[i]});
	sort(e.begin(),e.end());
	for(ll i=1;i<=tot;i++)
	{	if(i<=n||i>tot-n)use[i]=1;
		else use[i]=0;
		f[i]=0,fa[i]=i;
	}
	for(ll i=0;i<e.size();i++)
		if(find(e[i].u)!=find(e[i].v))
			ans+=e[i].w,read(e[i].u,e[i].v,e[i].w),read(e[i].v,e[i].u,e[i].w),fa[find(e[i].u)]=find(e[i].v);
//	printf("ne:%d\n",ne);
	dfs1(1,0);
	ne=0;
	for(ll i=1;i<=tot;i++)
		if(use[i])use[i]=++ne;
//	printf("ne:%d\n",ne);
	e.clear();
	dfs2(1,0,0,0);
	res.tot=ne;res.sum=x.sum+y.sum+ans;
	res.e=e;
	return res;
}
int main()
{	//freopen("worldmap2.in","r",stdin);
	//freopen("ans.out","w",stdout);
	scanf("%lld%lld%u%u%u%lld",&n,&m,&SA,&SB,&SC,&lim);
	for(ll i=1;i<=n;i++)
		for(ll j=1;j<=m;j++)
			row[j][i]=getw();
	for(ll i=1;i<n;i++)
		for(ll j=1;j<=m;j++)
			col[j][i]=getw();
	pre[1]=MST(col[1]),suf[m]=MST(col[m]);
	for(ll i=2;i<m;i++)pre[i]=merge(pre[i-1],MST(col[i]),row[i-1]);
	for(ll i=m-1;i>1;i--)suf[i]=merge(MST(col[i]),suf[i+1],row[i]);
	scanf("%lld",&q);
	while(q--)
	{	ll l,r;
		scanf("%lld%lld",&l,&r);
		MST res=merge(suf[r+1],pre[l-1],row[m]);
		printf("%lld\n",res.query());
	}
	return 0;
}

[ARC069D] Flags

Snuke 将 \(N\) 个标志放在一条线上。

\(i\) 个标志可以放置在坐标 \(x_i\) 或坐标 \(y_i\) 上。

Snuke 认为当他们中的两个之间的最小距离 \(d\) 更大时,标志看起来更好。找出 \(d\) 的最大可能值。

其中 \(2 \le n \le 10^4,1 \le x_i,y_i \le 10^9\)

二分答案。然后根据答案来见图,用 2-sat 判断是否合法。

发现这样会 T ,考虑用线段树优化建图。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm> 
using namespace std;
const int N=2e5+5,M=1e6+5;
struct edge{
	int v,nx;
}e[M];
struct node{
	int pos,id;
}a[N<<1];
int n,ne,ans,cntn,f[N],id[M];
int tot,top,cnt,dfn[N],low[N],st[N],arr[N];
bool vis[N];
bool cmp(node u,node v){
	return u.pos<v.pos;
}
void read(int u,int v)
{	e[++ne].v=v;
	e[ne].nx=f[u];
	f[u]=ne;
}
void build(int k,int l,int r)
{	id[k]=++cntn;
	if(l==r)
	{	read(id[k],(a[l].id+n)%(2*n));
		return;
	}
	int mid=(l+r)>>1;
	build(k<<1,l,mid);
	build(k<<1|1,mid+1,r);
	read(id[k],id[k<<1]),read(id[k],id[k<<1|1]);
}
void getlk(int k,int l,int r,int x,int y,int z)
{	if(y<x)return;
	int mid=(l+r)>>1;
	if(l==x&&r==y)read(z,id[k]);
	else if(y<=mid)getlk(k<<1,l,mid,x,y,z);
	else if(x>mid)getlk(k<<1|1,mid+1,r,x,y,z);
	else getlk(k<<1,l,mid,x,mid,z),getlk(k<<1|1,mid+1,r,mid+1,y,z);
}
void Tarjan(int u)
{	dfn[u]=low[u]=++tot;vis[u]=1;
	st[++top]=u;
	for(int i=f[u];i;i=e[i].nx)
	{	int v=e[i].v;
		if(!dfn[v]){Tarjan(v);low[u]=min(low[u],low[v]);}
		else if(vis[v])low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u])
	{	cnt++;
		while(st[top]!=u){arr[st[top]]=cnt,vis[st[top]]=0;top--;} 
		arr[st[top]]=cnt,vis[st[top]]=0;top--;
	}
}
bool check(int x)
{	top=ne=tot=cnt=0,cntn=2*n;
	memset(f,0,sizeof(f));
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));
	build(1,1,cntn);
	for(int i=1;i<=2*n;i++)
	{	int l=upper_bound(a+1,a+1+2*n,(node){a[i].pos-x,0},cmp)-a;
		int r=upper_bound(a+1,a+1+2*n,(node){a[i].pos+x-1,0},cmp)-a-1;
		getlk(1,1,2*n,l,i-1,a[i].id);
		getlk(1,1,2*n,i+1,r,a[i].id);
	}
	for(int i=1;i<=2*n;i++)
		if(!dfn[i])Tarjan(i);
	for(int i=1;i<=n;i++)
		if(arr[i]==arr[i+n])return 0;
	return 1;
}
int main()
{	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{	scanf("%d%d",&a[i].pos,&a[i+n].pos);
		a[i].id=i,a[i+n].id=i+n;
	}
	sort(a+1,a+1+n*2,cmp);
	int l=0,r=a[n+n].pos-a[1].pos+1;
	while(l<=r)
	{	int mid=(l+r)>>1;
		if(check(mid))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d\n",ans);
	return 0;
}

CF843D Dynamic Shortest Path

给一个有向图,支持两种操作。询问 \(1\)\(t\) 的最短路。或修改一堆边,使得边权 \(+1\)

其中 \(n,m \le 10^5 , q \le 2000\) ,总修改边数 \(\le 10^6\)

因为操作只有加法,而且每次只加 \(1\) 。所以我们先跑 dijkstra 预处理出每个点的最短路 \(d_i\) 。然后基于最小值来拓展。

建一张新图,边权改为 \(d_u+e-e_v\) 。显然这个新图的最短路和原来的图是等价的。于是新图的到 \(v\) 的长度变成了旧图的最短路减去 \(d_v\) 。也就是增量。

然后我们考虑新加的 \(1\) 对其他的边的影响。因为等价所以可以直接考虑对新图的影响。跑最短路。

可以枚举值域,因为更新 \(x\) 条边,对最短路的影响最多加 \(c\) 。而所有最短路路径是一棵树,最多增加 \(n-1\) 。取 \(\min\) 即可。

复杂度 \(\mathcal{O}(m+w_{\max})\)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
typedef long long ll;
typedef pair<ll,ll> P;
const ll N=1e5+5;
const ll inf=1e18;
struct edge{
	ll v,w,nx;
}e[N];
ll n,m,qq,ne,f[N],d[N],g[N];
priority_queue<P,vector<P>,greater<P> >q;
queue<int> s[N];
void read(ll u,ll v,ll w)
{	e[++ne].v=v;
	e[ne].nx=f[u];
	e[ne].w=w;
	f[u]=ne;
}
void dijkstra(ll s)
{	for(ll i=1;i<=n;i++)d[i]=inf;
	while(!q.empty())q.pop();
	d[s]=0;
	q.push(make_pair(0,s));
	while(!q.empty())
	{	P p=q.top();
		q.pop();
		ll u=p.second;
		if(d[u]<p.first)continue;
		for(ll i=f[u];i;i=e[i].nx)
		{	ll v=e[i].v;
			if(d[v]>d[u]+e[i].w)
			{	d[v]=d[u]+e[i].w;
				q.push(make_pair(d[v],v));
			}
		}
	}
}
void bfs(ll x)
{	ll mx=0;
	for(ll i=0;i<=mx;i++)
	{	while(!s[i].empty())
		{	ll u=s[i].front();
			s[i].pop();
			if(g[u]<i)continue;
			for(ll i=f[u];i;i=e[i].nx)
			{	ll v=e[i].v,w=d[u]+e[i].w-d[v];
				if(g[v]>g[u]+w)
				{	g[v]=g[u]+w;
					if(g[v]<=min(x,n-1))
					{	s[g[v]].push(v);
						mx=max(mx,g[v]);
					}
				}
			}
		}
	}
}
int main()
{	scanf("%lld%lld%lld",&n,&m,&qq);
	for(ll i=1,u,v,w;i<=m;i++)
	{	scanf("%lld%lld%lld",&u,&v,&w);
		read(u,v,w);
	}
	dijkstra(1);
	while(qq--)
	{	ll opt,x;
		scanf("%lld%lld",&opt,&x);
		if(opt==1)
		{	if(d[x]!=inf)printf("%lld\n",d[x]);
			else printf("-1\n");
		}
		else{
			for(ll i=1,y;i<=x;i++)
			{	scanf("%lld",&y);
				e[y].w++;
			}
			memset(g,0x3f,sizeof(g));
			g[1]=0;s[0].push(1);
			bfs(x);
			for(ll i=1;i<=n;i++)d[i]=min(inf,g[i]+d[i]);
		}
	}
	return 0;
}

\(\text{Hall}\) 定理

\(N(S)\) 表示与 \(S\) 中的点相连的点集。

如果一个二分图 \(G(V_1,V_2,E)\) ,其中 \((|V_1| < |V_2|)\) ,若满足对于 \(V_1\) 任意点集 \(Q\)\(|Q| \le |N(Q)|\) ,则存在完美匹配。反之亦然。

先证明必要性。若存在完美匹配 \(M\) ,而 \(M\) 中将 \(Q\) 中的点都有一个匹配点,也就是说和 \(N(Q)\) 中都有一个匹配点,那么显然 \(|Q| \le |N(Q)|\)

再证明充要性。

考虑用网络流证明,我们把这个图定向,\(V_1 \to V_2\) ,然后加一个源点汇点 \(s,t\) 。设 \(P_u(x,y)\)\(G\)\(x\)\(y\) 的点不交的路径集合。设 \(f_u(x,y)=\max ( | P_u(x,y) | )\) ,设 \(g_u(x,y)\) 表示让 \(x,y\) 不连通,最少删除的点数。由 \(\text{Menger}\) 定理的点形式即 \(f_u(x,y)=g_u(x,y)\)

于是设 \(T\) 为删除点使 \(s,t\) 的最小点集。设 \(T_1=T \cap V_1,T_2=T \cap V_2\)

所以设最大流为 \(flow=|M|=f_u(s,t)=g_u(s,t)=|T|\) 。则有 \(N(V_1 \setminus T_1) \subset T_2\)

于是又有 \(|M|=|T|=|T_1|+|T_2| \ge |T_1| + |N(V_1 \setminus T_1)| \ge |T_1|+|V_1 \setminus T_1|=|V_1|\)

所以 \(|M| \ge |V_1|\) 但又因为显然 \(|M| \le |V_1|\) 。所以 \(|M|=|V_1|\)

\(\text{Vizing}\) 定理

对于一张二分图,定义最大点的度数为 \(\delta\) ,最少需要的颜色为 \(f\) ,则有 \(f(G)=\delta(G)\)

首先肯定有 \(f(G) \ge \delta(G)\) 考虑数学归纳法,设有 \(m\) 条边。

\(m=0\) 时,有 \(d=f=0\)

假设当 \(m=k\) 时成立,则当 \(m=k+1\) 时,不妨设当前的图为 \(G=(V,E)\) 。设一条边 \(e=(u,v) \in E\) 。设一个不含 \(e\) 的图为 \(G'\)

于是有 $ f(G') = \delta(G') \le \delta(G)$ 。

染色时,一定存在一个颜色不在 \(u\) 所连出去边集的颜色中,\(v\) 也一样。

如果此时同时存在一个没有出现的颜色,那么对 \((u,v)\) 染上这个颜色即可。

如果没有这种颜色,我们不妨考虑最差情况,也就是说 \(u\) 只有一个颜色 \(lx\) 没出现,\(v\) 只有 \(ly\)

反之,\(u\) 连的边有 \(ly\)\(v\) 连的边有 \(lx\)

我们设图 \(G_s'\) 表示所有为 \(lx\)\(ly\) 的边的子图。此时可发现,如果 \(u,v\) 不联通,就一定存在一个染色方法。将一个连通块反过来染色即可。

不妨假设 \(u,v\) 是连通的,因为 \(u,v\) 分别在左右部,所以路径长度一定为奇数,并且是两个颜色交错出现的。

那么一个颜色,假设是 \(lx\) ,第一个出现,那么也会以这个颜色结束。这么说明 \(lx\) 同时出现在 \(u,v\) 中。与假设矛盾。

所以 \(u,v\) 是不联通的。所以得证。所以我们只要将一个连通块的 \(lx\)\(ly\) 反过来就好了。

除了对于二分图,对于一个简单图也有结论。

对于一个没有自环的简单图,有 \(\delta(G) \le f(G) \le \delta(G) +1\)

但因为标题是二分图所以先不写证明。

CF737E Tanya is 5!

\(n\) 个人玩 \(m\) 台游戏机,每台游戏机一秒内只能一人玩,每人一秒内只能玩一台。

每台游戏机有个价格,在规定总价格内可以把一部分游戏机复制一次,每台只能复制一次。

给每个人对在每台游戏机上的需求时间,求一种方案使得所有玩游戏结束的时间尽量早。

其中 \(n \le 120,m \le 60\)

我们先考虑没有买机器这一操作。

不妨假设每个孩子要玩每个机器,把不是题目给定的都设为 \(t_{i,j}=0\) 。不妨设 \(r_i=\sum\limits_{j=1}^m t_{i,j},c_j=\sum\limits_{i=1}^nt_{i,j}\)

于是可以发现答案的下限是 \(T=\max(\max\limits_{i=1}^n\{ r_i \},\max\limits_{i=1}^m\{ c_i \})\) 。接下来我们尝试证明一定存在一个时间安排使得最后的时间为 \(T\)

考虑建一张二分图,使得二分图两边都有 \(n+m\) 个点。然后把每个人和每个机器连起来表示时间。

当然因为并没有 \(n+m\) 个点。所以我们设 \(n+1 \sim n+m\) 的孩子表示虚假的孩子。然后把 \(u_{n+i}\)\(i\) 机器连边。

同理,\(m+1 \sim n+m\) 的机器表示虚假的机器。把 \(v_{m+i}\)\(i\) 小孩连边。

于是会有四种边。(1) 真孩子和真机器 (2) 假孩子和真机器 (3) 真孩子和假机器 (4) 假孩子和假机器

然后我们尝试保证每个点的连出去的边边权和为 \(T\)

第一种。对于 \((u_i,v_j)\) 。如果 \(t_{i,j}>0\) 就设其为边权。

第二种。这意味着这个机器会剩下一些时间。于是他会有一段 \(T-c_j\) 的时间(权值和)连向假孩子。

第三种。类似的,孩子会剩下一些时间,于是会有 \(T-r_i\) 的时间(权值和)连向假机器。

第四种。此时可以发现对于真的点边权和都是 \(T\) ,而假的不是。所以我们通过假的和假的连边使得假点度数也为 \(T\)

于是根据 Hall 定理,我们发现对于任何一种二分图都有完美匹配。

如果有钱的话,对于一个 \(j\) 满足 \(c_j=T\) 时,考虑把 \(c_j\) ,把机器拆开使 \(T\) 变小,一份拆成 \(\left\lfloor \dfrac{T}{2} \right\rfloor\) ,另一份拆成 \(\left\lceil \dfrac{T}{2} \right\rceil\)。能用就用,直到不行。

然后根据这个答案再来构造。类似最小染色。

CF212A Privatization

给定 \(n+m\) 个点 \(k\) 条边的二分图,左边 \(n\) 个点,右面 \(m\) 个点。现在要把每条边标号,标号为 \(1 \sim t\)

求出最小的对于每个点与之相邻的边中最多的标号与最少标号的数量差的平均值。

输出这个和并且输出一组构造方案。

其中 \(n,m,t \le 200, k \le 5000\)

首先答案的下限是所有度数不为 \(t\) 的倍数的点。原因显然,如果一个点的度数为 \(t\) 的倍数,那么标号可以平均分配。而对于不是 \(t\) 倍数的点,他们的不均衡值( \(\max-\min\) )至少为 \(1\)

于是考虑一些边然后使每个点的度数都为 \(t\) 的倍数,然后把这些点分成 \(\dfrac{d_i}{t}\) 个块,每一块中每个编号恰好出现一次。

考虑剩下边的情况。于是为了使答案达到下限,即一个点的贡献只为 \(1\) ,意味着这变成了最小边染色问题,由 Vizing 定理得,此时最小的颜色数就是最大点度数。

对于剩下的边,考虑一个 \((u,v)\) 加入的情况,设此时 \(u,v\) 最小没被染的颜色是 \(t_1,t_2\) 。如果他们相等,可以直接连边;否则直接找增广路,因为 Vizing 定理所以一定存在,复杂度是 \(\mathcal{O}(m^2)\)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N=400+5,M=1e4+5;
struct edge{
	int u,v;
}e[M];
int n,m,k,t,tot,ans,d[N],id[N],res[M],f[M<<3][N];
int find(int x){
	for(int i=1;i<=t;i++)
		if(!f[x][i])return i;
}
void tz(int x,int s,int t)
{	int et=f[x][s],y=(x==e[et].u)?(e[et].v):(e[et].u);
	f[x][s]=f[y][s]=0;
	if(f[y][t])tz(y,t,s);
	f[x][t]=f[y][t]=et;res[et]=t;
}
int main()
{	scanf("%d%d%d%d",&n,&m,&k,&t);
	for(int i=1;i<=k;i++)
	{	scanf("%d%d",&e[i].u,&e[i].v);
		e[i].v+=n;
		if(d[e[i].u]%t==0)id[e[i].u]=++tot;
		if(d[e[i].v]%t==0)id[e[i].v]=++tot;
		d[e[i].u]++;d[e[i].v]++;
		e[i].u=id[e[i].u];e[i].v=id[e[i].v];
		int t1=find(e[i].u),t2=find(e[i].v);
		//printf("t:(%d)%d (%d)%d\n",e[i].u,t1,e[i].v,t2);
		if(t1==t2)f[e[i].u][t1]=f[e[i].v][t1]=i,res[i]=t1;
		else{
			if(f[e[i].u][t2])tz(e[i].u,t2,t1);
			f[e[i].u][t2]=f[e[i].v][t2]=i,res[i]=t2;
		}
	}
	for(int i=1;i<=n+m;i++)
		if(d[i]%t)ans++;
	printf("%d\n",ans);
	for(int i=1;i<=k;i++)
		printf("%d ",res[i]);
	printf("\n");
	return 0;
}

中国邮递员问题

给出一张有向带权连通图,求出一条总边权和最小的回路,使得经过每一条边至少一次。

其中 \(n \le 400\)

把原题转化一下,相当于要给原图加一些重边,使得新图的所有点都是入度等于出度。

所以对于每个点设 \(t_i\) 表示入度减去入度。把 \(t_i<0\) 的点和 \(t_i>0\) 的点连边。

比如 \(t_i=x\) 那么把 \(x\) 拆成 \(x\)\(1\)\(-1\) 。然后两两之间连边。边权为每对点之间的最短路。

然后跑 KM 即可。复杂度 \(\mathcal{O}(n^3)\)

但如果是无向图呢?

那就相当于让新图的所有点都是偶点。那我们只需要考虑原图的奇点。奇点之间相互连边,边权为最短路。

跑带花树求一般图最大权匹配即可。

所有可能完美匹配价值

给一个有边权的二分图,求所有可能的完美匹配价值(即不是最大匹配,而是所有可能的值)。

假设边权 \(\mathcal{O}(n)\),求一个多项式做法 。

考虑用行列式。

考虑所有完美匹配的形式,也就是说每一个点在另一边都恰好有一个匹配点。

那么对于所有的匹配,可以用排列表示。但不是所有排列都合法。

构造一个矩阵,若存在边 \((u,v)=c\) ,我们设矩阵 \(A_{u,v}=x^c\)

于是问题转化成为了该矩阵的行列式的所有非零多项式的次数的和。

但行列式存在 \(-1\) ,可能会让一些非 \(0\) 项变成 \(0\) ,于是我们对每项随机一个系数,计算的时候取膜大质数即可。

真是因为这些 \(-1\) 存在,所以二分图的最大匹配数无法解决。

稳定婚姻问题

\(n\) 个男生, \(n\) 个女生,每人对所有异性有一个排名要求一个完美匹配,使得不存在两对情侣中两个异性看对方都比自己情侣更好。

其中 \(n \le 2000\)

题目来源竟然是现实生活!!!(问题来自于一场“3分钟相亲”活动——百度

从每一个男生考虑。每次找一个仍孤寡的男生。找到一个愿意和他一起的最好女生。如果那个女生有配偶就扔掉她的配偶。然后把重新孤寡的男生放进孤寡男生队列里。

复杂度 \(\mathcal{O}(n^2)\)

CF1147F Zigzag Game

这是一道交互题。

Alice 和 Bob 又在玩游戏。

有一张二分图,左右各 \(n\) 个点,左右的点之间两两有边,每条边有边权,保证边权两两不同。将左边的点标号为 \(1 \sim n\),右边的点为 \(n+1 \sim 2n\)

游戏的开始,甲需要选择增加或者减少,乙则会自动选择另一项,之后甲需要选定一个点,将棋子放在上面。接着从乙开始,轮流移动棋子移动到一个之前没有移动到过的相邻点上,要求移动时要满足之前的选项,即如果甲选择增加,那么本次移动的边权必须大于上次移动的边权,减小通理,乙也是如此(第一次移动除外),第一个无法移动的人输了。

由 Alice 决定做甲还是乙,帮助 Alice 与 Bob(交互库)对决,赢下这场游戏。

类似二分图博弈。只要甲走时候乙能有一个匹配边就是必胜。于是我们考虑一个二分图最大权完美匹配。

首先注意到递增递减本质不变,所以我们只考虑选先手增加的情况。

考虑一组情况左边点为 \(x,y\) 右边的点为 \(w,z\) 。有匹配 \((x,w),(y,z)\)

于是先手从 \(x\) 移到 \(y\) 的时候,有 \((x,y)>(x,w),(x,y)>(y,z)\)

于是我们要构造出一个完美匹配,使得不存在匹配边满足 \((x,w)<(x,y)<(y,z)\)

这是一个稳定婚姻问题。对于左边,按照权值由大到小来设排行;右边相反。

由于稳定婚姻问题一定有解,于是乙必胜。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<vector>
#include<algorithm>
using namespace std;
const int N=50+5;
int T,n,b[N],p[N<<1],a[N][N];
vector<int> e[N];
bool cmp(int u,int v){return b[u]>b[v];}
int main()
{	scanf("%d",&T);
	while(T--)
	{	memset(p,0,sizeof(p));
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				scanf("%d",&a[i][j]);
		printf("B\n");
		char opt[3];int x;
		fflush(stdout);
		scanf("%s%d",opt,&x);
		if((opt[0]=='D')^(x>n))
		{	for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++)
					a[i][j]=-a[i][j];
		}
		for(int i=1;i<=n;i++)
		{	for(int j=1;j<=n;j++)
				b[j]=a[i][j],e[i].push_back(j);
			sort(e[i].begin(),e[i].end(),cmp);
		}
		int m=n;
		while(m)
		{	for(int i=1;i<=n;i++)
			{	if(p[i])continue;
				while(1)
				{	int j=e[i].back();
					e[i].pop_back();
					if(!p[j+n])
					{	p[i]=j+n,p[j+n]=i;
						m--;
						break;
					}
					else if(a[i][j]>a[p[j+n]][j])
					{	p[p[j+n]]=0;
						p[i]=j+n,p[j+n]=i;
						break;
					}
				}
			}
		}
		while(1)
		{	printf("%d\n",p[x]);
			fflush(stdout);
			scanf("%d",&x);
			if(x==-1)break;
		}
	}
	return 0;
}

CF1307G Cow and Exercise

给出一个 \(n\) 个点 \(m\) 条边的有向图,每条边有边权 \(w_i\)

\(Q\) 次询问,每次询问给出一个 \(x\)。你可以把一条边修改成 \(w_i+a_i\)\(a_i\) 不一定是整数),不过需要保证 \(a_i \ge 0\)\(\sum a_i<x\)

你要通过修改边权使得从 \(1\)\(n\) 的最短路径尽可能长,每次询问之间独立

数据保证至少存在一条从 \(1\)\(n\) 的路径,无重边自环。

首先我们会想到,每次修改 \(1\)\(n\) 的每组路径的必经之路一定最优。也就是修改最小割。

于是把经过同一个最小割的路径归为一个路径集。

因为要使最短路径最长,我们对于一个 \(x\) ,修改路径集中最小的 \(k\) 条边,并且使他们更改后最短路径一样长即可。假设他们都是 \(L\) ,如果此时又其他路径长度小于 \(L\) ,那肯定是选择修改那些路径集。

考虑 EK 算法,因为每次跑完最短路只会找到一个增广路。

建图时每条边流量为 \(1\) ,费用为 \(w_i\) 。当增光 \(k\) 次后就有流量 \(k\) ,因为这 \(k\) 个路径必然没有交点,就是有 \(k\) 个路径集的最短路。

所以可以把每次增广的结果流量和费用存下来,每组的结果为 \(\dfrac{cost_j+x}{flow_j}\)

我们发现这个函数是凸的。假设最优的是 \(k\) ,如果 \(j>k\) 意味着增广了更长的路径,那么结果必然更小。 如果 \(j<k\) ,修改的 \(j\) 个路径会小于 \(k-j\) 个路径的最短路。

于是有答案为 \(\min\{\dfrac{cost_j+x}{flow_j}\}\)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<queue>
#include<vector>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int N=50+5,M=N*N;
const int inf=1e9;
struct edge{
	int v,w,c,nx;
}e[M<<1];
int n,m,qq,ne=-1,s,t,f[N];
int maxflow,mincost,dis[N],incf[N],pre[N];
bool vis[N];double ans;
queue<int>q;
vector<P>res;
void read(int u,int v,int w,int c)
{	e[++ne].v=v;
	e[ne].w=w;
	e[ne].nx=f[u];
	e[ne].c=c;
	f[u]=ne;
}
bool spfa()
{	for(int i=0;i<=n;i++)dis[i]=inf;
	memset(vis,0,sizeof(vis));
	q.push(s);dis[s]=0;vis[s]=1;
	incf[s]=inf;
	while(!q.empty())
	{	int u=q.front();
		q.pop();
		vis[u]=0;
		for(int i=f[u];i!=-1;i=e[i].nx)
		{	int v=e[i].v;
			if(e[i].w==0)continue;
			if(dis[u]+e[i].c<dis[v])
			{	dis[v]=dis[u]+e[i].c;
				incf[v]=min(incf[u],e[i].w);
				pre[v]=i;
				if(!vis[v])q.push(v),vis[v]=1;
			}
		}
	}
	return (dis[t]<inf);
}
void MCMF()
{	while(spfa())
	{	int i,x=t;
		maxflow+=incf[t],mincost+=dis[t]*incf[t];
		while(x!=s)
		{	i=pre[x];
			e[i].w-=incf[t],e[i^1].w+=incf[t];
			x=e[i^1].v;
		}
		res.push_back(make_pair(maxflow,mincost));
	}
}
int main()
{	memset(f,-1,sizeof(f));
	scanf("%d%d",&n,&m);
	s=1,t=n;
	for(int i=1,u,v,w;i<=m;i++)
	{	scanf("%d%d%d",&u,&v,&w);
		read(u,v,1,w),read(v,u,0,-w);
	}
	MCMF();
	scanf("%d",&qq);
	while(qq--)
	{	int x;
		scanf("%d",&x);
		ans=1.0*inf;
		for(int i=0;i<res.size();i++)
			ans=min(ans,(1.0*res[i].second+x)/res[i].first);
		printf("%.10lf\n",ans);
	}
	return 0;
}

计算方格贡献和的题

给出一个 \(W \times H\) 的网格,坐标从 \((0, 0)\)\((W - 1, H - 1)\)。有公共边的网格之间有 \(1\) 的边权。

\(n\) 个网格上有障碍,求所有非障碍点对间的最短路之和,保证非障碍点联通。

考虑一行没有障碍的贡献。因为没有障碍所以这一行把整个网格分成了两部分 \((1,i)\) 部分和 \((i+1,n)\) 部分。显然每一部分内部的贡献不会穿过这行,只有之间的贡献才会穿过行。

于是我们单独计算这行对两部分的贡献。然后把这一行删掉(边权变成 \(0\) )。列也一样。

暴力 bfs 即可。

研究鱼

\(n\) 个研究室在做关于鱼的研究,这个 \(n\) 个鱼塘连成一棵有边权的树,每条鱼每天可以游最多一个单位。

现在研究的需求是形如在第 \(A_i\) 天在 \(C_i\) 鱼塘需要至少有 \(B_i\) 条鱼。

假设鱼都很听话(即想让它们去哪就去哪),问至少需要购买几条鱼才能满足研究的需求。

考虑把每个限制拆成点。

于是对于一条鱼,如果在满足 \(x\) 研究的情况下能满足 \(y\) 研究,那么对 \(x,y\) 连边。于是有了一张 DAG 。

对于第 \(A_i\) 个点,要至少经过 \(C_i\)什么上下界网络流!

于是成为了最小链覆盖模型。根据 Dilworth 定理,可以把问题转化为最长反链模型。

考虑一颗子树是反链的条件。首先他的全部子树都是反链。其次,存在一个时刻,使得子树无法到达根,根也不能到达子树。

DP 鸽。


\[\text{by Rainy7} \]

posted @ 2022-02-15 19:56  Rainy7  阅读(116)  评论(0编辑  收藏  举报