洛谷题单指南-线段树的进阶用法-P5445 [APIO2019] 路灯

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

题意解读:给定一个长度为n的01串,一共有q个时刻,对于每个时刻,可能有两种操作:1. 把第x个位置取反 2. 查询a ~ b - 1之间的串在过去有多少个时刻都为1。

解题思路:

一、朴素想法

每个时刻对路灯的状态建立线段树,可以通过可持久化来优化空间,查询时需要遍历a ~ b - 1之间所有线段树,去统计区间和是否为b - a,这样整体复杂度是O(n^2 * logn)。

二、另辟蹊径

由于要统计两点之间都是1的时长,不妨直接定义一个矩阵,矩阵的坐标(x,y)的值表示从x到y连通(x ~ y-1都是1)的时长,然后在每一个时刻,都去更新操作对连通时长的贡献。

1、影响无非是两种:增加时长、减少时长

增加时长的情况:初始连通块、toggle操作将两段连通块连起来

  初始连通块:对于初始序列中所有的连通块[l,r],应该将矩阵横坐标为[l,r]、纵坐标为[l,r]的矩形范围所有值都增加q,也就是对左下角(l,l)右上角(r,r)的子矩阵所有值增加q。

  toggle操作将两段连通块连起来:当将第x位置为1,也就是x~x+1连通起来,这样之前[l,x],[x+1,r]两个独立的连通块可以合并为一个连通块[l,r],应该将矩阵横坐标为[l,x]、纵坐标为[x+1,r]的矩形范围内所有值都增加q-t,t为当前时刻,也就是对左下角(l,x+1)右上角(x,r)的子矩阵所有值增加q-t。

减少时长的情况:query操作、toggle操作将一段连通块断开

  query操作:直接在矩阵中查询(a,b)的值ans,同时要判断当前[a,b]是否仍然是连通的,如果任然是连通的,答案ans要减去q - t,t为当前时刻。

  toggle操作将一段连通块断开:同样,如果x位置0后,导致原来[l,r]连通块断开,应该将矩阵横坐标为[l,x]、纵坐标为[x+1,r]的矩形范围内所有值都减少q-t,t为当前时刻,也就是对左下角(l,x+1)右上角(x,r)的子矩阵所有值减少q-t。

2、数据结构的选择

连通块维护:所有连通块可以认为是一个区间,需要存储所有区间,要判断一个点是否被区间包含并查询,要实现区间的合并、区间的分裂,可以采用类似珂朵莉树的方式,用set维护,详情可以参考此题:https://www.cnblogs.com/jcwy/p/18447333

矩阵值的加减:直接定一个二维矩阵不可行,因为路灯总数为300000,会爆内存,可以通过树套树来维护二维矩阵的信息,第一维用树状数组维护x坐标,第二维用权值线段树为y坐标区间对应的时长,而对矩阵的操作有两种:区间更新、单点查询,因此可以借助二维差分来优化,将区间更新转换为四个单点更新,单点查询转换为前缀和的查询,比如要查(1,1)~(x,y)所有值的和,在树套树中,直接通过树状数组对所有root[x]、root[x-lowbit(x)]...的线段树查询值域范围1~y的时长之和并累加。

3、总结

此题思维难度非常大,这里我们已经见识过树套树的几种典型应用,做一个梳理:

第一种:动态区间求第k小

第二种:三维偏序问题,在一个序列中不断查询满足一定条件的某种数量,条件可以转换为两个维度,同时将当前值加入到树套树中

第三种:矩阵操作,通过树套树来实现二维数据的维护,以解决数组矩阵导致的空间问题

100分代码:

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

const int N = 300005;

struct Range //区间结构体
{
    int l, r;
    bool operator < (const Range &x) const
    {
        return r < x.l; 
    }
};
set<Range> s; //存储连通区间

struct Node //线段树结构体
{
    int L, R;
    int cnt;
} tr[N * 200];
int root[N], idx; //root是树套树的根节点,idx是线段树节点编号
int stat[N]; //记录每个站点的状态
int n, q; //由于n是路灯树,站点数要+1,又由于差分操作还要加1,因此树套树的两维最大值是n+2

int lowbit(int x)
{
    return x & -x;
}

//在根为u的线段树中,将值pos的数量加v
int update(int u, int l, int r, int pos, int v)
{
    if(!u) u = ++idx;
    tr[u].cnt += v;
    if(l == r) return u;
    int mid = l + r >> 1;
    if(pos <= mid) tr[u].L = update(tr[u].L, l, mid, pos, v);
    else tr[u].R = update(tr[u].R, mid + 1, r, pos, v);
    return u;
}

//在根为u的线段树中查询值域范围在lv~rv的所有cnt之和
int query(int u, int l, int r, int lv, int rv)
{
    if(l >= lv && r <= rv) return tr[u].cnt;
    if(!u || l > rv || r < lv) return 0;
    int mid = l + r >> 1;
    return query(tr[u].L, l, mid, lv, rv) + query(tr[u].R, mid + 1, r, lv, rv);
}

//在树套树中将差分矩阵(x,y)的值加v
void insert(int x, int y, int v)
{
    for(int i = x; i <= n + 2; i += lowbit(i))
    {
        root[i] = update(root[i], 1, n + 2, y, v);
    }
}

//在树套树中查询矩阵(x,y)的值,转换为二维前缀和
int find(int x, int y)
{
    int res = 0;
    for(int i = x; i; i -= lowbit(i))
    {
        res += query(root[i], 1, n + 2, 1, y); //二维前缀和在差分数组中就是横坐标1~x,纵坐标1~y的所有值之和
    }
    return res;
}

//利用差分操作,将子矩阵(x1,y1) (x2,y2)所有值加v
void add(int x1, int y1, int x2, int y2, int v)
{
    insert(x1, y1, v);
    insert(x1, y2 + 1, -v);
    insert(x2 + 1, y1, -v);
    insert(x2 + 1, y2 + 1, v);
}

int main()
{
    cin >> n >> q;
    char c;
    for(int i = 1; i <= n; i++) 
    {
        cin >> c; 
        stat[i] = c - '0';
    }

    for(int i = 1; i <= n; i++)
    {
        if(stat[i] == 1)
        {
            int j = i;
            while(stat[j] == 1) j++; //找连续1的区间
            s.insert({i, j});
            i = j - 1;
        }
    }

    for(auto i : s) add(i.l, i.l, i.r, i.r, q); //将子矩阵(i.l,i.l) (i,r,i.r)所有值加q

    string op;
    int x, a, b;
    for(int t = 1; t <= q; t++)
    {
        cin >> op;
        if(op == "query")
        {
            cin >> a >> b;
            int ans = find(a, b);
            set<Range>::iterator it1 = s.find({a, a}); //包含a的区间
            set<Range>::iterator it2 = s.find({b, b}); //包含b的区间
            //printf("%d,%d==%d,%d\n",(*it1).l,(*it1).r,(*it2).l,(*it2).r);
            Range r1 = *it1, r2 = *it2;
            if(it1 != s.end() && it2 != s.end() && r1.l == r2.l) 
                ans -= q - t; //如果a,b已经连通,时长要减q-t
            cout << ans << endl;
        }
        else if(op == "toggle")
        {
            cin >> x;
            if(stat[x] == 1) //断开某段连通区间
            {
                set<Range>::iterator it1 = s.find({x, x});
                set<Range>::iterator it2 = s.find({x + 1, x + 1});
                Range r1 = *it1, r2 = *it2;
                if(it1 != s.end() && it2 != s.end() && r1.l == r2.l) 
                {
                    s.erase(it1);
                    s.insert({r1.l, x});
                    s.insert({x + 1, r1.r});
                    add(r1.l, x + 1, x, r1.r, -(q - t));
                }
            }
            else //连接某两段连通区间
            {
                set<Range>::iterator it1 = s.find({x, x});
                set<Range>::iterator it2 = s.find({x + 1, x + 1});
                Range r1 = *it1, r2 = *it2;
                if(it1 != s.end() && it2 != s.end() && r1.r + 1 == r2.l) //如果x左右两边都有连通区间,则将x左右两边的连通区间合并
                {
                    s.erase(it1);
                    s.erase(it2);
                    s.insert({r1.l, r2.r});
                    add(r1.l, x + 1, x, r2.r, q - t); //连通区间[r1.l,x] [x+1,r2.r]合并后,将矩阵(r1.l,x + 1) (x,r2.r)的值加q-t
                }
                else if(it1 != s.end() && r1.r == x) //如果x左边有连通区间,x右边没有,则将x~x+1加入左边的连通区间
                {
                    s.erase(it1);
                    s.insert({r1.l, x + 1});
                    add(r1.l, x + 1, x, x + 1, q - t);  //连通区间[r1.l,x] [x+1,x+1]合并后,将矩阵(r1.l,x + 1) (x,x+1)的值加q-t
                }
                else if(it2 != s.end() && r2.l == x + 1) //如果x右边有连通区间,x左边没有,则将x~x+1加入右边的连通区间
                {
                    s.erase(it2);
                    s.insert({x, r2.r});
                    add(x, x + 1, x, r2.r, q - t); //连通区间[x,x] [x+1,r2.r]合并后,将矩阵(x,x + 1) (x,r2.r)的值加q-t
                }
                else //如果x左右两边都没有连通区间,则新建一个连通区间
                {
                    s.insert({x, x + 1});
                    add(x, x + 1, x, x + 1, q - t); //新建连通区间[x,x+1],相当于合并[x,x] [x+1,x+1],将矩阵(x,x + 1) (x,x+1)的值加q-t
                }
            }
            stat[x] ^= 1;
        }
    }

    return 0;
}

 

posted @   五月江城  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示