洛谷

洛谷题单指南-连通性问题-P4606 [SDOI2018] 战略游戏

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

题意解读:连通的无向图,给出若干个询问,每个询问是一组点,求图中有多少个点删去会造成这组点不连通。

解题思路:

1、问题分析

对于询问中的一组点,可以构建其最小连通的子图(也就是树),就是要求这棵树中所有的点数再减去已知的点数。

对于图中一组点,构造的树,称为虚树(https://oi-wiki.org/graph/virtual-tree/)

比如:已知点是4、6、7

但本题与虚树有所不同,对于已知点4、6、7,最小连通的树涉及的节点包括:4、2、1、3、6、7

那如何要求得最小连通树的节点数呢?

2、算法流程

由于原图不易处理,可以先将无向图转化成圆方树;

点权不易处理,可以转成边权,圆点的父边权值为1,方点的父边权值为0;

将已知点按DFN排序,再两两求lca,累加每相邻两点到lca的距离之和,最后再累加首项和末项到lca的距离,除以2,就得到最小连通树除根节点外的节点个数,根节点就是首项和末项的lca,如果根节点是圆点,节点数再加1,方点则忽略。

如此即可算出一组点的最小连通树中所有节点的个数,减去已知点的个数即得到答案。

举例说明以上计算过程:

如图,已知点4,5,6,7

先将点按dfn排序,得到4,5,6,7

求4、5的lca,是2,4、5到2的距离之和为2,ans += 2

求5、6的lca,是1,5、6到1的距离之和为4,ans += 4

求6、7的lca,是3,6、7到1的距离之和为2,ans += 2

求4、7的lca,是1,4、7到1的距离之和为4,ans += 4

此时ans = 12,ans / 2 = 6,再看根节点1是圆点,因此ans = 7

最后,减去已知点数ans = 7 - 4 = 3,正好是1,2,3三个点的数量

100分代码:

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

const int N = 200005, M = 800005;
int head1[N], head2[N], to[M], nxt[M], idx; //head1:原图 head2:圆方树
int dfn[N], low[N], timestamp;
int stk[N], top;
int cnt; //点双个数
int dist[N]; //圆方树中到根节点的距离
int depth[N], fa[N][18]; //圆方树中节点深度、第2^i个父节点信息
int a[N]; //一次询问中已占领的点
int t, n, m;

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

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

void tarjan(int u, int p)
{
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u;
    for(int i = head1[u]; i; i = nxt[i])
    {
        int v = to[i];
        if(v == p) continue;
        if(!dfn[v])
        {
            tarjan(v, u);
            low[u] = min(low[u], low[v]);

            if(low[v] >= dfn[u])
            {
                cnt++;
                add(head2, cnt + n, u), add(head2, u, cnt + n); //圆方树建边
                while(true)
                {
                    int t = stk[top--];
                    add(head2, cnt + n, t), add(head2, t, cnt + n); //圆方树建边
                    if(t == v) break;
                }
            }
        }
        else low[u] = min(low[u], dfn[v]);
    }
}

void dfs(int u, int p)
{
    dfn[u] = ++timestamp;
    depth[u] = depth[p] + 1;
    dist[u] = dist[p] + (u <= n); //当前节点是圆点距离+1,当前节点是方点距离+0
    fa[u][0] = p;
    for(int i = 1; i < 18; i++)
    {
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    }
    for(int i = head2[u]; i; i = nxt[i])
    {
        int v = to[i];
        if(v == p) continue;
        dfs(v, u);
    }
}

int lca(int u, int v)
{
    if(depth[u] < depth[v]) swap(u, v);
    for(int i = 17; i >= 0; i--)
    {
        if(depth[fa[u][i]] >= depth[v])
        {
            u = fa[u][i];
        }
    }
    if(u == v) return u;
    for(int i = 17; i >= 0; i--)
    {
        if(fa[u][i] != fa[v][i])
        {
            u = fa[u][i];
            v = fa[v][i];
        }
    }
    return fa[u][0];
}

void clear()
{
    memset(head1, 0, sizeof(head1));
    memset(head2, 0, sizeof(head2));
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    idx = 0, top = 0, cnt = 0, timestamp = 0;
}

int main()
{
    cin >> t;
    while(t--)
    {
        clear();
        cin >> n >> m;
        for(int i = 1; i <= m; i++)
        {
            int u, v;
            cin >> u >> v;
            add(head1, u, v), add(head1, v, u);
        }
        tarjan(1, 0); //建立圆方树
        timestamp = 0; 
        dfs(1, 0); //dfs重新计算dfn,预处理lca相关数组,以及到根节点的距离
        int q;
        cin >> q;
        while(q--)
        {
            int s;
            cin >> s;
            for(int i = 1; i <= s; i++) cin >> a[i];
            sort(a + 1, a + s + 1, cmp); //已占领点按dfn排序

            int ans = 0;
            for(int i = 1; i < s; i++)
            {
                int u = a[i], v = a[i + 1], l = lca(u, v);
                ans += dist[u] + dist[v] - 2 * dist[l];
            }
            int u = a[1], v = a[s], l = lca(u, v);
            ans += dist[u] + dist[v] - 2 * dist[l];
            ans = ans / 2 + (l <= n) - s;
            cout << ans << endl;
        }
    }
    return 0;
}