noi.ac#10 小x的城池 题解

noi.ac#10 小x的城池 题解

题意

\(n\) 个城市,从左到右编号 \(1...n\)。城市分为 A 类和 B 类,并且有一个人口值 \(v_i\)。城市 \(i,i+1\) 间有一条单向道路,初始都是向右 (即 \(i\to i+1\))。

支持两种操作,\(q\) 次:

  1. REVERSE l r:将 \(l,r\) 间的所有道路反向 (注意,城市不变,只变道路)
  2. UPDATE x y:将 \(v_x\) 改为 \(y\)

对于一个 A 类城市 \(i\),如果它能沿着道路走到一个 B 类城市 \(j\),且 \(v_j>v_i\),则城市 \(i\) 是危险的。

每次操作后,输出危险的城市数量。

\(n,q\le 10^5\)。任何时刻,人口值不超过 \(75\)\(1\le x\le n,1\le l\le r\le n\)

题解

这题码量不算大,但是细节很多。

首先咱肯定考虑线段树。区间翻转的常见套路是,对于我们维护的所有数据,都同时维护一个翻转之后的数据。翻转的时候直接交换它俩就行了。

这里有一个问题是,线段树维护边还是维护点。我们粗略一想发现维护边好像要麻烦很多,从而考虑维护点。但这样也要处理一下区间之间的边 (比如 \([l,r]\) 分成 \([l,mid]\)\([mid,r]\),此时还需处理 \(mid,mid+1\) 之间的边,但这个很容易搞)

接下来相当于只考虑维护区间信息,并且支持合并(没错,本题的所有毒瘤细节都在合并这里)。

为了方便描述问题,我们这样约定:

  • 对于一条边 \((i,i+1)\),将它的方向记为 \(dir[i]\)\(0\)\(1\) 左。
  • 假如点 \(l\)\(r\) 之间的边全都是同向的,我们称 \([l,r]\) 为一个同向段。

考虑合并两个区间后,危险的A类城市数量的变化。设两个区间为 \(A,B\)\(A\) 在左 \(B\) 在右。\(A,B\) 区间之间还连了一条边。

接下来考虑 \(A,B\) 间的边是向右的情况,向左同理。

容易发现此时只有 \(A\) 的最后一段向右的同向段中会产生新的危险城市。如何维护这部分贡献呢?

粗略一想好像并不容易,但注意到值域很小,只有 \(75\),考虑在值域上暴力。

我们暴力维护 \(A\) 最后一段右向同向段中,值为 \(i\) 的还不危险的A类城市数量,设为 \(c[i]\)

\(B\) 中第一段右向同向段中,B类城市的最大人口值为 \(k\),则新产生的贡献为 \(\sum\limits_{i<k} c[i]\),加上这个贡献即可。

大致思路就是这样,把该维护的维护了,复杂度是 \(O(n+75qlogn)\)

接下来有一个细节问题:接上面的考虑,如果 \(A\) 中有一个A类城市 \(p\),使得它能同时到达左右边界(至多只有一个),是否会出现漏算或者重算的问题?

由于还有 \(A,B\) 间边向左的情况,因此我们还需要维护第一段左向同向段的 \(c[i]\)。设这个是 \(cl[i]\),原来向右的那个是 \(cr[i]\)。然后我们就发现,如果原来 \(p\) 不是危险城市,但是 \(val[p]<k\),那么 \(p\) 就是危险的城市了,但此时 \(cl[i]\) 中还是会把 \(p\) 记为 “还不危险的城市” 并统计上。此时我们需要减掉这个贡献。

那我们如何判断 \(p\)\(A\) 中是不是危险的呢?我们发现,我们还需要记:第一段左向同向段的最大的 B 类城市人口,和最后一段右向同向段的最大的 B 类城市人口。

维护 \(p\) 时,我们还需要知道某一段是否全是右向/全是左向。这个可以通过记区间长度和区间 \(dir\) 的和来得到。

整理一下,我们需要记:

ans: 区间中已经危险的A类城市数量
len: 区间长度
drs: 区间中dir的和 (dir: 0右1左)
pos: 区间中,能同时到达左右边界的A类城市位置,若没有记为-1
li,ri,lo,ro: 边界同向段上B类城市人口最大值
// 这里的命名规则: i/o表示in/out,描述方向是向外的还是向内的; l,r表示左边界还是右边界
// 比如 li 就是左边界向内的段,就是第一段右向同向段; ro就是最后一段右向同向段; 其它同理
cl[i],cr[i]: 能到左/右边界的,人口数为i的,还不危险的A类城市数量

合并详见代码。li,ri,lo,ro的合并和最大子段和的那个合并有点像,就是要判一下如果有一段全是向左/向右的,另一段也可以更新过来,cl,cr也是。然后就是要写两个合并,分别是 \(A,B\) 间的边向左/右的情况。

然后建议是把它开个struct,然后记两个数组,一个是真实值,一个是反过来的值。翻转的时候交换它俩然后update即可。

另外本题卡空间。线段树无脑实现是四倍空间,但其实你可以只记非叶节点信息。然后对于非叶节点 \([L,R]\)\(L+R\) 显然是两两不同的。从而我们可以把 \(L+R\) 排序离散化到 \(1...n-1\)。然后线段树里就可以只开 \(n\) 个数组了。

代码

#include<bits/stdc++.h>
using namespace std;
#define N 100005
#define F(i,l,r) for(int i=(l);i<=(r);++i)
#define D(i,r,l) for(int i=(r);i>=(l);--i)
#define MEM(x,a) memset(x,a,sizeof(x))
int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}

int n,val[N]; char typ[N];
struct node
{
	int len,ans,drs,pos; int li,ri,lo,ro;
	int cl[76],cr[76];
};
node sing(int p,int x){char _=typ[p]; node t=(node){1,0,0,-1,-1,-1,-1,-1};MEM(t.cl,0);MEM(t.cr,0); if(_=='A')t.cl[x]=t.cr[x]=1,t.pos=p;else t.li=t.ri=t.lo=t.ro=x; return t;}
// 单点
node mg_r(node A,node B) // A->B
{
	node C; C.ans=A.ans+B.ans; C.len=A.len+B.len; C.drs=A.drs+B.drs;
	F(i,0,B.li-1)C.ans+=A.cr[i];
	F(i,0,75)C.cl[i]=A.cl[i],C.cr[i]=B.cr[i];
	C.li=A.li; C.lo=A.lo; C.ri=B.ri; C.ro=B.ro;
	C.pos=-1;
	if(B.drs==0) {C.pos=A.pos; F(i,max(B.li,0),75)C.cr[i]+=A.cr[i]; C.ro=max(C.ro,A.ro);}
	if(A.drs==0) {C.li=max(C.li,B.li);}
	if(A.pos>0) if(val[A.pos]<B.li && val[A.pos]>=A.ro && val[A.pos]>=A.lo) C.cl[val[A.pos]]--;
	return C;
}
node mg_l(node A,node B) // B->A
{
	node C; C.ans=A.ans+B.ans; C.len=A.len+B.len; C.drs=A.drs+B.drs+1;
	F(i,0,A.ri-1)C.ans+=B.cl[i];
	F(i,0,75)C.cl[i]=A.cl[i],C.cr[i]=B.cr[i];
	C.li=A.li; C.lo=A.lo; C.ri=B.ri; C.ro=B.ro;
	C.pos=-1;
	if(A.drs==A.len-1) {C.pos=B.pos; F(i,max(A.ri,0),75)C.cl[i]+=B.cl[i]; C.lo=max(C.lo,B.lo);}
	if(B.drs==B.len-1) {C.ri=max(C.ri,A.ri);}
	if(B.pos>0) if(val[B.pos]<A.ri && val[B.pos]>=B.lo && val[B.pos]>=B.ro) C.cr[val[B.pos]]--;
	return C;
}

int sgt_rec[N],sgt_id[N<<1],___p=0;
void sgt_fuck(int L=1,int R=n){if(L==R)return; sgt_rec[++___p]=L+R; int mid=(L+R)>>1; sgt_fuck(L,mid);sgt_fuck(mid+1,R);}
void sgt_init()
{
	___p=0; sgt_fuck();
	sort(sgt_rec+1,sgt_rec+___p+1);
	F(i,1,___p)sgt_id[sgt_rec[i]]=i;
}
#define sgi(L,R) sgt_id[L+R]
// 这一部分是卡空间那块,它可以把线段树非叶节点的 [L,R] 映射到 1...n-1
// sgi(L,R) 就表示非叶节点 [L,R] 的新编号

class SegT
{
public:
	int dir[N]; int rv[N<<2];
	node a[N],ra[N];
#define ls sgi(L,mid)
#define rs sgi(mid+1,R)
	void up(int L,int R)
	{
		int u=sgi(L,R),mid=(L+R)>>1,d=dir[mid];
		node l1,l2,r1,r2;
		if(L==mid)l1=l2=sing(L,val[L]);
		else l1=a[ls],l2=ra[ls];
		if(mid+1==R)r1=r2=sing(R,val[R]);
		else r1=a[rs],r2=ra[rs];
        // 这里处理一下叶子节点即可 (容易发现这样常数较大
		if(d==0) a[u]=mg_r(l1,r1),ra[u]=mg_l(l2,r2);
		else     a[u]=mg_l(l1,r1),ra[u]=mg_r(l2,r2);
	}
	void build(int L=1,int R=n)
	{
		if(L==R) return;
		int mid=(L+R)>>1;
		build(L,mid); build(mid+1,R); up(L,R);
	}
	void rv1(int L,int R)
	{
		if(L<R){int u=sgi(L,R); swap(a[u],ra[u]);rv[u]^=1;}
	}
	void down(int L,int R)
	{
		if(L==R)return;
		int u=sgi(L,R),mid=(L+R)>>1;
		if(rv[u])
		{
			dir[mid]^=1;
			rv1(L,mid);rv1(mid+1,R);
			rv[u]=0;
		}
	}
	void rev(int l,int r,int L=1,int R=n)
	{
		if(l<=L and R<=r) {rv1(L,R); return;}
		int mid=(L+R)>>1; down(L,R);
		if(r<=mid) rev(l,r,L,mid);
		else if(mid<l) rev(l,r,mid+1,R);
		else {rev(l,r,L,mid),rev(l,r,mid+1,R),dir[mid]^=1;}
		up(L,R); 
	}
	void change(int p,int x,int L=1,int R=n)
	{
		if(L==R)return;
		int mid=(L+R)>>1; down(L,R);
		if(p<=mid) change(p,x,L,mid); else change(p,x,mid+1,R);
		up(L,R);
	}
}T;
void flandre()
{
	n=I(); int q=I();
	F(i,1,n)
	{
		int v=I(); char __[10];scanf("%s",__); char c=__[0];
		val[i]=v; typ[i]=c;
	}
	sgt_init(); T.build();
	F(_,1,q)
	{
		char o[10];scanf("%s",o);
		if(o[0]=='R')
		{
			int l=I(),r=I();
			T.rev(l,r);
		}
		else
		{
			int p=I(),x=I();
			val[p]=x; T.change(p,x);
		}
		printf("%d\n",T.a[sgi(1,n)].ans);
	}
}
int main()
{
	int t=1;
	while(t-->0){flandre();}
	return 0;
}
posted @ 2022-08-24 11:32  Flandre-Zhu  阅读(36)  评论(0编辑  收藏  举报