peiwenjun's blog 没有知识的荒原

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\) 条边)。本质问题是有两类出边,但是总边数固定,这符合网络流模型中间节点(而不是源点)的特征。

posted on 2024-12-08 22:37  peiwenjun  阅读(132)  评论(0)    收藏  举报

导航