集合问题

题面

题目描述

定义区间 \([l,r](r>l)\) 的长度为 \(r-l\) 。(注意,区间不能有 \(l=r\)

定义两个区间 \(A,B\)\(\rm and\) 为所有满足 \(S\subseteq A\)\(S\subseteq B\) 的区间中的长度最长的区间(这里可以理解为两个区间的交集)。特别的,如果无任何 \(S\) 满足条件,则 \(\rm and\) 为空集。定义空集长度为 \(0\)

定义两个区间 \(A,B\)\(\rm or\) 为所有满足 \(A\subseteq S\)\(B\subseteq S\) 的区间中的长度最短的区间(也就是说这里不可以理解成并集)。

一个区间可重集的 \(\rm and\) 定义为其所有区间的 \(\rm and\) 和,\(\rm or\) 同理。

你要维护一个区间可重集,并进行 \(m\) 次一下两种操作:

  1. 加入区间 \([l,r]\)
  2. 删除区间 \([l,r]\),保证其存在,如果有多个仅删除一个。

每次操作后,你要找到区间可重集中的一个子集(不能是空集),满足这个子集的 \(\rm and\) 最小(当然这个 \(\rm and\) 可以是空集)。在满足 \(\rm and\) 最小的所有可行子集中,你希望找到那个 \(\rm or\) 最小的,并输出其 \(\rm or\) 的长度。

保证任意时刻(初始时除外)集合非空。

数据范围

  • \(m\in\mathbb{Z}\cap[1,5\times 10^5]\)
  • \(l<r\)
  • \(l,r\in\mathbb{Z} \cap[1,10^6]\)

时间限制 3s。

题解

解题思路

问题转化 1

首先我们可以看出,一个区间构成的多重集的 \(\rm and\) 就是多重集内任意两个区间的 \(\rm and\) 的最小值,因为其他区间不对其造成贡献,而任意两个区间都是当前最大多重集的子集,因此只用考虑两个区间,而不考虑更多的。同理可以推广到 \(\rm or\)

现在目标变为:若存在 \(2\) 个以上线段(也就是区间),求存在的线段中,满足某两个线段的交最小的 \(\rm or\) 的最小值。

另外,如果只存在一个线段,当然就选那一个了。

问题转化 2

我们发现若任意两个线段的交都不是空集,这种情况很容易解出,贪心即可。我们开两个 multiset(以下简称 setlfrf。其中 lf 储存所有线段最右的左端点,rf 存最左的右端点,设这两个点分别为 \(lfi\)\(rfi\)。只要 \(lfi<rfi\),则不存在 \(\rm and\) 出空集来,并且 \([lfi,rfi]\) 就是最小的 and。接下来只需找到两点对应的另外一个端点,并求出最小的 \(\rm or\)

但是假如存在空集,问题又变得扑朔迷离起来。因为我们通过以上方法不能得到空集的具体位置,便不能贪心,而 \(\rm or\) 也就不确定了。

问题转化 3

存在空集,意味着不能贪心,也意味着我们只用考虑空集。

问题可以转化成以下描述:

有一个序列 \(A=(A_1,A_2,\dots,A_K)\),还有一个长度一样的序列 \(B=(B_1,B_2,\dots,B_K)\),同时满足 \(\forall i\in[1,K]\cap \mathbb{Z},A_i<i,B_i>i\)。求 \(B_j-A_i(i<j)\) 的最小值。

什么意思呢?在本题,\(K\) 就是值域的上限 \(10^6\)。每个 \(i\) 都是一个整点。\(A_i\) 就是以 \(i\) 为左端点最小的右端点,\(B_i\) 就是以 \(i\) 为右端点最大的左端点。\(i<j\) 又保证了两者的交是空集。因此两个问题是等价的。

解决问题

如何求这个最小值呢?我们发现若要最小,肯定是 \(B_j\) 尽可能小,\(A_i\) 尽可能大了。

因此我们采用线段树。线段树的每个节点存三个信息:\(minr,maxl,minres\)。若该节点对应区间 \([L,R]\),则 \(minr\) 表示以 \([L,R]\) 内某个点为左端点的的线段的右端点构成的集合中最小的那一个(\(B_j\));\(maxl\) 表示以 \([L,R]\) 内某个点为右端点的线段的左端点构成的集合中最大的那一个(\(A_i\));\(minres\) 表示我们要求的最小值。

如何上传呢?\(minr\)\(maxl\) 是比较好更新的,这里略去。对于 \(minres\),我们设 \(minres\) 对应的那个最小区间的两个端点为 \(ln\)\(rn\)。因为要保证 \(i<j\),所以只有三种情况:\(ln,rn\) 都选自左半区间的两个点对应的端点;\(ln,rn\) 都选自右半区间的两个点对应的端点;\(ln\) 选自左区间,\(rn\) 选自右区间。对于前两种情况,那就是子区间的事了,直接引用 \(minres_{p<<1}\) 或者 \(minres_{p<<1|1}\)。对于最后一种情况,\(ln\) 应当是左半区间的 \(lmax\),而 \(rn\) 是右半区间的 \(rmin\)

我们还需要对每个叶子结点进行维护(对数轴直接进行维护)。具体地说,数轴上每个点开两个 set rmlm,分别储存以这个点为左端点的的线段的右端点构成的集合与以这个点为右端点的线段的左端点构成的集合。每一次单点更新到叶子结点时,都要往里面插入对应的值,并且更新树上的值。

最后我们来复盘一下整个过程(插入):

  1. lfrf 插入区间 \([l,r]\),在线段树 \(l\) 处插入 \(r\)\(r\) 处插入 \(l\)
  2. 判断是否存在 \(\rm and\) 空区间。
    1. 如果不存在空区间,就输出求得的最小区间的 \(\rm or\)
    2. 如果存在空区间,输出线段树维护的最小值。

代码实现

#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.
posted @ 2023-03-01 17:02  robinyqc  阅读(76)  评论(0编辑  收藏  举报