[省选集训2022] 模拟赛5

A

题目描述

给定 \(n\) 个数 \(a_i\),其中 \(k\)\(a_i\) 是奇数,再给定一个 \(n\times n\) 的矩阵 \(\{c_{i,j}\}\),都保证是非负整数,你可以做下列操作任意次:

  • \(a_i\)\(1\)\(a_j\)\(1\),花费 \(c_{i,j}\) 的代价,\(i=j\) 是被允许的。

问把所有 \(a_i\) 都变成 \(0\) 的最小花费什么,无解输出 \(-1\)

\(1\leq n\leq 50,k\leq8,c_{i,j}\leq10^5,a_i\leq 100\)

解法

一般图最大匹配是不在我能力范围内的,但是鉴于本题还是匹配问题,我们尽量把问题化归到二分图上去,并且我们合理揣测出题人的意图,把奇数点作为关键点

首先考虑没有奇数点的情况,如果 \((i,j)\) 之间操作了一次就把他们之间连一条无向边,最后得到一个可能有重边和自环的图,并且满足每个点的度数都是偶数。那么这个图是个欧拉图,也就是我们能找到一种边定向方案,使得所有点的入度等于出度。那么可以得到关键结论:存在最优方案,使得对应的图可以找到一种边定向方案使得所有点入度等于出度

那么我们拆点,把点 \(i\) 拆成左部点和右部点,分别代表了一个点的入度和出度。那么两部之间可以直接连费用为 \(c_{i,j}\) 的完全二分图,源点向 \(x_{in}\) 连容量 \(\frac{a_i}{2}\) 的边,\(x_{out}\) 向汇点连容量为 \(\frac{a_i}{2}\) 的边,对这个图跑费用流即可。

那么如果有奇数点怎么办?如果我们在图上通过加边的方式把奇数点补成偶数点,那么可以类似地得到结论:存在最优方案,奇数点的入度和出度相差 \(1\)

因为奇数点的数量很少,所以我们花 \(O(2^k)\) 来枚举奇数点的度数,然后跑费用流即可。

总结

二元操作可以通过建边转化到图上去思考。

往已知的问题上化归,这时一定要坚信题目具有某种特殊性。

#include <cstdio>
#include <iostream>
#include <queue>
using namespace std;
const int M = 105;
const int inf = 0x3f3f3f3f;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],b[M],c[M][M],p[M];
int S,T,tot,f[M],dis[M],pre[M],lst[M],flow[M];
struct edge{int v,f,c,next;}e[M*M];
void add(int u,int v,int F,int c)
{
	e[++tot]=edge{v,F,c,f[u]},f[u]=tot;
	e[++tot]=edge{u,0,-c,f[v]},f[v]=tot;
}
int bfs()
{
	queue<int> q;
	for(int i=0;i<=T;i++) dis[i]=inf,flow[i]=0;
	q.push(S);dis[S]=0;flow[S]=inf;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=f[u];i;i=e[i].next)
		{
			int v=e[i].v,c=e[i].c;
			if(e[i].f>0 && dis[v]>dis[u]+c)
			{
				dis[v]=dis[u]+c;
				pre[v]=u;lst[v]=i;
				flow[v]=min(flow[u],e[i].f);
				q.push(v);
			}
		}
	}
	return flow[T]>0;
}
int work()
{
	int res=0;
	tot=1;S=0;T=2*n+1;
	for(int i=0;i<=T;i++) f[i]=0;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			add(i,j+n,inf,c[i][j]);
	for(int i=1;i<=n;i++)
	{
		add(S,i,(a[i]+b[i])/2,0);
		add(i+n,T,(a[i]-b[i])/2,0);
	}
	while(bfs())
	{
		res+=flow[T]*dis[T];int u=T;
		while(u)
		{
			e[lst[u]].f-=flow[T];
			e[lst[u]^1].f+=flow[T];
			u=pre[u];
		}
	}
	return res;
}
signed main()
{
	freopen("match.in","r",stdin);
	freopen("match.out","w",stdout);
	n=read();ans=1e9;
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		if(a[i]%2) p[m++]=i;
	}
	if(m%2) {puts("-1");return 0;}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			c[i][j]=read();
	for(int i=0;i<(1<<m);i++)
	{
		int cnt=0;
		for(int j=0;j<m;j++)
		{
			if(i>>j&1) b[p[j]]=1;
			else b[p[j]]=-1,cnt++;
		}
		if(cnt!=m/2) continue;
		ans=min(ans,work());
	}
	printf("%d\n",ans);
}

C

题目描述

\(n\) 个点的树,边有边权,每个点有一个范围为 \(r_i\) 的炸弹,如果引爆它会连锁引爆和它距离 \(\leq r_i\) 的所有炸弹,问初始最少引爆多少炸弹可以使得所有炸弹都被引爆。

\(n\leq 3\cdot 10^5\),所有权值 \(\leq 10^9\)

解法

显然这题是 炸弹 搬到了树上,所以我们可以沿用那题的做法。

每个点向距离 \(\leq r_i\) 的点连边,边表示引爆关系,然后对这个图跑 \(\tt tarjan\),发现引爆度数为 \(0\) 的点是必要的,因为没人可以覆盖它们;同时也是充分的,因为引爆它们所有点都会被覆盖,所以缩点后度数为 \(0\) 的点的个数就是答案。

暴力跑是 \(O(n^2)\) 就算剪枝了也会被卡,考虑在序列上我们是用线段树优化建图,那么搬到树上可以考虑点分治优化建图,当然中心思想还是通过虚点减少连边。

考虑连边是一对点的路径关系,所以我们在分治中心 \(u\) 取出分治子树内的所有点,然后按两个关键字排序:\(A\) 是深度 \(dep[u]\)\(B\) 是向自己子树外的覆盖能力 \(a[u]-dep[u]\)

那么 \(u\)\(v\) 连有向边当且仅当 \(a[u]-dep[u]\geq dep[v]\),注意子树内部的连边不用考虑,因为就算连出来了不合法的边也是没有影响的(到中心再回到子树覆盖能力白白减小)

排序之后就可以双指针了,\(B\) 数组的每个元素都建一个虚点,这个虚点需要完成连接 \(A\) 前缀的功能,所以它要连上一个虚点,再连向新增的 \(A\) 中的元素即可。具体实现中有一些小细节。

建完图之后就可以无脑跑 \(\tt tarjan\) 了,时间复杂度 \(O(n\log^2n)\),瓶颈是排序。

总结

一定要对路径这个东西有敏锐的感觉,当式子中涉及到路径就可以考虑掏出点分治了。

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#include <stack> 
using namespace std;
const int M = 300005;
const int N = M*50;
#define pb push_back
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,sz,rt,tot,ans,f[M],a[M],vis[M],siz[M],mx[M];
int m1,m2,Ind,cnt,low[N],dfn[N],col[N],d[N],in[N];
vector<int> g[N];stack<int> s;
struct edge{int v,c,next;}e[M<<1];
struct node
{
	int u,c;
	bool operator < (const node &b) const
		{return c<b.c;}
}pa[M],pb[M];
void find(int u,int fa)
{
	siz[u]=1;mx[u]=0;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(v==fa || vis[v]) continue;
		find(v,u);
		siz[u]+=siz[v];
		mx[u]=max(mx[u],siz[v]);
	}
	mx[u]=max(mx[u],sz-siz[u]);
	if(mx[u]<mx[rt]) rt=u;
}
void dfs(int u,int fa,int d)
{
	pa[++m1]=node{u,d};
	if(a[u]>=d) pb[++m2]=node{u,a[u]-d};
	if(d>1e9) return ;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(v==fa || vis[v]) continue;
		dfs(v,u,d+e[i].c);
	}
}
void solve(int u)
{
	vis[u]=1;m1=m2=0;dfs(u,0,0);
	sort(pa+1,pa+1+m1);
	sort(pb+1,pb+1+m2);
	for(int i=1,j=1,lst=0;i<=m2;i++)
	{
		int x=lst;
		while(j<=m1 && pa[j].c<=pb[i].c)
		{
			if(x==lst) x=++n;
			g[x].pb(pa[j].u),j++;
		}
		if(x!=lst && lst) g[x].pb(lst);
		if(x) g[pb[i].u].pb(x);lst=x;
	}
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(vis[v]) continue;
		rt=0;sz=siz[v];
		find(v,0);solve(rt);
	}
}
void tarjan(int u)
{
	dfn[u]=low[u]=++Ind;
	s.push(u);in[u]=1;
	for(int v:g[u])
	{
		if(!dfn[v])
		{
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(in[v])
			low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u])
	{
		int v=0;cnt++;
		do
		{
			v=s.top();s.pop();
			col[v]=cnt;in[v]=0;
		}while(v!=u);
	}
}
signed main()
{
	freopen("infect.in","r",stdin);
	freopen("infect.out","w",stdout);
	n=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read(),c=read();
		e[++tot]=edge{v,c,f[u]},f[u]=tot;
		e[++tot]=edge{u,c,f[v]},f[v]=tot;
	}
	mx[0]=sz=n;find(1,0);solve(rt);
	for(int i=1;i<=n;i++)
		if(!dfn[i]) tarjan(i);
	for(int u=1;u<=n;u++) for(int v:g[u])
		if(col[u]!=col[v]) d[col[v]]++;
	for(int i=1;i<=cnt;i++) ans+=!d[i];
	printf("%d\n",ans);
}
posted @ 2022-02-10 21:51  C202044zxy  阅读(175)  评论(0编辑  收藏  举报