洛谷

洛谷题单指南-连通性问题-P7737 [NOI2021] 庆典

原题链接:https://www.luogu.com.cn/problem/P7737

题意解读:在有向图中,有q个询问,求从s到t的路径上,可以借助k个临时边的情况下,一共可以经过多少个点,点可以重复走。

解题思路:

1、问题分析

点可以重复走,那么就应该先通过强联通分量进行缩点。

缩点后是DAG,题目还有一个重要的性质:对于三座城市 x,y,z,若 x⇒z 且 y⇒z,那么有 x⇒y 或 y⇒x。意味着如果从x有两条到z的路径,那么只需要保留一条,且不影响整个图的连通性,应该保留哪一条呢?答案是拓扑排序时,使其入度减为0的哪一条边,对此建立一棵树,更便于处理。

有了缩点后的DAG的生成树,还要处理临时边的问题,由于临时边不多,可以针对s、t以及所有临时边涉及的节点,建立虚树!虚树的点权值就是连通分量的大小,边权值就是两个缩点之间所有点权值之和。对于临时边,权值为0。

然后在虚树上,在正向图中从起点s BFS标记经过的点和边,在反向图中从终点t BFS标记经过点和边,对于交集的部分累加答案。

2、注意事项

关键点一:虚树的节点不多,不能沿用原图的节点号,这样会在每次memset邻接表数组时性能较慢,应该进行类似离散化处理,使得虚树节点编号从1开始,这样虚树邻接表头数组最长可以设置为30。

关键点二:q次询问,每次建立虚树的消耗在30*log(n),log(n)是lca的复杂度,这样也是无法通过的,需要采用O(1)的LCA算法,这里使用欧拉序+ST表的方式,其核心原理就是记录dfs过程中每个时间戳对应的节点(回溯时也要记录),对于lca(u,v)就是u、v欧拉序之间深度最小的节点,通过ST表维护区间深度最小的节点即可。

100分代码:

#include <bits/stdc++.h>
using namespace std;
using namespace std::chrono;

const int N = 300005, M = 2000005, V = 30;

//建图
int head1[N]; //原图
int head2[N]; //缩点图
int head3[N]; //拓扑排序生成叶向树
int head4[V]; //虚树-正向
int head5[V]; //虚树-反向
int to[M], w[M], nxt[M], idx;

//tarjan
int dfn[N], low[N], timestamp;
stack<int> stk;
bool vis[N];
int scc[N], sz[N], cnt;
int in[N];

int root;
int n, m, q, k;

void add(int head[], int a, int b, int c)
{
    to[++idx] = b, w[idx] = c, nxt[idx] = head[a], head[a] = idx;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++timestamp;
    stk.push(u);
    vis[u] = true;
    for(int i = head1[u]; i; i = nxt[i])
    {
        int v = to[i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        }
        else if(vis[v]) low[u] = min(low[u], dfn[v]);
    }
    if(low[u] == dfn[u])
    {
        cnt++;
        while(true)
        {
            int t = stk.top(); stk.pop();
            vis[t] = false;
            scc[t] = cnt;
            sz[cnt]++;
            if(t == u) break;
        }
    }
}

void buildDAG()
{
    for(int u = 1; u <= n; u++)
    {
        for(int i = head1[u]; i; i = nxt[i])
        {
            int v = to[i];
            int a = scc[u], b = scc[v];
            if(a != b) 
            {
                add(head2, a, b, 0);
                in[b]++;
            }
        }
    }
}

void topsort()
{
    queue<int> q;
    for(int i = 1; i <= cnt; i++)
        if(!in[i]) 
        q.push(i);
    while(q.size())
    {
        int u = q.front(); q.pop();
        for(int i = head2[u]; i; i = nxt[i])
        {
            int v = to[i];
            if(--in[v] == 0)
            {
                q.push(v);
                add(head3, u, v, 0);
                if(root == 0) root = u;
            }
        }
    }
}

//LCA,路径和,DFN
int depth[N], sum[N], dfn2[N], timestamp2;
int id[N << 1], st[N << 1][20]; //欧拉序实现O(1) LCA
void dfs(int u, int p)
{
    dfn2[u] = ++timestamp2; //记录DFN序
    id[timestamp2] = u; //记录欧拉序对应的节点
    depth[u] = depth[p] + 1;
    sum[u] = sum[p] + sz[u];
    for(int i = head3[u]; i; i = nxt[i])
    {
        int v = to[i];
        if(v == p) continue;
        dfs(v, u);
        id[++timestamp2] = u; //记录欧拉序对应的节点
    }
}

//st[i][j]表示欧拉序区间[i, i + 2^j - 1]的最小深度节点
void init()
{
    for(int i = 1; i <= timestamp2; i++) st[i][0] = id[i];
    for(int j = 1; j <= 19; j++)
    {
        for(int i = 1; i + (1 << j) - 1 <= timestamp2; i++)
        {
            int a = st[i][j - 1], b = st[i + (1 << (j - 1))][j - 1];
            if(depth[a] < depth[b]) st[i][j] = a;
            else st[i][j] = b;
        }
    }
}

//u和v的lca是区间[dfn2[u], dfn2[v]]的最小深度节点
int lca(int u, int v)
{
    u = dfn2[u], v = dfn2[v];
    if(u > v) swap(u, v);
    int t = log2(v - u + 1);
    int f1 = st[u][t], f2 = st[v - (1 << t) + 1][t];
    return depth[f1] < depth[f2] ? f1 : f2;
}

//获取虚树节点编号
map<int, int> n2v, v2n; 
int vid;
int getVNode(int x)
{
    if(!n2v.count(x))
    {
        n2v[x] = ++vid;
        v2n[vid] = x;
    }
    return n2v[x];
}

bool cmp(int x, int y)
{
    return dfn2[x] < dfn2[y];
}

//构建虚树
int nodes[N], total;
void buildVTree()
{
    sort(nodes + 1, nodes + total + 1, cmp);
    int len  = total;
    for(int i = 1; i < len; i++)
    {
        int u = nodes[i], v = nodes[i + 1];
        int l = lca(u, v);
        nodes[++total] = l;
    }
    sort(nodes + 1, nodes + total + 1, cmp);
    total = unique(nodes + 1, nodes + total + 1) - nodes - 1;
    for(int i = 1; i < total; i++)
    {
        int u = nodes[i], v = nodes[i + 1];
        int l = lca(u, v);
        if(l == v) continue;
        int distsum = sum[v] - sum[l] - sz[v];
        add(head4, getVNode(l), getVNode(v), distsum), add(head5, getVNode(v), getVNode(l), distsum);
    }
}

//双向BFS查找路径上的点数
bool vis1[V], edge1[V], edge2[V];
int find(int s, int t)
{
    memset(vis1, 0, sizeof(vis1));
    memset(edge1, 0, sizeof(edge1));
    memset(edge2, 0, sizeof(edge2));
    int res = 0;
    queue<int> q;
    //正向图中从起点s开始标记
    q.push(s);
    while(q.size())
    {
        int u = q.front(); q.pop();
        vis1[u] = true;
        for(int i = head4[u]; i; i = nxt[i])
        {
            int v = to[i];
            if(!edge1[i]) 
            {
                q.push(v);
                edge1[i] = true;
            }
        }
    }
    //反向图中从终点t开始标记,累加交集的个数
    q.push(t);
    while(q.size())
    {
        int u = q.front(); q.pop();
        if(vis1[u]) //发现点交集
        {
            res += sz[v2n[u]];
            vis1[u] = false;
        }
        for(int i = head5[u]; i; i = nxt[i])
        {
            int v = to[i];
            if(!edge2[i]) 
            {
                q.push(v);
                edge2[i] = true;
                if(edge1[i - 1]) //发现边交集
                {
                    res += w[i];
                    edge1[i - 1] = false;
                }
            }
        }
    }
    return res;
}

int main()
{
    cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
    cin >> n >> m >> q >> k;
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        add(head1, u, v, 0);
    }

    for(int i = 1; i <= n; i++)
        if(!dfn[i])
            tarjan(i);

    buildDAG(); //缩点
    topsort(); //拓扑排序建立叶向树
    dfs(root, 0); //预处理
    init(); //初始化LCA

    while(q--)
    {
        memset(head4, 0, sizeof(head4));
        memset(head5, 0, sizeof(head5));
        total = 0, idx = 0, vid = 0, n2v.clear(), v2n.clear();
        int s, t;
        cin >> s >> t;
        s = scc[s], t = scc[t];
        nodes[++total] = s;
        nodes[++total] = t;
        for(int i = 1; i <= k; i++)
        {
            int a, b;
            cin >> a >> b;
            a = scc[a], b = scc[b];
            if(a == b) continue; //自环
            nodes[++total] = a;
            nodes[++total] = b;
            add(head4, getVNode(a), getVNode(b), 0), add(head5, getVNode(b), getVNode(a), 0); //虚树中加0边
        }
        buildVTree(); //建立虚树其余边
        cout<< find(getVNode(s), getVNode(t)) << endl; //找路径上的点数
    }
    return 0;
}