CF2046D For the Emperor! 题解
题目描述
\(T\) 组数据, \(n\) 个点, \(m\) 条单向边,第 \(i\) 个点有 \(a_i\) 个信使。
初始你可以激活若干个信使,被激活的信使可以沿着单向边移动,并激活到达的点的所有信使。
求初始最少需要激活多少个信使,才能满足每个点都有被激活的信使到达过,无解输出 -1 。
数据范围
- \(1\le T\le 100,2\le n\le 200,1\le m\le 800,0\le a_i\le n\) 。
- \(\sum n\le 200,\sum m\le 800\) 。
分析
容易发现同一强联通分量中只要有一个信使被激活,那么它可以走遍整个强连通分量,激活强连通分量内的所有信使。
因此我们可以先缩点,信使个数为强连通分量中原先 \(a_i\) 之和,从而转化成有向无环图上的问题。
观察数据范围,考虑网络流,用一条流量表示一个信使的行为。
考虑如何统计答案。如果手动激活一个点的代价为 1-tag ,被动激活一个点的代价为 -tag ,那么有解当且仅当总代价略大于 -n*tag ,且答案为总代价减去 n*tag 。
考虑如何实现上述想法,令 \(u'\) 为入点, \(u\) 为出点,连边 \((u',u,1,-tag),(u',u,\infty,0)\) ,这样第一次走 \(u'\to u\) 的边(即 \(u\) 被激活)就会拿上 -tag 的代价。
再考虑如何模拟信使的行为。对同一个点的 \(a_i\) 个信使,有两种情况:
- 被其他信使激活,代价 \(0\) 。
- 手动激活一个信使,代价 \(1\) 。
再添加一个虚点 \(u''\) 控制流量为 \(a_u\) ,连边 \((S,u'',a_u,0),(u'',u',1,1),(u'',u,\infty,0)\) 即可。
连边正确性解释:如果手动激活 \(u\) ,为什么不会所有流量都走代价为零的边呢?这是因为
tag很大,为了拿到 \(u'\to u\) 的-tag代价,让其中一条流量走 \((u'',u',1,1)\) 的边是更优选择。
还有 \((u,v',\infty,0),(u,T,\infty,0)\) 要连边,很容易理解,不解释。
也许读者会疑惑为什么要缩点,事实上,缩点的意义是防止网络流建图出现负环。
时间复杂度 \(\mathcal O(Tnm\sum a_i)\) ,能过就行。
#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int m,n,t,u,v,cnt,num;
int a[maxn],b[maxn],bel[maxn],dfn[maxn],low[maxn];
bool ins[maxn];
stack<int> st;
vector<int> g[maxn];
void tarjan(int u)
{
dfn[u]=low[u]=++cnt,st.push(u),ins[u]=true;
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(ins[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
int v;
b[++num]=0;
do v=st.top(),st.pop(),ins[v]=false,bel[v]=num,b[num]+=a[v];
while(v!=u);
}
}
namespace flow
{
const int maxn=605,maxm=4005,tag=1e5,inf=1e9;
int s,t,tot;
int head[maxn],to[maxm],nxt[maxm],f[maxm],val[maxm];
int d[maxn],pre[maxn],incf[maxn];
bool vis[maxn];
void init(int n)
{
s=0,t=3*n+1,tot=1,fill(head,head+t+1,0);
}
void addedge(int u,int v,int w,int c)
{
nxt[++tot]=head[u],to[tot]=v,f[tot]=w,val[tot]=c,head[u]=tot;
nxt[++tot]=head[v],to[tot]=u,f[tot]=0,val[tot]=-c,head[v]=tot;
}
bool spfa()
{
queue<int> q;
memset(d,0x3f,sizeof(d));
memset(vis,false,sizeof(vis));
memset(incf,0,sizeof(incf));
q.push(s),d[s]=0,incf[s]=inf;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=false;
for(int i=head[u];i!=0;i=nxt[i])
{
int v=to[i];
if(f[i]&&d[v]>d[u]+val[i])
{
d[v]=d[u]+val[i];
pre[v]=i;
incf[v]=min(incf[u],f[i]);
if(!vis[v])
q.push(v),vis[v]=true;
}
}
}
return incf[t]>0;
}
int ek()
{
int flow=0,cost=0;
while(spfa())
{
flow+=incf[t];
cost+=d[t]*incf[t];
for(int i=t;i!=s;i=to[pre[i]^1])
{
f[pre[i]]-=incf[t];
f[pre[i]^1]+=incf[t];
}
}
return cost;
}
}
using flow::addedge;
using flow::tag;
using flow::inf;
int main()
{
for(scanf("%d",&t);t--;)
{
scanf("%d%d",&n,&m),num=0;
for(int i=1;i<=n;i++) scanf("%d",&a[i]),dfn[i]=low[i]=0,g[i].clear();
while(m--) scanf("%d%d",&u,&v),g[u].push_back(v);
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
flow::init(num);
for(int u=1;u<=n;u++) for(auto v:g[u]) if(bel[u]!=bel[v]) addedge(3*bel[u],3*bel[v]-1,inf,0);
n=num;
for(int u=1;u<=n;u++)
{
if(b[u]) addedge(flow::s,3*u-2,b[u],0),addedge(3*u-2,3*u-1,1,1),addedge(3*u-2,3*u,inf,0);
addedge(3*u-1,3*u,1,-tag),addedge(3*u-1,3*u,inf,0),addedge(3*u,flow::t,inf,0);
}
int cur=flow::ek()+n*tag;
printf("%d\n",cur<=n?cur:-1);
}
return 0;
}
总结
-
怎么想到网络流的?
缩点以后笔者最先想的是能不能 \(\mathcal O(n^2m)\) 或 \(\mathcal O(m^2n)\) 动态规划,结果发现虽然单个信使的转移无后效性,但是信使之间的联系非常复杂,必须记下 每个点有多少信使/每个信使在哪个点 才能封闭转移,状态直接爆炸。
把动态规划否决后,结合数据范围大概就猜到网络流了。再考虑怎么判无解,把所有信使手动点亮以后发现是经典网络流问题(连边 \((S,u,a_u),(u,v,\infty),(v,T,1)\)),于是更加坚定内心想法。
-
添加虚点控制流量的操作很巧妙。
笔者一开始的想法是连边 \((S,u',1,1),(S,u,a_u,0)\) ,但是发现这样控不住流量上界(即 \(S\) 可能会发出 \(a_u+1\) 条边)。本质问题是有两类出边,但是总边数固定,这符合网络流模型中间节点(而不是源点)的特征。
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/18593924
浙公网安备 33010602011771号