2023.1.26 日寄

2023.1.26 日寄

一言

在只能活三个季的蚱蜢眼里,秋季就是生命的尽头,后面是地狱般的寒冬,活着定是无尽的痛苦。
在只能活一天的蜉蝣眼里,池塘之大就是彼之沧海,朝生暮死,不知晦朔,世间值得已全部看尽。
——永生是否是一种酷刑? - 也说的回答 - 知乎 https://www.zhihu.com/question/31756387/answer/2146825423

理论复习内容:数据结构

\(~~~~\) 因为还有陈情表要背,所以今天也是短小但不一定精悍的日寄呢。

TEST_152

题意

\(~~~~\) \(m\) 个操作,对 \(l_i\sim r_i\) 赋值为 \(v_i\)\(q\) 个询问,询问操作 \(l\sim r\) 的操作后序列的和。
\(~~~~\) \(1\leq n,m,q\leq 5\times 10^5\).

题解

\(~~~~\) 考虑扫描线,由于后发生的操作优先级最高,所以我们用右端点来扫描。那把询问挂在右端点上后我们只需要询问 \(\geq l\) 的操作的贡献。

\(~~~~\) 那么我们扫右端点过后加入新的,把覆盖到的区间的贡献更新。用后缀BIT来做就好了。

代码
查看代码
#include <bits/stdc++.h>
#define ll long long
#define PII pair<int,int>
#define mp(a,b) make_pair(a,b)
using namespace std;
template<typename T>void read(T &x)
{
    T f=1;x=0;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') {x=x*10+ch-'0';ch=getchar();}
    x*=f;
}
ll m,n,q,Sum[500005],Ans[500005];
vector <PII> Q[500005];
struct Op{
	ll l,r,v;
	Op(){}
	Op(ll L,ll R,ll V){l=L,r=R,v=V;}
}P[500005];
struct node{//在set内维护颜色段和来自哪个操作 
	ll l,r,v,id;
	node(){}
	node(ll L,ll R,ll V,ll ID){l=L,r=R,v=V,id=ID;}
};
set<node>S;
bool operator<(node a,node b){return a.l<b.l;}
struct BIT{
	ll tr[500005];
	inline ll lowbit(ll x){return x&(-x);}
	void Modify(ll x,ll Val){for(;x;x-=lowbit(x)) tr[x]+=Val;}
	ll Query(ll x){ll ret=0;for(;x<=m;x+=lowbit(x)) ret+=tr[x]; return ret;}
}BIT;
int main() {
//	freopen("1.in","r",stdin);
//	freopen("1.out","w",stdout);
	read(m);read(n);read(q);
	for(ll i=1;i<=m;i++) read(P[i].l),read(P[i].r),read(P[i].v);
	for(ll i=1,l,r;i<=q;i++) read(l),read(r),Q[r].push_back(mp(l,i));
	for(ll i=1,l,r,v;i<=m;i++)
	{
		l=P[i].l,r=P[i].r,v=P[i].v;
		auto it=S.lower_bound(node(l,r,v,i));
		while(it!=S.end())
		{	
			ll Nl,Nr,Nv,Nid;
			Nl=max((*it).l,r+1); Nr=(*it).r;
			Nv=(*it).v; Nid=(*it).id;
			S.erase(it);ll Del=((Nr-Nl+1)-((*it).r-(*it).l+1))*Nv;
			if(Nl<=Nr) {S.insert(node(Nl,Nr,Nv,Nid)),BIT.Modify(Nid,Del);break;}
			else BIT.Modify(Nid,0-((*it).r-(*it).l+1)*(*it).v),it=S.lower_bound(node(l,r,v,i)),Sum[Nid]-=((*it).r-(*it).l+1)*(*it).v;
		}
		it=S.lower_bound(node(l,r,v,i));
		if(it!=S.begin())
		{
			it--;
			if(l<=(*it).r)
			{
				ll Nl1,Nl2,Nr1,Nr2,Nv,Nid;
				Nl1=(*it).l; Nr1=min((*it).r,l-1);
				Nl2=max((*it).l,r+1),Nr2=(*it).r;
				Nv=(*it).v; Nid=(*it).id;
				S.erase(it);
				ll Del=-(((*it).r)-((*it).l)+1)*Nv;
				if(Nl1<=Nr1) S.insert(node(Nl1,Nr1,Nv,Nid)),Del+=(Nr1-Nl1+1)*Nv;
				if(Nl2<=Nr2) S.insert(node(Nl2,Nr2,Nv,Nid)),Del+=(Nr2-Nl2+1)*Nv;
				BIT.Modify(Nid,Del);
			}	
		}
		S.insert(node(l,r,v,i)); BIT.Modify(i,(r-l+1)*v);
		for(ll j=0;j<(int)Q[i].size();j++)
		{
			l=Q[i][j].first;ll id=Q[i][j].second;
			Ans[id]=BIT.Query(l);
		}
	}
	for(ll i=1;i<=q;i++) printf("%lld\n",Ans[i]);
	return 0;
}
/*
西风吹老洞庭波,一夜湘君白发多。
醉后不知天在水,满船清梦压星河。
*/

rmscne

题意

\(~~~~\) \(q\) 次询问求对于一个序列的 \([l,r]\) ,最小的使得 \([l,r]\) 内的元素都出现的 \([l',r']\) 的长度。
\(~~~~\) \(1\leq n,q\leq 2\times 10^6\).

题解

\(~~~~\) 考虑用 Pre ,即某个数上一次出现的下标来刻画一个合法的 \([l',r']\) 区间。

\(~~~~\) 那么就是满足 \(r'\leq r,\min_{i=r'+1}^{r} Pre_i\geq l\),这样就是一个合理的区间。

\(~~~~\) 所以我们还是离线扫描,把询问挂在左端点上,那么通过线段树上维护 \(Pre\) 我们可以二分出合理的右端点区间。

\(~~~~\) 那么我们就只需要查这几个作为右端点的时候最小的合法长度。我们倒着扫描的时候我们就只需要把从这个数到下一个该数之前的一段区间长度都+1即可,也就是区间赋一个等差数列。

代码
查看代码
#include <bits/stdc++.h>
#define PII pair<int,int>
#define mp(a,b) make_pair(a,b)
using namespace std;
template<typename T>void read(T &x)
{
    T f=1;x=0;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') {x=x*10+ch-'0';ch=getchar();}
    x*=f;
}
int arr[2000005],P[2000005],Tmp[2000005],Ans[2000005];
struct SegmentTree{
	#define ls p<<1
	#define rs p<<1|1
	#define lson p<<1,l,mid
	#define rson p<<1|1,mid+1,r
	int tr[8000005],Tag[8000005],Pre[8000005];
	void Build(int p,int l,int r)
	{
		Tag[p]=-1;
		if(l==r) {Pre[p]=P[l];return;}
		int mid=(l+r)>>1;
		Build(lson); Build(rson);
		Pre[p]=min(Pre[ls],Pre[rs]);
	}
	void Change(int p,int l,int x){Tag[p]=x;tr[p]=l-x;}
	void pushDown(int p,int l,int r)
	{
		if(~Tag[p])
		{
			int mid=(l+r)>>1;
			Change(ls,l,Tag[p]); Change(rs,mid+1,Tag[p]);
			Tag[p]=-1;	
		}
	}
	void Modify(int p,int l,int r,int lx,int rx)
	{
		if(lx>rx||l>rx||r<lx) return;
		if(lx<=l&&r<=rx) {Change(p,l,lx-1);return;}
		int mid=(l+r)>>1; pushDown(p,l,r);
		Modify(lson,lx,rx); Modify(rson,lx,rx);
		tr[p]=min(tr[ls],tr[rs]); 
	}
	int Find(int p,int l,int r,int lx,int rx)
	{
		if(rx<l||Pre[p]>=lx) return 0;
		if(l==r) return l;
		int ret=0,mid=(l+r)>>1;pushDown(p,l,r);
		ret=Find(rson,lx,rx);
		if(!ret) return Find(lson,lx,rx);
		else return ret;
	}
	int Query(int p,int l,int r,int lx,int rx)
	{
		if(l>rx||r<lx) return 2e9;
		if(lx<=l&&r<=rx) return tr[p];
		int mid=(l+r)>>1;pushDown(p,l,r);
		return min(Query(lson,lx,rx),Query(rson,lx,rx)); 
	}
	#undef ls
	#undef rs
	#undef lson
	#undef rson
}Seg;
vector<PII>A[2000005];
int main() {
	int n,Q;read(n);
	for(int i=1;i<=n;i++) read(arr[i]),P[i]=Tmp[arr[i]],Tmp[arr[i]]=i;
	read(Q); Seg.Build(1,1,n);
	for(int i=1,l,r;i<=Q;i++) read(l),read(r),A[l].push_back(mp(r,i));
	for(int i=1;i<=n;i++) Tmp[i]=n+1;
	for(int i=n;i>=1;i--)
	{
		int l=i,r=Tmp[arr[i]]-1;
		Seg.Modify(1,1,n,l,r);//所有的这之间的
		Tmp[arr[i]]=i;
		for(int j=0;j<(int)A[i].size();j++)
		{
			int Ql=i,Qr=A[i][j].first,id=A[i][j].second;
			int Nr=Seg.Find(1,1,n,Ql,Qr);
			Ans[id]=Seg.Query(1,1,n,Nr,Qr);
		}
	}
	for(int i=1;i<=Q;i++) printf("%d\n",Ans[i]);
	return 0;
}
/*
西风吹老洞庭波,一夜湘君白发多。
醉后不知天在水,满船清梦压星河。
*/

y-fast trie

题意

\(~~~~\) \(n\) 次操作往集合内插入或删除元素(保证时刻元素不重复),求每次操作后集合内两个元素的和模定值 \(C\) 后的最大值。
\(~~~~\) \(1\leq n\leq 5\times 10^5\).

题解

\(~~~~\) 考虑把每个元素先取模 \(C\) 再加入集合,那么显然是没有影响的,但是注意这样会使集合内存在重复元素。

\(~~~~\) 分成两种情况:

  • \(C\leq x+y\leq 2C\) ,那么这种情况和直接选集合里面最大的两个没什么区别。
  • 否则我们需要找每个数与之“最匹配的数”,换句话说也就是找与 \(C-1-x\) 的前驱。

\(~~~~\) 第一种情况很好处理,但第二种情况显然在最劣时我们会更新 \(\mathcal{O(|S|)}\) 次这种匹配,不优。

\(~~~~\) 打开题解,我们发现对于取数对的这种题目,通常的套路是先 \(\mathcal{O(n^2)}\) 暴力,然后看有多少有用的,并且仅保留那些有用的。

\(~~~~\) 那我们放到这道题来看。我们发现若 \(x\) 匹配 \(y\),\(y\) 匹配 \(z\)\(x<z\) ,那是不是一定有 \(x+y<y+z\) ,也就是说 \((x,y)\) 这对是无用的。那么以此类推,最后我们会推出若 \((y,z)\) 是有用的匹配,那 \(z\) 不能去匹配比 \(y\) 大的数,但也不可能会找到一个比 \(y\) 小的数(如果有这种情况那找 \(y\) 肯定更香),也就是说 \(z\) 也只能匹配 \(y\)。所以所有有用的数对都应该是双向匹配的。(这就是双向奔赴才有意义吗)

\(~~~~\) 所以现在我们只用记录下所有的双向匹配,显然这样的对数是 \(\mathcal{O(n)}\) 的。而在修改时,我们也只需要找到与当前 \(x\) 更好的那个匹配 \(y\)。然后删去原来 \(y\) 的匹配(如果有)。而原来 \(y\) 的匹配 \(z\) 必然不会再产生新的双向匹配。(要么还是 \(y\) ,要么是新加入的 \(x\),但是 \(x\) 已经和 \(y\) 双向匹配了天降的胜利!

\(~~~~\) 删除的时候可以视作重新加入 \(x\) 匹配的那个 \(y\) 的过程,总之与上面类似。

代码
查看代码
#include <bits/stdc++.h>
using namespace std;
template<typename T>void read(T &x)
{
    T f=1;x=0;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') {x=x*10+ch-'0';ch=getchar();}
    x*=f;
}
int n,C;
multiset<int>S,Ans;
inline int Link(int x,bool Type)//Type表示可否是自身 
{
	if(x==-1) return -1;
	auto it=S.upper_bound(C-1-x);
	if(it==S.begin()) return -1; it--;
	if((!Type)&&(*it)==x&&S.count(x)==1)
	{
		if(it==S.begin()) return -1;
		else {it--;return *it;}
	}
	else return *it;
}
void Insert(int x)
{
	if(S.empty()) S.insert(x);
	else
	{
		int y=Link(x,1),z=Link(y,0),w=Link(z,0);
		if(~y&&x>z)
		{
			if(~z&&y==w) Ans.erase(Ans.find(y+z));
			Ans.insert(x+y);
		}
		S.insert(x);
	}
}
void Delete(int x)
{
	S.erase(S.find(x));
	if(S.empty()) return;
	int y=Link(x,1),z=Link(y,0),w=Link(z,0);
	if(~y&&x>z)
	{
		if(~z&&y==w) Ans.insert(y+z);
		Ans.erase(Ans.find(x+y));
	}
}
inline int Query()
{
	int ret=0;
	auto it=prev(S.end());
	if(S.count(*it)>=2) ret=2*(*it)%C;
	else ret=((*it)+(*prev(it)))%C;
	if(!Ans.empty()) ret=max(ret,*prev(Ans.end()));
	return ret;
}
int main() {
	read(n);read(C);
	for(int i=1,op,x,lst=0;i<=n;i++)
	{
		read(op);read(x);x^=lst;
		x%=C;
		if(op==1) Insert(x);
		else Delete(x);
		if(S.size()<2) puts("EE"),lst=0;
		else printf("%d\n",lst=Query());
	}
	return 0;
}
/*
西风吹老洞庭波,一夜湘君白发多。
醉后不知天在水,满船清梦压星河。

题解:维护n^2数对一般都是先暴力,然后做出优化(哪些数对非必要)
可以发现双向匹配才是最优的。 
*/

「北大集训 2021」简单数据结构

题意

\(~~~~\) 三种针对数列 \(\{a_n\}\) 的操作:
\(~~~~\) 1.将所有 \(a_i\) 赋值为 \(\min(a_i,v)\).
\(~~~~\) 2.将所有 \(a_i\) 赋值为 \(a_i+i\).
\(~~~~\) 3.询问 \(\sum_{i=l}^r a_i\).

题解

\(~~~~\) 是嘴巴题,现在没时间写。(看了题解还叫嘴巴题吗

\(~~~~\) 注意到两个操作是全局(一桌四个人看错了四个)

\(~~~~\) 我们记录一下已经做过的 \(2\) 操作数量 \(k\) ,那么最后这个数是 \(a_i+k\times i\),而这个时候取个 \(\min\) 不就相当于在最开始和 \(v-k\times i\) 取个 \(\min\) 吗?这个东西不是单减的吗?

\(~~~~\) 也就是说什么呢?也就是说如果最开始 \(a_i=+\infty\),那是不是就一定会有一段后缀取 \(\min\) 而前面的不变。也就是说取 \(\min\) 实际变成了区间赋值等差数列,这样就好做了。

\(~~~~\) 但问题是我们的 \(a_i\) 有初始值,这个时候可以想到 「JOISC 2020 Day1」扫除,也就是操作一次过后具有某些特殊性质那就找到操作时间然后整体维护开冲。

\(~~~~\) 在这题我们可以整体二分出每个 \(a_i\) 第一次被修改的时候。然后整体维护就维护一些点被锁住,维护那些没有锁的点的和,对没有锁的点覆盖等差数列和对单点开锁。最后复杂度 \(\mathcal{O(n\log^2 n)}\).

\(~~~~\) 先贴一个准备理解的代码 Submissions

posted @ 2023-01-26 21:59  Azazеl  阅读(62)  评论(0编辑  收藏  举报