noi.ac#10 小x的城池 题解
noi.ac#10 小x的城池 题解
题意
有 \(n\) 个城市,从左到右编号 \(1...n\)。城市分为 A 类和 B 类,并且有一个人口值 \(v_i\)。城市 \(i,i+1\) 间有一条单向道路,初始都是向右 (即 \(i\to i+1\))。
支持两种操作,\(q\) 次:
REVERSE l r
:将 \(l,r\) 间的所有道路反向 (注意,城市不变,只变道路)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;
}