集合问题
题面
题目描述
定义区间
定义两个区间
定义两个区间
一个区间可重集的
你要维护一个区间可重集,并进行
- 加入区间
。 - 删除区间
,保证其存在,如果有多个仅删除一个。
每次操作后,你要找到区间可重集中的一个子集(不能是空集),满足这个子集的
保证任意时刻(初始时除外)集合非空。
数据范围
时间限制 3s。
题解
解题思路
问题转化 1
首先我们可以看出,一个区间构成的多重集的
现在目标变为:若存在
另外,如果只存在一个线段,当然就选那一个了。
问题转化 2
我们发现若任意两个线段的交都不是空集,这种情况很容易解出,贪心即可。我们开两个 multiset
(以下简称 set
)lf
和 rf
。其中 lf
储存所有线段最右的左端点,rf
存最左的右端点,设这两个点分别为 and
。接下来只需找到两点对应的另外一个端点,并求出最小的
但是假如存在空集,问题又变得扑朔迷离起来。因为我们通过以上方法不能得到空集的具体位置,便不能贪心,而
问题转化 3
存在空集,意味着不能贪心,也意味着我们只用考虑空集。
问题可以转化成以下描述:
有一个序列
什么意思呢?在本题,
解决问题
如何求这个最小值呢?我们发现若要最小,肯定是
因此我们采用线段树。线段树的每个节点存三个信息:
如何上传呢?
我们还需要对每个叶子结点进行维护(对数轴直接进行维护)。具体地说,数轴上每个点开两个 set
rm
和 lm
,分别储存以这个点为左端点的的线段的右端点构成的集合与以这个点为右端点的线段的左端点构成的集合。每一次单点更新到叶子结点时,都要往里面插入对应的值,并且更新树上的值。
最后我们来复盘一下整个过程(插入):
- 向
lf
和rf
插入区间 ,在线段树 处插入 , 处插入 。 - 判断是否存在
空区间。- 如果不存在空区间,就输出求得的最小区间的
。 - 如果存在空区间,输出线段树维护的最小值。
- 如果不存在空区间,就输出求得的最小区间的
代码实现
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<set>
#include<vector>
#include<utility>
#define R myio::read_int()
using namespace std;
namespace myio{
int read_int(){
int x=0;char ch,f=1;
while(!isdigit(ch=getchar())) if(ch=='-') f=0;
do x=(x<<1)+(x<<3)+ch-'0';
while(isdigit(ch=getchar()));
return (f==1?x:-x);
}void PRINT(int x){
if(x<=9) putchar(x+'0');
else PRINT(x/10),putchar(x%10+'0');
}void print_int(int x){
if(x<0) putchar('-'),PRINT(-x);
else PRINT(x);
putchar('\n');
}
}
const int MAXN=1e6;
struct Node { //线段树上的一个节点所储存的信息
int minr,maxl,minres;
Node():minr(MAXN<<1|1),maxl(-MAXN-1),minres(MAXN<<1|1){}
};
class Leaf { //线段树叶节点存储的信息
private:
multiset<int,greater<int> > lm;
multiset<int,less<int> > rm;
int pos;
public:
Leaf(){
lm.insert(-MAXN-1);
rm.insert(MAXN<<1|1);
}
void setPos(int _pos){pos=_pos;} //设置叶节点位置
int rmin() {return *rm.begin();} //获取最小的右端点
int lmax() {return *lm.begin();} //获取最大的左端点
void insert(int x) { //插入一个左端点或右端点,根据 pos 信息自动识别是左还是右
if(x>pos) rm.insert(x);
else lm.insert(x);
}
void erase(int x) { //删除一个左端点或右端点,同理自动调整
if(x>pos) rm.erase(rm.find(x));
else lm.erase(lm.find(x));
}
};
class SegmentTree { //线段树(现在可以开多个),用于处理全局存在正整数数量空 and 区间的最小 or 值
/*
这颗线段树每个节点存储以节点对应区间内任意点为端点的线段(区间)构成集合的
1. 最小的右端点 -> minr
2. 最大的左端点 -> maxl
3. 最小的 or 值 -> minres
*/
private:
vector<Node> nodes;
vector<Leaf> leaves;
int size;
void PUSHUP(int p) {//巧妙的上传原理
nodes[p].minr=min(nodes[p<<1].minr,nodes[p<<1|1].minr);
nodes[p].maxl=max(nodes[p<<1].maxl,nodes[p<<1|1].maxl);
nodes[p].minres=min({nodes[p<<1|1].minr-nodes[p<<1].maxl,
nodes[p<<1].minres,nodes[p<<1|1].minres});
} void MODIFY(int P,int X) { //修改叶子节点后,将树上对应叶子节点与其同步
nodes[P].minr=leaves[X].rmin();
nodes[P].maxl=leaves[X].lmax();
nodes[P].minres=nodes[P].minr-nodes[P].maxl;
} void INSERT(int X,int D,int l,int r,int p) { //单点修改,X 是修改的点,D 是另一端点
if(l>=r) {leaves[X].insert(D);MODIFY(p,X);return ;}
int mid=(l+r)>>1;
if(mid>=X) INSERT(X,D,l,mid,p<<1);
else INSERT(X,D,mid+1,r,p<<1|1);
PUSHUP(p);
}
void ERASE(int X,int D,int l,int r,int p) { //单点删除
if(l>=r) {leaves[X].erase(D);MODIFY(p,X);return ;}
int mid=(l+r)>>1;
if(mid>=X) ERASE(X,D,l,mid,p<<1);
else ERASE(X,D,mid+1,r,p<<1|1);
PUSHUP(p);
}
public:
SegmentTree(int _size) {
nodes.resize((_size<<2)+50);
leaves.resize(_size+50);
size=_size;
for(int i=1;i<=size;i++)
leaves[i].setPos(i);
}
void insert(int X,int D) {INSERT(X,D,1,size,1);} //封装后的函数
void erase(int X,int D) {ERASE(X,D,1,size,1);}
int GetMin() {return nodes[1].minres;} //获取最小值
};
struct Seg { //储存一个线段
int l,r;
Seg(){}
Seg(int l,int r):l(l),r(r){}
};
struct Gseg { //将两线段以左端点为第一关键字(倒序),右端点为第二关键字(顺序)比较
bool operator ()(Seg a,Seg b)
{return a.l==b.l?a.r<b.r:a.l>b.l;}
};
struct Lseg { //将两线段以右端点为第一关键字(顺序),左端点为第二关键字(倒序)比较
bool operator ()(Seg a,Seg b)
{return a.r==b.r?a.l>b.l:a.r<b.r;}
};
SegmentTree seg0(MAXN);
multiset<Seg,Gseg> lf;
multiset<Seg,Gseg>::iterator lfi;
multiset<Seg,Lseg> rf;
multiset<Seg,Lseg>::iterator rfi;
signed main(){
int m=R,lft,rit,op;
while(m--) {
op=R,lft=R,rit=R;
if(op==1) {
lf.insert(Seg(lft,rit));
rf.insert(Seg(lft,rit));
seg0.insert(lft,rit);
seg0.insert(rit,lft);
}else {
lf.erase(lf.find(Seg(lft,rit)));
rf.erase(rf.find(Seg(lft,rit)));
seg0.erase(lft,rit);
seg0.erase(rit,lft);
}
rfi=rf.begin(),lfi=lf.begin();
if((rfi->r)>(lfi->l)) myio::print_int(max(rfi->r,lfi->r)-min(lfi->l,rfi->l)); //如果不存在空区间
else myio::print_int(seg0.GetMin());
}
return 0;
}
思维方式
遇到一个问题,将问题的不必要部分(比如新定义、故事等等)略去,抽象成一个数学模型。再通过一系列转化得到熟悉的问题及其组合。
CodeForces Legendary GM Um_nik 在他的 blog How to read problem statements 中就提出了相同的观点:
ㅤ
- The result of reading the statement is usually pure math model. If story helps to build correct understanding, you can keep it, but still try to discard as many unnecessary details as possible.
- Imagine you want to tell the problem to someone else. What parts you want to tell? (According to my PM, this rule won't help you).
- Shorter = better.
- Simpler = better.
以及:
ㅤ
- Try to find familiar patterns, maybe objects you already know something about. If you are given some connected graph with exactly one simple path between each pair of vertices, it's called tree. 4 letters instead of 12 words, see?
- Try even harder to spot something strange, something you not expecting. That probably will be the cornerstone of the problem.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律