[NOIP2022] 比赛 题解

[NOIP2022] 比赛 题解


知识点

随机化思想(部分分)、笛卡尔树(部分分)、线段树、分块、线段树分治、莫队二次离线。

题意简述

给定 \(n\) 和两个排列 \(\{a_i\}_1^n,\{b_i\}_1^n\)

现在有 \(Q\) 个询问,每个询问如下:给出 \(l,r\),求

\[\sum_{p=l}^r \sum_{q=p}^r \max_{i=p}^q a_i \max_{j=p}^q b_j \pmod{2^{64}} \\ \]

分析

\(20\%\)

我们首先离线操作,然后枚举 \(p\),前缀和处理出以 \(p\) 为左端点时的所有情况,然后将离线下来的询问放到这里一一检验。

代码

时间复杂度:\(O(n^2+nQ)\),空间复杂度:\(O(n+Q)\)

namespace Subtask1 {
	constexpr int N(3e3+10);
	ull pre[N];
	bool Check() {
		return ID<=5;
	}
	int Cmain() {
		FOR(p,1,n) {
			int ma(0),mb(0);
			pre[p-1]=0;
			FOR(q,p,n)tomax(ma,a[q]),tomax(mb,b[q]),pre[q]=pre[q-1]+1ull*ma*mb;
			FOR(i,1,Q)if(l[i]<=p&&r[i]>=p)ans[i]+=pre[r[i]];
		}
		Print();
		return 0;
	}
}

\(52\%\)

发现 \(Q \le 5\),说明我们可以对每个询问依次处理,那么这时就变成了一个分治套路题。

对于分治区间 \([l,r]\),设中点 \(mid = \lfloor \frac{l+r}2 \rfloor\),左半边 \([l,mid]\) 分别处理出 \(a,b\) 排列的后缀最大值,右半边 \((mid,r]\) 分别处理出 \(a,b\) 排列的前缀最大值,都存在 \(mxa,mxb\) 中。

那么我们考虑在左半边 \([l,mid]\) 中倒序枚举端点 \(i\),那么这时,所有 \([i,j],j\in (mid,r]\)\(a,b\) 最大值分布最多会出现三种情况,且它们是三段连续的区间,那么从左到右列出:

  1. \(a,b\) 最大值都在 \([i,mid]\) 中;

    那么这些区间的答案都是 \(mxa_i \cdot mxb_i\)

  2. \(a,b\) 最大值有且仅有一个在 \([i,mid]\)​ 中;

    1. 如果 \(a\) 最大值在 \([i,mid]\) 中:

      那么这些区间的答案和是 \(mxa_i \cdot \sum mxb_i\)​。

    2. 如果 \(b\) 最大值在 \([i,mid]\) 中:

      那么这些区间的答案和是 \(mxb_i \cdot \sum mxa_i\)

    这些可以尺取处理。

  3. \(a,b\) 最大值没有一个在 \([i,mid]\) 中:

    那么这些区间的答案和是 \(\sum (mxa_i \cdot mxb_i)\)

    这可以提前后缀和处理。

那么搭配尺取、后缀和,就可以做到 \(O(r-l+1)\)

那么单次询问就是 \(O(n\log_2{n})\) 的。

代码

时间复杂度:\(O(Qn\log_2{n})\),空间复杂度:\(O(n+Q)\)

namespace Subtask2 {
	int mxa[N],mxb[N];
	ull suf[N];
	bool Check() {
		return ID<=13;
	}
	ull Sep(int l,int r) {
#define mid ((l+r)>>1)
		if(l==r)return 1ull*a[l]*b[l];
		ull ans(Sep(l,mid)+Sep(mid+1,r));
		mxa[mid]=a[mid],mxa[mid+1]=a[mid+1],mxb[mid]=b[mid],mxb[mid+1]=b[mid+1];
		DOR(i,mid-1,l)mxa[i]=max(mxa[i+1],a[i]),mxb[i]=max(mxb[i+1],b[i]);
		FOR(i,mid+2,r)mxa[i]=max(mxa[i-1],a[i]),mxb[i]=max(mxb[i-1],b[i]);
		suf[r+1]=0;
		DOR(i,r,mid+1)suf[i]=suf[i+1]+1ull*mxa[i]*mxb[i];
		int j(mid+1),k(mid+1);
		ull suma(0),sumb(0);
		DOR(i,mid,l) {
			while(j<=r&&mxa[j]<=mxa[i])suma-=mxa[j],sumb+=mxb[j],++j;
			while(k<=r&&mxb[k]<=mxb[i])suma+=mxa[k],sumb-=mxb[k],++k;
			ans+=1ull*mxa[i]*mxb[i]*(min(j,k)-1-mid)+(j>=k?1ull*mxa[i]*sumb:1ull*mxb[i]*suma)+suf[max(j,k)];
		}
		return ans;
#undef mid
	}
	int Cmain() {
		FOR(i,1,Q)wr(Sep(l[i],r[i]));
		return 0;
	}
}

\(84\%\)

特殊性质 A:保证 \(a\) 是均匀随机生成的 \(n\) 的排列。

特殊性质 B:保证 \(b\) 是均匀随机生成的 \(n\) 的排列。

这里有两个随机的特殊性质,我们考虑如何利用它。看到“均匀随机生成的排列”,我们可以想到随机生成树,它的期望高度是 \(O(\log_2{n})\)

再考虑如何把这个序列和随机生成树联系起来,又可以想到笛卡尔树,我们用排列 \(a\) 可以构建一棵以下标为键值、排列为权值、权值满足大根堆性质的笛卡尔树,它的期望高度是 \(O(\log_2{n})\)。我们想到一种依靠笛卡尔树性质的方法。

预处理

我们先预处理出求区间最大值的 ST 表。

先用单调栈求出 \(a\) 中每个值 \(a_i\) 作为最大值的区间 \([L_i,R_i]\),然后像上面分治的方法一样求出 \(\sum_{p=L_i}^i \sum_{q=i}^{R_i} \max_{j=p}^q a_j \max_{k=p}^q b_k \pmod{2^{64}}\)

我们设 \(sum_x\) 表示 \(\sum_{i=1}^x \sum_{p=L_i}^i \sum_{q=i}^{R_i} \max_{j=p}^q a_j \max_{k=p}^q b_k \pmod{2^{64}}\),就是上面式子的前缀和。

复杂度

这部分总时间复杂度是 \(O(n\log_2{n})\) 的,可以证明。

总复杂度为 \(\sum_{i=1}^n (R_i-L_i+1)\),而 \([L_i,R_i]\) 区间就相当于 \(i\) 在笛卡尔树上的所有子孙结点范围。

那么我们从单个结点的角度想:每个结点有几个祖先,它就会被加几次,那么就是它的深度。

所以复杂度就为 \(\sum_{i=1}^n dep_i\),即 \(O(n\log_2{n})\),因为整棵树期望深度为 \(O(\log_2{n})\) 的。

代码

时间复杂度:\(O(n\log_2{n})\),空间复杂度:\(O(n\log_2{n})\)

int Lg[N];
int mxi[N][lV];
ull sum[N];
vector<ull> lef[N];
int Mx(int u,int v) {
	return a[u]>a[v]?u:v;
}
int Mxi(int l,int r) {
	int x(Lg[r-l+1]);
	return Mx(mxi[l][x],mxi[r-(1<<x)+1][x]);
}
Lg[0]=-1;
FOR(i,1,n)Lg[i]=Lg[i>>1]+1;
FOR(i,1,n)mxi[i][0]=i;
FOR(j,1,lN)FOR(i,1,n-(1<<j)+1)mxi[i][j]=Mx(mxi[i][j-1],mxi[i+(1<<(j-1))][j-1]);
FOR(i,1,n)A.push(i,a,[&](int idx) -> void { R[idx]=i-1; }),L[i]=A[A.n-1]+1;
while(!A.empty())R[A.top()]=n,A.pop();
FOR(i,1,n) {
	int lmx(b[i]);
	lef[i]=vector<ull> (i-L[i]+1,b[i]);
	FOR(j,1,i-L[i])lef[i][j]=lef[i][j-1]+tomax(lmx,b[i-j]);
	int it(0),rmx(0);
	lmx=0;
	FOR(j,0,R[i]-i) {
		tomax(rmx,b[i+j]);
		while(it<=i-L[i]&&max(b[i-it],lmx)<rmx)tomax(lmx,b[i-it]),++it;
		sum[i]+=1ull*it*rmx+lef[i].back()-(it?lef[i][it-1]:0);
	}
	sum[i]*=a[i];
}
FOR(i,1,n)sum[i]+=sum[i-1];

离线分解询问

我们发现对于一个询问 \(l,r\),以及一个 \(a_i\),当 \(l \le L_i \land R_i \le R\) 时,我们可以直接把 \(\sum_{p=L_i}^i \sum_{q=i}^{R_i} \max_{j=p}^q a_j \max_{k=p}^q b_k \pmod{2^{64}}\) 加到这个询问的答案中,否则我们就需要重新处理。

那么我们从询问 \(l,r\) 中的最大值 \(a_u\) 开始在笛卡尔树上向两侧递归(这里的两侧指的是一开始 \([l,r]\) 的两边),我们发现只有递归区间的最大值有可能有重新处理的区间,那么我们离线下来,其余的可以用我们上面处理出来的 \(sum\) 数组累加上去。

那么单个询问最多会被分解 \(O(\log_2{n})\) 次,总共产生 \(O(Q\log_2{n})\) 个离线处理区间。

代码

时空复杂度:\(O(Q\log_2{n})\)

vector<Tiii> Que[N];
template<bool lef,bool rig>void dfs(int u,int l,int r,int idx) {
	Que[u].push_back(Tiii(l,r,idx));
	if(lef&&l<u) {
		int ls(Mxi(l,u-1));
		ans[idx]+=(sum[u-1]-sum[ls]),dfs<1,0>(ls,l,u-1,idx);
	}
	if(rig&&u<r) {
		int rs(Mxi(u+1,r));
		ans[idx]+=(sum[rs-1]-sum[u]),dfs<0,1>(rs,u+1,r,idx);
	}
}
FOR(i,1,Q)dfs<1,1>(Mxi(l[i],r[i]),l[i],r[i],i);

处理离线询问

我们对于每个 \(a_i\) 都独自进行一遍处理。枚举右端点,仍旧是尺取维护左右大小关系,用支持区间修改和区间查询的树状数组维护左端点对应的值,取大的值加上,还是类似分治的思想,只不过这次只有 \(b_i\) 的最大值需要维护。

代码

时间复杂度:\(O(n\log_2^2{n}+Q\log_2{n}\log_2{Q})\),空间复杂度:\(O(n)\)

struct BIT2 {
	int n;
	struct BIT {
#define lowbit(i) ((i)&-(i))
		int n;
		ull c[N];
		void Clear() {
			RCL(c+1,0,ull,n);
		}
		void Plus(int x,ull d) {
			if(x>0)for(; x<=n; x+=lowbit(x)) c[x]+=d;
		}
		ull Sum(int x) {
			ull ans(0);
			if(x>0)for(; x; x&=x-1)ans+=c[x];
			return ans;
		}
#undef lowbit
	} bit[2];
	void Init(int _n) {
		n=bit[0].n=bit[1].n=_n,bit[0].Clear(),bit[1].Clear();
	}
	void Plus(int x,ull d) {
		bit[0].Plus(x,d),bit[1].Plus(x,d*x);
	}
	void Plus(int l,int r,ull d) {
		Plus(++l,d),Plus((++r)+1,-d);
	}
	ull Sum(int x) {
		return (x+1)*bit[0].Sum(x)-bit[1].Sum(x);
	}
	ull Sum(int l,int r) {
		return Sum(++r)-Sum((++l)-1);
	}
} bit;
FOR(i,1,n) {
	int it(0),qit(0),lmx(0),rmx(0);
	bit.Init(i-L[i]+1),sort(Que[i].begin(),Que[i].end(),[&](Tiii a,Tiii b) {
		return get<1>(a)<get<1>(b);
	});
	FOR(j,0,R[i]-i) {
		tomax(rmx,b[i+j]);
		while(it<=i-L[i]&&max(b[i-it],lmx)<rmx)tomax(lmx,b[i-it]),bit.Plus(it,it,1ull*lmx*j),++it;
		if(it)bit.Plus(0,it-1,rmx);
		while(qit<(int)Que[i].size()&&get<1>(Que[i][qit])==i+j) {
			int &l(get<0>(Que[i][qit]));
			if(it)ans[get<2>(Que[i][qit])]+=1ull*a[i]*bit.Sum(0,min(it-1,i-l));
			if(it<=i-l)ans[get<2>(Que[i][qit])]+=1ull*a[i]*(j+1)*(lef[i][i-l]-(it?lef[i][it-1]:0));
			++qit;
		}
	}
}

代码

时间复杂度:\(O(n\log_2^2{n}+Q\log_2{n}\log_2{Q})\),空间复杂度:\(O(n\log_2{n}+Q\log_2{n})\)

namespace Subtask2 {
	int Lg[N];
	int mxi[N][lV];
	ull sum[N];
	vector<ull> lef[N];
	vector<Tiii> Que[N];
	struct BIT2 {
		int n;
		struct BIT {
#define lowbit(i) ((i)&-(i))
			int n;
			ull c[N];
			void Clear() {
				RCL(c+1,0,ull,n);
			}
			void Plus(int x,ull d) {
				if(x>0)for(; x<=n; x+=lowbit(x)) c[x]+=d;
			}
			ull Sum(int x) {
				ull ans(0);
				if(x>0)for(; x; x&=x-1)ans+=c[x];
				return ans;
			}
#undef lowbit
		} bit[2];
		void Init(int _n) {
			n=bit[0].n=bit[1].n=_n,bit[0].Clear(),bit[1].Clear();
		}
		void Plus(int x,ull d) {
			bit[0].Plus(x,d),bit[1].Plus(x,d*x);
		}
		void Plus(int l,int r,ull d) {
			Plus(++l,d),Plus((++r)+1,-d);
		}
		ull Sum(int x) {
			return (x+1)*bit[0].Sum(x)-bit[1].Sum(x);
		}
		ull Sum(int l,int r) {
			return Sum(++r)-Sum((++l)-1);
		}
	} bit;
	bool Check() {
		return ID<=21;
	}
	int Mx(int u,int v) {
		return a[u]>a[v]?u:v;
	}
	int Mxi(int l,int r) {
		int x(Lg[r-l+1]);
		return Mx(mxi[l][x],mxi[r-(1<<x)+1][x]);
	}
	template<bool lef,bool rig>void dfs(int u,int l,int r,int idx) {
		Que[u].push_back(Tiii(l,r,idx));
		if(lef&&l<u) {
			int ls(Mxi(l,u-1));
			ans[idx]+=(sum[u-1]-sum[ls]),dfs<1,0>(ls,l,u-1,idx);
		}
		if(rig&&u<r) {
			int rs(Mxi(u+1,r));
			ans[idx]+=(sum[rs-1]-sum[u]),dfs<0,1>(rs,u+1,r,idx);
		}
	}
	int Cmain() {
		Lg[0]=-1;
		FOR(i,1,n)Lg[i]=Lg[i>>1]+1;
		FOR(i,1,n)mxi[i][0]=i;
		FOR(j,1,lN)FOR(i,1,n-(1<<j)+1)mxi[i][j]=Mx(mxi[i][j-1],mxi[i+(1<<(j-1))][j-1]);
		FOR(i,1,n)A.push(i,a,[&](int idx) -> void { R[idx]=i-1; }),L[i]=A[A.n-1]+1;
		while(!A.empty())R[A.top()]=n,A.pop();
		FOR(i,1,n) {
			int lmx(b[i]),rmx(0),it(0);
			lef[i]=vector<ull> (i-L[i]+1,b[i]);
			FOR(j,1,i-L[i])lef[i][j]=lef[i][j-1]+tomax(lmx,b[i-j]);
			lmx=0;
			FOR(j,0,R[i]-i) {
				tomax(rmx,b[i+j]);
				while(it<=i-L[i]&&max(b[i-it],lmx)<rmx)tomax(lmx,b[i-it]),++it;
				sum[i]+=1ull*it*rmx+lef[i].back()-(it?lef[i][it-1]:0);
			}
			sum[i]*=a[i];
		}
		FOR(i,1,n)sum[i]+=sum[i-1];
		FOR(i,1,Q)dfs<1,1>(Mxi(l[i],r[i]),l[i],r[i],i);
		FOR(i,1,n) {
			int it(0),qit(0),lmx(0),rmx(0);
			bit.Init(i-L[i]+1),sort(Que[i].begin(),Que[i].end(),[&](Tiii a,Tiii b) {
				return get<1>(a)<get<1>(b);
			});
			FOR(j,0,R[i]-i) {
				tomax(rmx,b[i+j]);
				while(it<=i-L[i]&&max(b[i-it],lmx)<rmx)tomax(lmx,b[i-it]),bit.Plus(it,it,1ull*lmx*j),++it;
				if(it)bit.Plus(0,it-1,rmx);
				while(qit<(int)Que[i].size()&&get<1>(Que[i][qit])==i+j) {
					int &l(get<0>(Que[i][qit]));
					if(it)ans[get<2>(Que[i][qit])]+=1ull*a[i]*bit.Sum(0,min(it-1,i-l));
					if(it<=i-l)ans[get<2>(Que[i][qit])]+=1ull*a[i]*(j+1)*(lef[i][i-l]-(it?lef[i][it-1]:0));
					++qit;
				}
			}
		}
		FOR(i,1,Q)wr(ans[i]);
		return 0;
	}
}

\(100\%\)

正解有四种做法:线段树、分块、线段树分治、莫队二次离线。

莫队二次离线

这种方法复杂度最劣,实测最劣,代码最长,如果不是没办法,实在不建议使用。

设:

\[f(l,r) = \sum_{p=l}^r\sum_{q=p}^r \max_{i=p}^q a_i \max_{j=p}^q b_j \\ g(x,r) = \sum_{i=1}^x f(i,r) , h(l,x) = \sum_{i=x}^r f(l,i) \\ \]

对于一个莫队区间 \([l,r]\) 的移动,那么分类讨论:(\(\gets\) 表示 +=

  1. \(r\) 的移动:移动到 \(R\)

    1. \(r = R\):不修改;

    2. \(r < R\)

      \[\begin{aligned} sum & \gets \sum_{i=r+1}^{R} [h(i,i) - h(l-1,i)] \\ sum & \gets \sum_{i=r+1}^{R} h(i,i) - \sum_{i=r+1}^{R} h(l-1,i) \\ \end{aligned} \]

    3. \(r > R\)

      \[\begin{aligned} sum & \gets - \sum_{i=R}^{r-1} [h(i,i) - h(l-1,i)] \\ sum & \gets - \sum_{i=R}^{r-1} h(i,i) + \sum_{i=R}^{r-1} h(l-1,i) \\ \end{aligned} \]

  2. \(l\) 的移动:移动到 \(L\)

    1. \(l = L\):不修改;

    2. \(l > L\)

      \[\begin{aligned} sum & \gets \sum_{i=L}^{l-1} [g(i,i) - g(i,r+1)] \\ sum & \gets \sum_{i=L}^{l-1} g(i,i) - \sum_{i=L}^{l-1} g(i,r+1) \\ \end{aligned} \]

    3. \(l < L\)

      \[\begin{aligned} sum & \gets - \sum_{i=l+1}^{L} [g(i,i) - g(i,r+1)] \\ sum & \gets - \sum_{i=l+1}^{L} g(i,i) + \sum_{i=l+1}^{L} g(i,r+1) \\ \end{aligned} \]

我们对于所有的移动都能分解成两部分:

  1. \(\sum_{i=x}^y h(i,i)\)\(\sum_{i=x}^y g(i,i)\)
  2. \(\sum_{i=x}^y h(z,i)\)\(\sum_{i=x}^y g(i,z)\),其中 \(z\) 固定。

那么我们现在的任务很明显就是要尝试维护 \(h(x,y)\)\(g(x,y)\),并且要实现 \(O(1)\) 的查询,那么显然使用分块。

以维护 \(h(x,y)\) 为示例:我们枚举右端点 \(y\),每次单调栈配合区间覆盖更新,这样就可以做到 \(O(\sqrt{n})\) 分别更新所有整块和某一单块。维护 \(g(x,y)\) 同理。

我们现在就可以获得所有 \(h(x,y),x \in [1,y]\),但是我们没办法直接一次性地查询 \(\sum_{i=x}^y h(z,i)\),所以要分开一个个来查询,但是如果全部存下来实测空间开不下。

那么我们只能开一个链表,把二次离线下来的询问的第 2 部分装进链表,询问时遍历,在范围内就加入,不在范围内就删掉,可以将空间降到 \(O(Q)\),而时间复杂度不变。

询问的第 1 部分在处理二次离线的时候把它们做一个前缀和还有后缀和,最后再做一遍莫队,然后加上就可以。

代码

时间复杂度:\(O(n\sqrt{n} + n\sqrt{Q})\),空间复杂度:\(O(n+Q)\)

#define Plus_Cat "match"
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define Fi first
#define Se second
#define ll long long
#define ull unsigned long long
#define Tiii tuple<int,int,int>
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define FOR(i,a,b) for(int i(a);i<=(int)(b);++i)
#define DOR(i,a,b) for(int i(a);i>=(int)(b);--i)
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
using namespace std;
namespace IOstream {
#define getc() getchar()
#define putc(c) putchar(c)
#define isdigit(c) ('0'<=(c)&&(c)<='9')
	template<class T>void rd(T &x) {
		static char ch(0);
		for(x=0,ch=getc(); !isdigit(ch); ch=getc());
		for(; isdigit(ch); x=(x<<1)+(x<<3)+(ch^48),ch=getc());
	}
	template<class T>void wr(T x,const char End='\n') {
		static int top(0);
		static int st[50];
		do st[++top]=x%10,x/=10;
		while(x);
		while(top)putc(st[top]^48),--top;
		putc(End);
	}
} using namespace IOstream;
constexpr int N(2.5e5+10),sN(5e2+10),lN(17),lV(lN+1);
int ID,n,Q;
int a[N],b[N],ql[N],qr[N];
ull ans[N];
struct Stack {
	int n;
	int st[N];
	int &operator [](int i) {
		return st[i];
	}
	int top() {
		return st[n];
	}
	bool empty() {
		return !n;
	}
	void clear(int x) {
		st[n=0]=x;
	}
	template<class T>void push(int i,int *a,T func) {
		while(n&&a[i]>a[st[n]])--n;
		func(st[n]),st[++n]=i;
	}
	void pop() {
		--n;
	}
} A,B;
namespace Subtask_Twice_Offline_Mo {
	int Bl,Bn;
	int st[N],en[N],idx[N],qry[N];
	ull pre[N],suf[N];
	struct Secondary_Query {
		int lim,x,idx;
	};
	vector<Secondary_Query> Ql[N],Qr[N];
	list<Secondary_Query> QL,QR;
	struct DB {
		int Bl,Bn;
		int st[N],en[N],idx[N];
		int set[N][2],val[N][2];
		ull Bsum[N];
		ull sum[N][3];
		/*Clear*/
		void Clear() {
			RCL(set,0,set,1),RCL(val,0,val,1),RCL(sum,0,sum,1),RCL(Bsum,0,Bsum,1);
		}
		/*Init*/
		void Init(int n) {
			Bl=ceil(sqrt(n)),Bn=(n-1)/Bl+1;
			FOR(i,1,Bn) {
				st[i]=en[i-1]+1,en[i]=min(n,en[i-1]+Bl);
				FOR(j,st[i],en[i])idx[j]=i;
			}
		}
		void Pre_Down(int u) {
			if(set[u][0]) {
				FOR(i,st[u],en[u]) {
					val[i][0]=set[u][0];
					sum[i][0]=1ull*(i-st[u]+1)*set[u][0],sum[i][2]=1ull*sum[i][1]*set[u][0];
				}
				set[u][0]=0;
			}
			if(set[u][1]) {
				FOR(i,st[u],en[u]) {
					val[i][1]=set[u][1];
					sum[i][1]=1ull*(i-st[u]+1)*set[u][1],sum[i][2]=1ull*sum[i][0]*set[u][1];
				}
				set[u][1]=0;
			}
		}
		void Suf_Down(int u) {
			if(set[u][0]) {
				DOR(i,en[u],st[u]) {
					val[i][0]=set[u][0];
					sum[i][0]=1ull*(en[u]-i+1)*set[u][0],sum[i][2]=1ull*sum[i][1]*set[u][0];
				}
				set[u][0]=0;
			}
			if(set[u][1]) {
				DOR(i,en[u],st[u]) {
					val[i][1]=set[u][1];
					sum[i][1]=1ull*(en[u]-i+1)*set[u][1],sum[i][2]=1ull*sum[i][0]*set[u][1];
				}
				set[u][1]=0;
			}
		}
		template<const bool t>void Pre_reset(int u,int l,int r,int d) {
			Pre_Down(u);
			FOR(i,l,en[u]) {
				sum[i][t]=(i>st[u]?sum[i-1][t]:0)+(i<=r?val[i][t]=d:val[i][t]);
				sum[i][2]=(i>st[u]?sum[i-1][2]:0)+1ull*val[i][0]*val[i][1];
			}
			FOR(i,u,Bn)Bsum[i]=Bsum[i-1]+sum[en[i]][2];
		}
		template<const bool t>void Suf_reset(int u,int l,int r,int d) {
			Suf_Down(u);
			DOR(i,r,st[u]) {
				sum[i][t]=(i<en[u]?sum[i+1][t]:0)+(i>=l?val[i][t]=d:val[i][t]);
				sum[i][2]=(i<en[u]?sum[i+1][2]:0)+1ull*val[i][0]*val[i][1];
			}
			DOR(i,u,1)Bsum[i]=Bsum[i+1]+sum[st[i]][2];
		}
		template<const bool t>void Pre_Reset(int l,int r,int d) {
			int bl(idx[l]),br(idx[r]);
			if(bl==br)return Pre_reset<t>(bl,l,r,d);
			Pre_reset<t>(bl,l,en[bl],d);
			FOR(i,bl+1,br-1) {
				sum[en[i]][t]=1ull*(set[i][t]=d)*(en[i]-st[i]+1);
				Bsum[i]=Bsum[i-1]+(sum[en[i]][2]=1ull*set[i][t]*sum[en[i]][t^1]);
			}
			Pre_reset<t>(br,st[br],r,d);
		}
		template<const bool t>void Suf_Reset(int l,int r,int d) {
			int bl(idx[l]),br(idx[r]);
			if(bl==br)return Suf_reset<t>(bl,l,r,d);
			Suf_reset<t>(br,st[br],r,d);
			DOR(i,br-1,bl+1) {
				sum[st[i]][t]=1ull*(set[i][t]=d)*(en[i]-st[i]+1);
				Bsum[i]=Bsum[i+1]+(sum[st[i]][2]=1ull*set[i][t]*sum[st[i]][t^1]);
			}
			Suf_reset<t>(bl,l,en[bl],d);
		}
		ull Pre_Sum(int x) {
			int u(idx[x]);
			if(set[u][0]&&set[u][1])return Bsum[u-1]+1ull*set[u][0]*set[u][1]*(x-st[u]+1);
			if(set[u][1])return Bsum[u-1]+1ull*set[u][1]*sum[x][0];
			if(set[u][0])return Bsum[u-1]+1ull*set[u][0]*sum[x][1];
			return Bsum[u-1]+sum[x][2];
		}
		ull Suf_Sum(int x) {
			int u(idx[x]);
			if(set[u][0]&&set[u][1])return Bsum[u+1]+1ull*set[u][0]*set[u][1]*(en[u]-x+1);
			if(set[u][1])return Bsum[u+1]+1ull*set[u][1]*sum[x][0];
			if(set[u][0])return Bsum[u+1]+1ull*set[u][0]*sum[x][1];
			return Bsum[u+1]+sum[x][2];
		}
	} D;
	int Cmain() {
		/*Init*/
		Bl=ceil(1.0*n/sqrt(Q)),Bn=(n-1)/Bl+1;
		FOR(i,1,Bn) {
			st[i]=en[i-1]+1,en[i]=min(n,en[i-1]+Bl);
			FOR(j,st[i],en[i])idx[j]=i;
		}
		/*Sort*/
		FOR(i,1,Q)qry[i]=i;
		sort(qry+1,qry+Q+1,[&](int x,int y) {
			return idx[ql[x]]^idx[ql[y]]?idx[ql[x]]<idx[ql[y]]:(idx[ql[x]]&1?qr[x]<qr[y]:qr[x]>qr[y]);
		});
		/*Secondary Offline*/
		int l(1),r(0);
		FOR(i,1,Q) {
			const int &L(ql[qry[i]]),&R(qr[qry[i]]);
			if(r<R)Qr[r+1].push_back({R,l-1,-qry[i]}),r=R;
			if(l>L)Ql[l-1].push_back({L,r+1,-qry[i]}),l=L;
			if(r>R)Qr[R+1].push_back({r,l-1,qry[i]}),r=R;
			if(l<L)Ql[L-1].push_back({l,r+1,qry[i]}),l=L;
		}
		/*Solve Offline Queries*/
		D.Init(n);
		//Prefix
		A.clear(0),B.clear(0),D.Clear();
		FOR(i,1,n) {
			A.push(i,a,[&](int idx) {D.Pre_Reset<0>(idx+1,i,a[i]);});
			B.push(i,b,[&](int idx) {D.Pre_Reset<1>(idx+1,i,b[i]);});
			for(const Secondary_Query &x:Qr[i])if(x.x>=1)QR.push_back(x);
			for(auto it(QR.begin()); it!=QR.end(); ++it) {
				while(it!=QR.end()&&it->lim<i)QR.erase(it++);
				if(it==QR.end())break;
				it->idx>0?ans[it->idx]+=D.Pre_Sum(it->x):ans[-it->idx]-=D.Pre_Sum(it->x);
			}
			Qr[i].clear(),pre[i]=pre[i-1]+D.Pre_Sum(i);
		}
		QR.clear();
		//Suffix
		A.clear(n+1),B.clear(n+1),D.Clear();
		DOR(i,n,1) {
			A.push(i,a,[&](int idx) {D.Suf_Reset<0>(i,idx-1,a[i]);});
			B.push(i,b,[&](int idx) {D.Suf_Reset<1>(i,idx-1,b[i]);});
			for(const Secondary_Query &x:Ql[i])if(x.x<=n)QL.push_back(x);
			for(auto it(QL.begin()); it!=QL.end(); ++it) {
				while(it!=QL.end()&&it->lim>i)QL.erase(it++);
				if(it==QL.end())break;
				it->idx>0?ans[it->idx]+=D.Suf_Sum(it->x):ans[-it->idx]-=D.Suf_Sum(it->x);
			}
			Ql[i].clear(),suf[i]=suf[i+1]+D.Suf_Sum(i);
		}
		QL.clear();
		//Again
		l=1,r=0;
		FOR(i,1,Q) {
			const int &L(ql[qry[i]]),&R(qr[qry[i]]);
			if(r<R)ans[qry[i]]+=pre[R]-pre[r],r=R;
			if(l>L)ans[qry[i]]+=suf[L]-suf[l],l=L;
			if(r>R)ans[qry[i]]-=pre[r]-pre[R],r=R;
			if(l<L)ans[qry[i]]-=suf[l]-suf[L],l=L;
		}
		/*Count*/
		FOR(i,1,Q)ans[qry[i]]+=ans[qry[i-1]];
		/*Print*/
		FOR(i,1,Q)wr(ans[i]);
		return 0;
	}
}
int main() {
#ifdef Plus_Cat
	freopen(Plus_Cat ".in","r",stdin),freopen(Plus_Cat ".out","w",stdout);
#endif
	rd(ID),rd(n);
	FOR(i,1,n)rd(b[i]);
	FOR(i,1,n)rd(a[i]);
	rd(Q);
	FOR(i,1,Q)rd(ql[i]),rd(qr[i]);
	return Subtask_Twice_Offline_Mo::Cmain();
}

线段树分治

线段树分治时间复杂度不高,但常数略大,也有些难想。

下挂询问

我们考虑把询问分解到一棵线段树上,并且在一个询问能分解到某个区间的两个子区间时,也一并挂入这个区间。

代码

时空复杂度:\(O(Q\log_2{n})\)

vector<int> Que[N];
void Hang(const int &idx,int l=1,int r=n,int p=1) {
	if(ql[idx]<=l&&r<=qr[idx])return Que[p].push_back(idx);
	if(ql[idx]<=mid)Hang(idx,l,mid,ls);
	if(mid<qr[idx])Hang(idx,mid+1,r,rs);
	if(ql[idx]<=mid&&mid<qr[idx])Que[p].push_back(idx);
}
FOR(i,1,Q)Hang(i);
分治

假设现在在编号为 \(p\),范围为 \([l,r]\) 的分治区间。

  1. 判断是否退出,否则先递归分治子区间,并且把它们的和加到自己区间的答案 \(res\) 中。

    ull Sep(int l=1,int r=n,int p=1) {
    	if(l==r) {
    		for(int idx:Que[p])ans[idx]+=1ull*a[l]*b[l];
    		return 1ull*a[l]*b[l];
    	}
        ull res(Sep(l,mid,ls)+Sep(mid+1,r,rs));
        //...
    }
    
  2. 再把那些不是完全包含这个区间的询问挂到对应的左右端点。

    ull Sep(int l=1,int r=n,int p=1) {
        //...
    	FOR(i,l,r)que[i].clear();
    	for(int idx:Que[p])if(l<ql[idx]||qr[idx]<r)
            que[max(l,ql[idx])].push_back(idx),que[min(r,qr[idx])].push_back(idx);
        //...
    }
    
  3. 然后处理左区间 \([l,mid]\)\(a,b\) 的后缀最大值,右区间 \((mid,r]\)\(a,b\) 的前缀最大值。

    ull Sep(int l=1,int r=n,int p=1) {
        //...
        mxa[mid]=a[mid],mxa[mid+1]=a[mid+1],mxb[mid]=b[mid],mxb[mid+1]=b[mid+1];
    	DOR(i,mid-1,l)mxa[i]=max(mxa[i+1],a[i]),mxb[i]=max(mxb[i+1],b[i]);
    	FOR(i,mid+2,r)mxa[i]=max(mxa[i-1],a[i]),mxb[i]=max(mxb[i-1],b[i]);
        //...
    }
    
  4. 为了保证正确性,我们得让 \(a_i\) 的最大值始终在端点上,这样才有一个正确的做法;同时,又为了保证完整性,我们还需要分别枚举右区间 \((mid,r]\) 中右端点和枚举左区间 \([l,mid]\) 中左端点,在两边都处理一遍挂上去的询问。

    这里需要开两棵历史版本和的线段树,为了减少时间复杂度,我们提前把里面开好标记,就是把 \(mxb_i\) 放进去,然后更新的时候只需要打上标记就好了。

    最后不能忘记把剩下的询问也统计了。

    struct SEG {
    	struct node {
    		ull cnt,sum,tag;
    		node(ull cnt=0,ull sum=0,ull tag=0):cnt(cnt),sum(sum),tag(tag) {}
    		void down(ull Tag) {
    			sum+=cnt*Tag,tag+=Tag;
    		}
    	} tr[N<<2];
    	void Up(int p) {
    		tr[p].sum=tr[ls].sum+tr[rs].sum;
    	}
    	void Down(int p) {
    		if(tr[p].tag)tr[ls].down(tr[p].tag),tr[rs].down(tr[p].tag),tr[p].tag=0;
    	}
    	template<const bool ty>void Build(int l,int r,int p=1) {
    		tr[p]=node();
    		if(l==r)return tr[p].cnt=(ty?mxb[l]:1),void();
    		Build<ty>(l,mid,ls),Build<ty>(mid+1,r,rs),tr[p].cnt=tr[ls].cnt+tr[rs].cnt;
    	}
    	void Mark(int L,int R,ull d,int l,int r,int p=1) {
    		if(L<=l&&r<=R)return tr[p].down(d);
    		Down(p);
    		if(L<=mid)Mark(L,R,d,l,mid,ls);
    		if(mid<R)Mark(L,R,d,mid+1,r,rs);
    		Up(p);
    	}
    	ull Sum(int L,int R,int l,int r,int p=1) {
    		if(L<=l&&r<=R)return tr[p].sum;
    		return Down(p),(L<=mid?Sum(L,R,l,mid,ls):0)+(mid<R?Sum(L,R,mid+1,r,rs):0);
    	}
    } seg[2];
    
    ull Sep(int l=1,int r=n,int p=1) {
        //...
        	int j(mid+1),k(mid+1);
    	seg[0].Build<0>(l,mid),seg[1].Build<1>(l,mid);
    	FOR(i,mid+1,r) {
    		while(j>l&&mxa[j-1]<mxa[i])--j;
    		while(k>j&&mxb[k-1]<mxb[i])--k;
    		if(k<=mid)seg[0].Mark(k,mid,1ull*mxa[i]*mxb[i],l,mid);
    		if(j<k)seg[1].Mark(j,k-1,mxa[i],l,mid);
    		for(int idx:que[i])
    			ans[idx]+=seg[0].Sum(max(l,ql[idx]),mid,l,mid)+seg[1].Sum(max(l,ql[idx]),mid,l,mid);
    	}
    	res+=seg[0].Sum(l,mid,l,mid)+seg[1].Sum(l,mid,l,mid);
    	j=mid,k=mid,seg[0].Build<0>(mid+1,r),seg[1].Build<1>(mid+1,r);
    	DOR(i,mid,l) {
    		while(j<r&&mxa[j+1]<mxa[i])++j;
    		while(k<j&&mxb[k+1]<mxb[i])++k;
    		if(k>mid)seg[0].Mark(mid+1,k,1ull*mxa[i]*mxb[i],mid+1,r);
    		if(j>k)seg[1].Mark(k+1,j,mxa[i],mid+1,r);
    		for(int idx:que[i])
    			ans[idx]+=seg[0].Sum(mid+1,min(r,qr[idx]),mid+1,r)+seg[1].Sum(mid+1,min(r,qr[idx]),mid+1,r);
    	}
    	res+=seg[0].Sum(mid+1,r,mid+1,r)+seg[1].Sum(mid+1,r,mid+1,r);
    	for(int idx:Que[p])if(ql[idx]<=l&&r<=qr[idx])ans[idx]+=res;
    	return res;
    }
    
代码

时间复杂度:\(O(n\log_2^2{n}+Q\log_2^2{n})\),空间复杂度:\(O(n+Q\log_2{n})\)

#define Plus_Cat "match"
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define Fi first
#define Se second
#define ll long long
#define ull unsigned long long
#define Tiii tuple<int,int,int>
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define FOR(i,a,b) for(int i(a);i<=(int)(b);++i)
#define DOR(i,a,b) for(int i(a);i>=(int)(b);--i)
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
using namespace std;
namespace IOstream {
#define getc() getchar()
#define putc(c) putchar(c)
#define isdigit(c) ('0'<=(c)&&(c)<='9')
	template<class T>void rd(T &x) {
		static char ch(0);
		for(x=0,ch=getc(); !isdigit(ch); ch=getc());
		for(; isdigit(ch); x=(x<<1)+(x<<3)+(ch^48),ch=getc());
	}
	template<class T>void wr(T x,const char End='\n') {
		static int top(0);
		static int st[50];
		do st[++top]=x%10,x/=10;
		while(x);
		while(top)putc(st[top]^48),--top;
		putc(End);
	}
} using namespace IOstream;
constexpr int N(2.5e5+10),sN(5e2+10),lN(17),lV(lN+1);
int ID,n,Q;
int a[N],b[N],ql[N],qr[N],mxa[N],mxb[N];
vector<int> Que[N<<2];
ull ans[N];
namespace Subtask_Segment_Tree_Separation {
	vector<int> que[N];
#define ls (p<<1)
#define rs (p<<1|1)
#define mid ((l+r)>>1)
	struct SEG {
		struct node {
			ull cnt,sum,tag;
			node(ull cnt=0,ull sum=0,ull tag=0):cnt(cnt),sum(sum),tag(tag) {}
			void down(ull Tag) {
				sum+=cnt*Tag,tag+=Tag;
			}
		} tr[N<<2];
		void Up(int p) {
			tr[p].sum=tr[ls].sum+tr[rs].sum;
		}
		void Down(int p) {
			if(tr[p].tag)tr[ls].down(tr[p].tag),tr[rs].down(tr[p].tag),tr[p].tag=0;
		}
		template<const bool ty>void Build(int l,int r,int p=1) {
			tr[p]=node();
			if(l==r)return tr[p].cnt=(ty?mxb[l]:1),void();
			Build<ty>(l,mid,ls),Build<ty>(mid+1,r,rs),tr[p].cnt=tr[ls].cnt+tr[rs].cnt;
		}
		void Mark(int L,int R,ull d,int l,int r,int p=1) {
			if(L<=l&&r<=R)return tr[p].down(d);
			Down(p);
			if(L<=mid)Mark(L,R,d,l,mid,ls);
			if(mid<R)Mark(L,R,d,mid+1,r,rs);
			Up(p);
		}
		ull Sum(int L,int R,int l,int r,int p=1) {
			if(L<=l&&r<=R)return tr[p].sum;
			return Down(p),(L<=mid?Sum(L,R,l,mid,ls):0)+(mid<R?Sum(L,R,mid+1,r,rs):0);
		}
	} seg[2];
	void Hang(const int &idx,int l=1,int r=n,int p=1) {
		if(ql[idx]<=l&&r<=qr[idx])return Que[p].push_back(idx);
		if(ql[idx]<=mid)Hang(idx,l,mid,ls);
		if(mid<qr[idx])Hang(idx,mid+1,r,rs);
		if(ql[idx]<=mid&&mid<qr[idx])Que[p].push_back(idx);
	}
	ull Sep(int l=1,int r=n,int p=1) {
		if(l==r) {
			for(int idx:Que[p])ans[idx]+=1ull*a[l]*b[l];
			return 1ull*a[l]*b[l];
		}
		ull res(Sep(l,mid,ls)+Sep(mid+1,r,rs));
		FOR(i,l,r)que[i].clear();
		for(int idx:Que[p])if(l<ql[idx]||qr[idx]<r)
				que[max(l,ql[idx])].push_back(idx),que[min(r,qr[idx])].push_back(idx);
		mxa[mid]=a[mid],mxa[mid+1]=a[mid+1],mxb[mid]=b[mid],mxb[mid+1]=b[mid+1];
		DOR(i,mid-1,l)mxa[i]=max(mxa[i+1],a[i]),mxb[i]=max(mxb[i+1],b[i]);
		FOR(i,mid+2,r)mxa[i]=max(mxa[i-1],a[i]),mxb[i]=max(mxb[i-1],b[i]);
		int j(mid+1),k(mid+1);
		seg[0].Build<0>(l,mid),seg[1].Build<1>(l,mid);
		FOR(i,mid+1,r) {
			while(j>l&&mxa[j-1]<mxa[i])--j;
			while(k>j&&mxb[k-1]<mxb[i])--k;
			if(k<=mid)seg[0].Mark(k,mid,1ull*mxa[i]*mxb[i],l,mid);
			if(j<k)seg[1].Mark(j,k-1,mxa[i],l,mid);
			for(int idx:que[i])
				ans[idx]+=seg[0].Sum(max(l,ql[idx]),mid,l,mid)+seg[1].Sum(max(l,ql[idx]),mid,l,mid);
		}
		res+=seg[0].Sum(l,mid,l,mid)+seg[1].Sum(l,mid,l,mid);
		j=mid,k=mid,seg[0].Build<0>(mid+1,r),seg[1].Build<1>(mid+1,r);
		DOR(i,mid,l) {
			while(j<r&&mxa[j+1]<mxa[i])++j;
			while(k<j&&mxb[k+1]<mxb[i])++k;
			if(k>mid)seg[0].Mark(mid+1,k,1ull*mxa[i]*mxb[i],mid+1,r);
			if(j>k)seg[1].Mark(k+1,j,mxa[i],mid+1,r);
			for(int idx:que[i])
				ans[idx]+=seg[0].Sum(mid+1,min(r,qr[idx]),mid+1,r)+seg[1].Sum(mid+1,min(r,qr[idx]),mid+1,r);
		}
		res+=seg[0].Sum(mid+1,r,mid+1,r)+seg[1].Sum(mid+1,r,mid+1,r);
		for(int idx:Que[p])if(ql[idx]<=l&&r<=qr[idx])ans[idx]+=res;
		return res;
	}
#undef ls
#undef rs
#undef mid
	int Cmain() {
		FOR(i,1,Q)Hang(i);
		Sep();
		FOR(i,1,Q)wr(ans[i]);
		return 0;
	}
}
int main() {
#ifdef Plus_Cat
	freopen(Plus_Cat ".in","r",stdin),freopen(Plus_Cat ".out","w",stdout);
#endif
	rd(ID),rd(n);
	FOR(i,1,n)rd(a[i]);
	FOR(i,1,n)rd(b[i]);
	rd(Q);
	FOR(i,1,Q)rd(ql[i]),rd(qr[i]);
	return Subtask_Segment_Tree_Separation::Cmain();
}

分块

个人认为这个方法最友好,不难想、也不算难写,常数也小,实测也和线段树没多大的区别。

这里我们开始涉及到历史版本和。如果你学过历史版本和,那么你可以很容易的就看出来这个是一个历史版本和的题目,思路很简单:每次移动右端点时,我们单调栈配合区间覆盖整体更新后,打上一个历史版本和的标记,然后就可以回答离线下来的询问了。

我们主要的难点就只有如何实现分块。

考虑所有的操作:区间覆盖、历史版本区间加。我们可以维护区间覆盖标记,在它的基础上进行历史版本区间加。具体的,我们对每一块开一个区间覆盖标记数组和一个历史版本区间加标记数组:\(Cha_{0/1},tag_{0/1/2/3}\)

其中 \(tag_{0/1/2/3}\) 的下标第零位二进制位为 \(1\) 的时候,表示这是在这块 \(a\) 被整体更新后加的值,否则是更新前加的值;而第一位二进制位为 \(1\) 的时候,表示这是在这块 \(b\) 被整体更新后加的值,否则是更新前加的值。

然后我们就可以很简单的做到标记下传:在这块 \(a\) 被整体更新前加的值就乘更新前的 \(a\),否则乘整体更新后的标记;\(b\) 的标记同理。然后把乘积加到历史版本和数组中去。

代码

时间复杂度:\(O(n\sqrt{n}+Q\sqrt{n})\),空间复杂度:\(O(n+Q)\)

#define Plus_Cat "match"
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define ull unsigned long long
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define FOR(i,a,b) for(int i(a);i<=(int)(b);++i)
#define DOR(i,a,b) for(int i(a);i>=(int)(b);--i)
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
using namespace std;
namespace IOstream {
#define getc() getchar()
#define putc(c) putchar(c)
#define isdigit(c) ('0'<=(c)&&(c)<='9')
	template<class T>void rd(T &x) {
		static char ch(0);
		for(x=0,ch=getc(); !isdigit(ch); ch=getc());
		for(; isdigit(ch); x=(x<<1)+(x<<3)+(ch^48),ch=getc());
	}
	template<class T>void wr(T x,const char End='\n') {
		static int top(0);
		static int st[50];
		do st[++top]=x%10,x/=10;
		while(x);
		while(top)putc(st[top]^48),--top;
		putc(End);
	}
} using namespace IOstream;
constexpr int N(2.5e5+10),sN(5e2+10),lN(17),lV(lN+1);
int ID,n,Q;
int a[N],b[N],l[N],r[N];
ull ans[N];
struct Stack {
	int n;
	int st[N];
	int &operator [](int i) {
		return st[i];
	}
	int top() {
		return st[n];
	}
	bool empty() {
		return !n;
	}
	void clear() {
		st[n=0]=0;
	}
	template<class T>void push(int i,int *a,T func) {
		while(n&&a[i]>a[st[n]])--n;
		func(st[n]),st[++n]=i;
	}
	void pop() {
		--n;
	}
} A,B;
namespace Subtask {
	vector<int> Que[N];
	struct DB {
		int Bl,Bn;
		int st[sN],en[sN],idx[N];
		int Cha[sN][2],val[N][2];
		ull Sum[sN],hsum[N],Hsum[sN];
		ull sum[sN][2],tag[sN][4];
		void Init(int n) {
			Bl=ceil(sqrt(n)),Bn=(n-1)/Bl+1;
			FOR(i,1,Bn) {
				st[i]=en[i-1]+1,en[i]=min(en[i-1]+Bl,n);
				FOR(j,st[i],en[i])idx[j]=i;
			}
		}
		void Down(int u) {
			if(tag[u][0]||tag[u][1]||tag[u][2]||tag[u][3]) {
				FOR(i,st[u],en[u])
					hsum[i]+=tag[u][3]+tag[u][2]*val[i][0]+tag[u][1]*val[i][1]+tag[u][0]*val[i][0]*val[i][1];
				tag[u][0]=tag[u][1]=tag[u][2]=tag[u][3]=0;
			}
			if(Cha[u][0]) {
				FOR(i,st[u],en[u])val[i][0]=Cha[u][0];
				Cha[u][0]=0;
			}
			if(Cha[u][1]) {
				FOR(i,st[u],en[u])val[i][1]=Cha[u][1];
				Cha[u][1]=0;
			}
		}
		template<const bool ty>void change(int u,int l,int r,int d) {
			Down(u);
			FOR(i,l,r) {
				Sum[u]-=1ull*val[i][0]*val[i][1];
				sum[u][ty]+=d-val[i][ty],val[i][ty]=d;
				Sum[u]+=1ull*val[i][0]*val[i][1];
			}
		}
		template<const bool ty>void Change(int l,int r,int d) {
			int bl(idx[l]),br(idx[r]);
			if(bl==br)return change<ty>(bl,l,r,d);
			FOR(i,bl+1,br-1)Cha[i][ty]=d,Sum[i]=1ull*d*sum[i][ty^1],sum[i][ty]=1ull*d*Bl;
			change<ty>(bl,l,en[bl],d),change<ty>(br,st[br],r,d);
		}
		void mark(int u,int l,int r) {
			Down(u);
			FOR(i,l,r)hsum[i]+=1ull*val[i][0]*val[i][1],Hsum[u]+=1ull*val[i][0]*val[i][1];
		}
		void Mark(int l,int r) {
			int bl(idx[l]),br(idx[r]);
			if(bl==br)return mark(bl,l,r);
			FOR(i,bl+1,br-1) {
				Hsum[i]+=Sum[i];
				if(Cha[i][0]&&Cha[i][1])tag[i][3]+=1ull*Cha[i][0]*Cha[i][1];
				else if(Cha[i][1])tag[i][2]+=Cha[i][1];
				else if(Cha[i][0])tag[i][1]+=Cha[i][0];
				else ++tag[i][0];
			}
			mark(bl,l,en[bl]),mark(br,st[br],r);
		}
		ull query(int u,int l,int r) {
			Down(u);
			ull ans(0);
			FOR(i,l,r)ans+=hsum[i];
			return ans;
		}
		ull Query(int l,int r) {
			int bl(idx[l]),br(idx[r]);
			if(bl==br)return query(bl,l,r);
			ull ans(0);
			FOR(i,bl+1,br-1)ans+=Hsum[i];
			return ans+query(bl,l,en[bl])+query(br,st[br],r);
		}
	} D;
	int Cmain() {
		D.Init(n);
		FOR(i,1,Q)Que[r[i]].push_back(i);
		FOR(i,1,n) {
			A.push(i,a,[&](int idx) {D.Change<0>(idx+1,i,a[i]);});
			B.push(i,b,[&](int idx) {D.Change<1>(idx+1,i,b[i]);});
			D.Mark(1,i);
			for(const int &idx:Que[i])ans[idx]=D.Query(l[idx],r[idx]);
		}
		FOR(i,1,Q)wr(ans[i]);
		return 0;
	}
}
int main() {
#ifdef Plus_Cat
	freopen(Plus_Cat ".in","r",stdin),freopen(Plus_Cat ".out","w",stdout);
#endif
	rd(ID),rd(n);
	FOR(i,1,n)rd(a[i]);
	FOR(i,1,n)rd(b[i]);
	rd(Q);
	FOR(i,1,Q)rd(l[i]),rd(r[i]);
	return Subtask::Cmain();
}

线段树

其实线段树和分块的做法没多大区别,分块能做的标记线段树基本都能做,而且通常时间复杂度更小。

为了方便,我们考虑把标记和维护的数据开成结构体,进行存储和修改,这样会更加方便。

代码

时间复杂度:\(O((n+Q)\log_2{n})\),空间复杂度:\(O(n+Q)\)

#define Plus_Cat "match"
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define ull unsigned long long
#define FOR(i,a,b) for(int i(a);i<=(int)(b);++i)
#define DOR(i,a,b) for(int i(a);i>=(int)(b);--i)
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
using namespace std;
namespace IOstream {
#define getc() getchar()
#define putc(c) putchar(c)
#define isdigit(c) ('0'<=(c)&&(c)<='9')
	template<class T>void rd(T &x) {
		static char ch(0);
		for(x=0,ch=getc(); !isdigit(ch); ch=getc());
		for(; isdigit(ch); x=(x<<1)+(x<<3)+(ch^48),ch=getc());
	}
	template<class T>void wr(T x,const char End='\n') {
		static int top(0);
		static int st[50];
		do st[++top]=x%10,x/=10;
		while(x);
		while(top)putc(st[top]^48),--top;
		putc(End);
	}
} using namespace IOstream;
constexpr int N(2.5e5+10);
int ID,n,Q;
int a[N],b[N],ql[N],qr[N];
ull ans[N];
struct Stack {
	int n;
	int st[N];
	template<class T>void push(int i,int *a,T func) {
		while(n&&a[i]>a[st[n]])--n;
		func(st[n]),st[++n]=i;
	}
} A,B;
namespace Subtask_Sum_Up_of_Previous_Versions {
	vector<int> Que[N];
	struct Data {
		int len;
		ull Sxy,Sx,Sy,Sh;
		Data(int len=0,ull Sxy=0,ull Sx=0,ull Sy=0,ull Sh=0):len(len),Sxy(Sxy),Sx(Sx),Sy(Sy),Sh(Sh) {}
		friend Data operator +(Data a,Data b) {
			return Data(a.len+b.len,a.Sxy+b.Sxy,a.Sx+b.Sx,a.Sy+b.Sy,a.Sh+b.Sh);
		}
	};
	struct Tag {
		int Cx,Cy;
		ull Txy,Tx,Ty,T;
		Tag(int Cx=0,int Cy=0,ull Txy=0,ull Tx=0,ull Ty=0,ull T=0):
			Cx(Cx),Cy(Cy),Txy(Txy),Tx(Tx),Ty(Ty),T(T) {}
		friend Data operator +(Data a,Tag b) {
			Data c(a.len,a.Sxy,a.Sx,a.Sy,a.Sh+a.Sxy*b.Txy+a.Sx*b.Tx+a.Sy*b.Ty+a.len*b.T);
			if(b.Cx&&b.Cy)return c.Sx=1ull*a.len*b.Cx,c.Sy=1ull*a.len*b.Cy,c.Sxy=1ull*a.len*b.Cx*b.Cy,c;
			if(b.Cx)return c.Sx=1ull*a.len*b.Cx,c.Sxy=b.Cx*a.Sy,c;
			if(b.Cy)return c.Sy=1ull*a.len*b.Cy,c.Sxy=b.Cy*a.Sx,c;
			return c;
		}
		friend Tag operator +(Tag a,Tag b) {
			Tag c(b.Cx?b.Cx:a.Cx,b.Cy?b.Cy:a.Cy,a.Txy,a.Tx,a.Ty,a.T);
			if(a.Cx&&a.Cy)return c.T+=b.Txy*a.Cx*a.Cy+b.Tx*a.Cx+b.Ty*a.Cy+b.T,c;
			if(a.Cx)return c.Ty+=b.Txy*a.Cx+b.Ty,c.T+=b.Tx*a.Cx+b.T,c;
			if(a.Cy)return c.Tx+=b.Txy*a.Cy+b.Tx,c.T+=b.Ty*a.Cy+b.T,c;
			return c.Txy+=b.Txy,c.Tx+=b.Tx,c.Ty+=b.Ty,c.T+=b.T,c;
		}
		bool empty() {
			return !Cx&&!Cy&&!Txy&&!Tx&&!Ty&&!T;
		}
	};
	struct SEG {
#define ls (p<<1)
#define rs (p<<1|1)
#define mid ((l+r)>>1)
		struct node {
			Data data;
			Tag tag;
			node(Data data=Data(),Tag tag=Tag()):data(data),tag(tag) {}
			void down(Tag _tag) {
				data=data+_tag,tag=tag+_tag;
			}
		} tr[N<<2];
		void Up(int p) {
			tr[p].data=tr[ls].data+tr[rs].data;
		}
		void Down(int p) {
			if(!tr[p].tag.empty())tr[ls].down(tr[p].tag),tr[rs].down(tr[p].tag),tr[p].tag=Tag();
		}
		void Build(int p=1,int l=1,int r=n) {
			tr[p]=node(Data(r-l+1));
			if(l^r)Build(ls,l,mid),Build(rs,mid+1,r);
		}
		void Update(int L,int R,Tag d=Tag(0,0,1),int p=1,int l=1,int r=n) {
			if(L<=l&&r<=R)return tr[p].down(d),void();
			Down(p);
			if(L<=mid)Update(L,R,d,ls,l,mid);
			if(mid<R)Update(L,R,d,rs,mid+1,r);
			Up(p);
		}
		ull Sum(int L,int R,int p=1,int l=1,int r=n) {
			if(L<=l&&r<=R)return tr[p].data.Sh;
			return Down(p),(L<=mid?Sum(L,R,ls,l,mid):0)+(mid<R?Sum(L,R,rs,mid+1,r):0);
		}
#undef ls
#undef rs
#undef mid
	} seg;
	int Cmain() {
		seg.Build();
		FOR(i,1,Q)Que[qr[i]].push_back(i);
		FOR(i,1,n) {
			A.push(i,a,[&](const int &idx) -> void {return seg.Update(idx+1,i,Tag(a[i],0));});
			B.push(i,b,[&](const int &idx) -> void {return seg.Update(idx+1,i,Tag(0,b[i]));});
			seg.Update(1,i);
			for(const int &idx:Que[i])ans[idx]=seg.Sum(ql[idx],qr[idx]);
		}
		FOR(i,1,Q)wr(ans[i]);
		return 0;
	}
}
int main() {
#ifdef Plus_Cat
	freopen(Plus_Cat ".in","r",stdin),freopen(Plus_Cat ".out","w",stdout);
#endif
	rd(ID),rd(n);
	FOR(i,1,n)rd(b[i]);
	FOR(i,1,n)rd(a[i]);
	rd(Q);
	FOR(i,1,Q)rd(ql[i]),rd(qr[i]);
	return Subtask_Sum_Up_of_Previous_Versions::Cmain();
}
总结

看完既已身披英雄之名,就应具备不容撼动的觉悟之后有感,这里正好顺便总结一下关于分块、线段树的复杂标记题做法,也可以作为一个对原文的学习笔记吧。

前置知识
  • 半群

    对于非空集合 \(G\) 和其上的二元运算 \(\cdot\),如果该运算满足结合律,则称 \((G,\cdot)\) 是一个 半群

  • 幺半群

    对于半群 \((G,\cdot)\),如果它还存在单位元,则称 \((G,\cdot)\) 是一个 幺半群

  • 交换半群

    对于半群 \((G,\cdot)\),如果运算 \(\cdot\) 还满足交换律,即对于所有 \(a,b \in G\)a,b\in G,都满足 \(a \cdot b = b \cdot a\),则称 \((G,\cdot)\) 是一个 交换半群

分析

借鉴原文,我们把线段树上存储的信息分为两种:

  • 非下传标记,即该区间已经更新过的信息,我们把它们整体看做一个元素,属于半群 \((\mathcal{D},+)\)
  • 需要下传的标记,我们同样把它们整体看做一个元素,属于半群 \((\mathcal{M},\cdot)\)

二元运算符 \(\cdot:\mathcal{D} \times \mathcal{M} \to \mathcal{D}\)

它们的运算符还满足:

  • 结合律:

    \[\forall a \in \mathcal{D},b,c \in \mathcal{M},(a \cdot b) \cdot c = a \cdot (b \cdot c) \\ \]

  • 分配律:

    \[\forall a,b \in \mathcal{D},c \in \mathcal{M},(a + b) \cdot c = a \cdot c + b \cdot c \\ \]

这形成了一个双半群模型。顺便提一嘴:线段树维护的信息至少需要是半群信息,大多数时候会是交换半群

那我们怎么利用这个双半群模型呢?很简单,我们直接按照需求来设计 \(\mathcal{D}\)\(\mathcal{M}\) 即可,大多数时候,我们下传标记都是维护 \(\mathcal{D}\) 的增量,也有时会维护区间覆盖和乘法等。

再看我们把它设计成双半群模型最大的优势是什么?就是在它的运算符,如果放到我们的程序中,就是类似 Push_UpPush_Down 等操作中的转移、合并。比如,Push_Up 中,父节点将两个子节点的信息合并起来对应 \(\mathcal{D} + \mathcal{D} \to D\)Push_Down 中的标记下传到子节点中的 \(\mathcal{D}\) 对应 \(\mathcal{D} \cdot \mathcal{M} \to \mathcal{D}\),而下传到子节点中的 \(\mathcal{M}\) 对应 \(\mathcal{M} \cdot \mathcal{M} \to \mathcal{M}\)

接下来我们对于如何设计 \(\mathcal{D}\)\(\mathcal{M}\) 就有了一个可循之道,不用再遇到一道题目就只能从头开始构思,同时这样的模型对于我们构思 \(\mathcal{D}\)\(\mathcal{M}\) 内部变量也有一定的提示作用。

posted @ 2024-11-14 19:28  plus_cat  阅读(16)  评论(0编辑  收藏  举报