[省选集训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);
}