CodeForces 1000F One Occurrence (线段树+离线/主席树)*

博客围绕一个序列查询问题展开,题目要求在给定序列和若干查询中,找出区间里只出现一次的数字。分析采用贪心思想,通过判断区间pre最小值与左端点的关系确定答案。介绍了线段树离线和主席树两种解法,时间复杂度为O(n+qlogn)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接:http://codeforces.com/problemset/problem/1000/F

题目大意

给定一个序列和若干个查询,
每次查询区间中只出现一次的那个数字,
若答案有若干个则输出任意一个,
否则输出零。

题目分析 

根据贪心的思想,对于一个特定的区间,
假设每个位置都有一个pre,即上一次出现该位置数的位置,
如果第一次出现明显位置应该为0,
一个区间中是否存在答案,就是看这个区间的pre最小值,是否
大于左端点,那么我们只要看其区间最小值即可,注意要存个二元组,,因为
如果pre值为0则无法确定位置数,一种常规方法是线段树离线,
因为在线的话不确定查询区间出现的时间顺序,
那么当前维护的线段树不会对以前的区间产生贡献,
离线可以解决这个问题,按右端点排序,直接往线段树按序更新值即可,
那么既然涉及到时间顺序的影响问题,主席树也是一个手段。
主席树就是暴力空间般把时间序都存储下来了,
但是一个时间刻只影响一条链所有空间不会特别大,
既然我们对每个扫描时间(总共n)都开线段树存储时间刻了,
那么对于这个时间线段树,当前位置的pre影响应该消去,
所以空间要开四倍左右,然后更新i位置用位置上的pre值即可。
最后直接在第r个树上区间查询找最小的即可。
时间复杂度:O(n+qlogn).

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

#define debug puts("YES");
#define rep(x,y,z) for(int (x)=(y);(x)<(z);(x)++)
#define ll long long

#define lrt int l,int r,int rt
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define root l,r,rt
#define mst(a,b) memset((a),(b),sizeof(a))
#define pii pair<int,int>
#define fi first
#define se second
#define mk(x,y) make_pair(x,y)
const int mod=1e9+7;
const int maxn=5e5+5;
const int maxm=2e7+5;
const int ub=1e6;
const int INF=1e9;
ll powmod(ll x,ll y){ll t; for(t=1;y;y>>=1,x=x*x%mod) if(y&1) t=t*x%mod; return t;}
ll gcd(ll x,ll y){return y?gcd(y,x%y):x;}
/*
题目大意:
给定一个序列和若干个查询,
每次查询区间中只出现一次的那个数字,
若答案有若干个则输出任意一个,
否则输出零。

题目分析:
根据贪心的思想,对于一个特定的区间,
假设每个位置都有一个pre,即上一次出现该位置数的位置,
如果第一次出现明显位置应该为0,
一个区间中是否存在答案,就是看这个区间的pre最小值,是否
大于左端点,那么我们只要看其区间最小值即可,注意要存个二元组,,因为
如果pre值为0则无法确定位置数,一种常规方法是线段树离线,
因为在线的话不确定查询区间出现的时间顺序,
那么当前维护的线段树不会对以前的区间产生贡献,
离线可以解决这个问题,按右端点排序,直接往线段树按序更新值即可,
那么既然涉及到时间顺序的影响问题,主席树也是一个手段。
主席树就是暴力空间般把时间序都存储下来了,
但是一个时间刻只影响一条链所有空间不会特别大,
既然我们对每个扫描时间(总共n)都开线段树存储时间刻了,
那么对于这个时间线段树,当前位置的pre影响应该消去,
所以空间要开四倍左右,然后更新i位置用位置上的pre值即可。
最后直接在第r个树上区间查询找最小的即可。
时间复杂度:O(n+qlogn).
*/
int rt[maxn],ls[maxm],rs[maxm];
struct node{ int pos , val; };
node sum[maxm];
int tot=0;
void build(int& o,int l,int r){
    o=++tot;
    sum[o].val=INF;
    if(l==r) return;
    int mid=l+r>>1;
    build(ls[o],l,mid);
    build(rs[o],mid+1,r);
}
void update(int& o,int l,int r,int last,int p,int v){
    o=++tot;
    ls[o]=ls[last],rs[o]=rs[last];
    if(l==r){
        sum[o].val=v;
        sum[o].pos=l;
        return;
    }
    int mid=l+r>>1;
    if(p<=mid) update(ls[o],l,mid,ls[o],p,v);
    else update(rs[o],mid+1,r,rs[o],p,v);
    if(sum[ls[o]].val<sum[rs[o]].val) sum[o]=sum[ls[o]];
    else sum[o]=sum[rs[o]];
}
node query(int o,int l,int r,int pl,int pr){
    if(pl<=l&&r<=pr) return sum[o];
    if(sum[o].val==INF) return sum[o];
    int mid=l+r>>1;
    node tp1,tp2;
    tp2.val=INF;
    if(pl<=mid){
        tp1=query(ls[o],l,mid,pl,pr);
        if(tp1.val<tp2.val) tp2=tp1;
    }
    if(mid<pr){
        tp1=query(rs[o],mid+1,r,pl,pr);
        if(tp1.val<tp2.val) tp2=tp1;
    }
    return tp2;
}
int a[maxn],n,x,y,q;
int pre[maxn],last[maxn];
int main(){
    scanf("%d",&n);
    build(rt[0],1,n);
    rep(i,1,n+1) scanf("%d",&a[i]);
    rep(i,1,n+1){
        if(last[a[i]]) pre[i]=last[a[i]];
        last[a[i]]=i;
    }
    rep(i,1,n+1){
        if(pre[i]){
            int tmp;
            update(tmp,1,n,rt[i-1],pre[i],INF);
            update(rt[i],1,n,tmp,i,pre[i]);
        }
        else update(rt[i],1,n,rt[i-1],i,0);
    }
    scanf("%d",&q);
    rep(i,0,q){
        scanf("%d%d",&x,&y);
        node ret=query(rt[y],1,n,x,y);
        if(ret.val>=x) puts("0");
        else printf("%d\n",a[ret.pos]);
    }

    return 0;
}

 

### 关于C++实现线段树的思维导图 线段树是一种高效的数据结构,主要用于解决区间查询和更新问题。以下是围绕C++中实现线段树的相关知识点构建的一个思维导图框架: #### 1. **线段树的基础概念** - 定义:线段树是一种二叉结构,用于处理数组上的范围查询和修改操作[^3]。 - 特点: - 每个节点表示一个区间。 - 叶子节点对应数组中的单个元素。 - 非叶子节点表示其两个子区间的并集。 #### 2. **线段树的核心功能** - 区间求和/最大值/最小值:通过递归方式计算指定区间的值。 - 单点更新:更改某个位置的值,并相应调整受影响的父节点。 - 区间更新:批量修改某一段区域内的值。 #### 3. **C++实现的关键要素** - 构造函数初始化列表:在线段树类定义中,利用构造函数初始化列表完成必要的变量设置[^5]。 - 动态内存分配:通常使用`std::vector<int>`作为底层容器存储节点数据。 - 延迟标记(Lazy Propagation):优化大规模区间更新操作,减少冗余计算。 ```cpp #include <iostream> #include <vector> using namespace std; class SegmentTree { private: vector<int> tree; int n; void build(int node, int start, int end, const vector<int>& arr) { if (start == end) { tree[node] = arr[start]; } else { int mid = (start + end) / 2; build(2 * node, start, mid, arr); build(2 * node + 1, mid + 1, end, arr); tree[node] = tree[2 * node] + tree[2 * node + 1]; // 修改此处可支持其他运算 } } public: SegmentTree(const vector<int>& arr) : n(arr.size()), tree(4 * n, 0) { build(1, 0, n - 1, arr); // 根节点编号为1 } int query(int node, int start, int end, int l, int r) { if (r < start || l > end) return 0; // 不相交的情况 if (l <= start && end <= r) return tree[node]; // 完全包含 int mid = (start + end) / 2; return query(2 * node, start, mid, l, r) + query(2 * node + 1, mid + 1, end, l, r); } void update(int node, int start, int end, int idx, int val) { if (start == end) { tree[node] += val; } else { int mid = (start + end) / 2; if (idx <= mid) { update(2 * node, start, mid, idx, val); } else { update(2 * node + 1, mid + 1, end, idx, val); } tree[node] = tree[2 * node] + tree[2 * node + 1]; } } }; ``` #### 4. **学习资源推荐** - 推荐书籍:《算法竞赛入门经典——训练指南》,其中详细讲解了线段树的应用场景及其变种形式[^2]。 - 在线教程:LeetCode 和 Codeforces 平台提供了丰富的练习题目以及社区讨论[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值