洛谷

洛谷题单指南-连通性问题-CF487E Tourists

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

题意解读:连通的无向图,每个节点有权值,支持两种操作:1、修改节点权值 2、计算一条路径上的最小点权值

解题思路:

1、问题分析

对于一个单点修改,区间查询问题,很容易想到线段树

但是由于是在路径上,因此需要用到树链剖分,有因为是无向图而不是树,需要先进行转换。

圆方树正合适,可以设方点权值为所连接圆点的最小值,圆点权值就是图中节点的权值,这样对于查询操作来说没有压力,但是对于修改操作,

如果修改圆点权值,需要修改与其连接的所有方点的点权值,这个复杂度就没法接受了。

进一步优化,在修改圆点权值时,只需要修改其父节点的权值即可,其父节点肯定是方点,另外也要通过dfs求出圆方树中的父子关系。

由于要更新圆点的父节点方点的权值,还需要保存方点所有子圆点的权值,这里可以用multiset,修改圆点时,先删除multiset中对应权值,然后插入新权值,再取最小值来更新方点的权值。

到这里,修改的问题解决了,查询应该如何处理?

在查询一条路径的最小值时,先正常采用树链剖分以及线段树求区间最小值,最后要看一下路径两端点的LCA,如果LCA是方点,则还需要看一下其

父节点的权值,综合求最小值。

这是因为:

方点记录的是所有子节点的最小值,还要看一下其父节点的圆点
路径中间的方点不需要特殊处理,因为路径上的方点的父节点也在路径上
2、算法流程

用tarjan算法,求圆方树
dfs1圆方树,计算父子关系,同时初始化树链剖分相关的数组
dfs2圆方树,计算树链剖分相关数组
执行单点修改以及路径查询操作,其中路径查询要操作要拆解到区间用线段树来执行
100分代码:

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

const int N = 200005, M = 800005, INF = 0x3f3f3f3f;

int head1[N], head2[N], to[M], nxt[M], idx; //head1:原图 head2:圆方树
int w[N]; //圆方树的点权值,方点的权值是子圆点的最小值
multiset<int> s[N]; //方点所有子圆点的权值
int dfn1[N], low[N], timestamp1; //tarjan相关
stack<int> stk;
int cnt; //点双个数
int depth[N], fa[N], son[N], sz[N], top[N], dfn2[N], rk[N], timestamp2; //树链剖分相关
struct 
{
    int l, r;
    int minx; //l~r区间最小的权值
} tr[N * 4]; //线段树
int n, m, q;

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

void tarjan(int u, int p)
{
    dfn1[u] = low[u] = ++timestamp1;
    stk.push(u);
    for(int i = head1[u]; i; i = nxt[i])
    {
        int v = to[i];
        if(v == p) continue;
        if(!dfn1[v])
        {
            tarjan(v, u);
            low[u] = min(low[u], low[v]);

            if(low[v] >= dfn1[u])
            {
                cnt++; //点双编号
                add(head2, u, cnt + n), add(head2, cnt + n, u); //圆方树建边
                while(true)
                {
                    int t = stk.top(); stk.pop();
                    add(head2, t, cnt + n), add(head2, cnt + n, t); //圆方树建边
                    if(t == v) break;
                }
            }
        }
        else low[u] = min(low[u], dfn1[v]);
    }
}

void dfs1(int u, int p)
{
    if(u <= n && p > n) 
    {
        w[p] = min(w[p], w[u]); //方点权值是所有子节点的权值最小值
        s[p].insert(w[u]); //保存方点的所有子圆点权值
    }
    fa[u] = p;
    depth[u] = depth[p] + 1;
    sz[u] = 1;
    for(int i = head2[u]; i; i = nxt[i])
    {
        int v = to[i];
        if(v == p) continue;
        dfs1(v, u);
        sz[u] += sz[v];
        if(sz[v] > sz[son[u]]) son[u] = v;
    }
}

void dfs2(int u, int t)
{
    top[u] = t;
    dfn2[u] = ++timestamp2;
    rk[timestamp2] = u;
    if(son[u]) dfs2(son[u], t);
    for(int i = head2[u]; i; i = nxt[i])
    {
        int v = to[i];
        if(v == fa[u] || v == son[u]) continue;
        dfs2(v, v);
    }
}

void pushup(int u)
{
    tr[u].minx = min(tr[u << 1].minx, tr[u << 1 | 1].minx);
}

void build(int u, int l, int r)
{
    tr[u] = {l, r, INF};
    if(l == r) tr[u].minx = w[rk[l]];
    else
    {
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

//修改线段树pos处节点值为val
void update(int u, int pos, int val)
{
    if(tr[u].l == tr[u].r) 
    {
        tr[u].minx = val; 
        w[rk[pos]] = val; 
    }
    else 
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if(pos <= mid) update(u << 1, pos, val);
        else update(u << 1 | 1, pos, val);
        pushup(u);
    }
}

//查询线段树区间L~R范围最小值
int query(int u, int L, int R)
{
    if(tr[u].l >= L && tr[u].r <= R) return tr[u].minx;
    else if(tr[u].l > R || tr[u].r < L) return INF;
    else return min(query(u << 1, L, R), query(u << 1 | 1, L, R));
}

int queryPath(int u, int v)
{
    int res = INF;
    while(top[u] != top[v])
    {
        if(depth[top[u]] < depth[top[v]]) swap(u, v);
        res = min(res, query(1, dfn2[top[u]], dfn2[u]));
        u = fa[top[u]];
    }
    if(depth[u] > depth[v]) swap(u, v);
    res = min(res, query(1, dfn2[u], dfn2[v]));
    if(u > n && fa[u]) res = min(res, w[fa[u]]); //如果lca是方点,则需要看父节点的权值
    return res;
}

int main()
{
    memset(w, 0x3f, sizeof(w));
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++) cin >> w[i];
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        add(head1, u, v), add(head1, v, u);
    }
    //tarjan构建圆方树
    tarjan(1, 0);

    //树链剖分初始化
    dfs1(1, 0);
    dfs2(1, 1);

    //构建线段树
    build(1, 1, n + cnt);

    while(q--)
    {
        char op;
        int a, b;
        cin >> op >> a >> b;
        if(op == 'C') 
        {
            if(fa[a]) //如果父节点是方点
            {
                s[fa[a]].erase(s[fa[a]].lower_bound(w[a])); //删除原来的权值
                s[fa[a]].insert(b); //插入新权值
                update(1, dfn2[fa[a]], *s[fa[a]].begin()); //修改父节点-方点权值为子圆点中最小值
            }
            update(1, dfn2[a], b); //注意要先修改父节点,以防w[a]被修改
        }
        else cout << queryPath(a, b) << endl;
    }

    return 0;
}