title: NOIP2017小结
tags:
---
Day2
T3
题意十分的简洁,就是需要弄个数据结构去维护一个矩阵中,删除一个位置,然后把这一行左移一个单位,再把最后一列前移一个单位。反正,我一看就觉得是线段树,直接建\(n+1\)个线段树,就可以直接搞了,二看就觉得空间分分钟炸掉。于是就没啥办法了。
\(30pts\)
考虑\(30pts\)的数据,暴力模拟就好了。
\(50pts\)
然后还有一个\(20pts\)的数据,\(q\)比较小,似乎有一种\(q^2\)的做法,反正我是不会。
但是后面还有一个\(20pts\)的数据,保证\(n=1\),那么就好办了,直接暴力维护一个线段树,感觉会被卡常,我就写了个树状数组。其实就是这么维护的,我维护了2个树状数组,然后,一个维护现在这个位置的原编号,一个维护前缀和。然后删除一个点,就是把两个树状数组都弄个后缀修改,最后再在最后一个位置加回来就好了,一个简单的差分思想。也就是说,我们把删除的那个位置直接空余出来,然后再最后的位置上再添加信息,貌似2016年的那个蚯蚓,我考场上也是用线段树这么写的,好套路的东西啊。
\(60pts\)
既然可以维护一个\(n=1\)的特殊情况,那么\(x=1\)的,就弄4个树状数组就好了,分别是维护第一行的标号与前缀和,维护最后一列的标号与前缀和,但是有点麻烦,而且只有10分,考场上我又没时间写了。
1.把第一行的这个位置删掉,输出结果,然后把最后一列的第二行加进来。
2.把最后一列的第一行删掉,把输出的结果加到最后一列的最后一行。
可能用树状数组写的真有点麻烦,所以可以直接建2个线段树,那就好写一点了。
\(60-100pts\)
这仅仅是个思路,尽管我在想到树状数组后马上就想到了,但是由于思路很混乱,我并没敢在考场上写。
考虑一下我们在\(60\)分做法中尽管效率\(O(qlogn)\)是可以过的,但是我们的做法扩展到\(n*m\)上来就被卡空间了,所以我想了一个这种的办法:暴力分块(阔怕)
我们可以对于一行,把\(\sqrt{n}\)列化作一块,然后对于最后一列,再把\(\sqrt{n}\)行化作一块,对于一个块,我们维护2个信息:块的大小和块内的前缀和。
然后删去一个点时,我们找到这一行,然后再这一行中,用二分查找确定这个点所在的块,直接把这个点删掉,然后把最后一列的对应行的加到这一行的最后一个块中,然后把这个点加到最后一列的最后一个块中。
等等,貌似还是会被卡空间哈。因为我们没有能避免建出一个\(n*m\)的矩阵。
但是,把这个思路用到线段树中,再加一些技巧就可以A了,那就是\(100\)分做法了。
\(100pts\)
基本原理
首先有线段树做法:
线段树的问题,常常都很容易被卡空间,然后就要弄一个叫动态开点的思想,就是避免一开始把所有的数据全部都建到线段树中,而是对于每次操作,都单独进行一个点修改,同时据此建一个大小为\(logn\)的线段树,这好像也就是主席树的基本思想哈,严格来说应该叫可持久化啊,但是这也就有麻烦了,因为如果我们在这道题中不一开始就把线段树建好,我们就存在2个麻烦:
1.我们没有办法确定一个点在某一行中线段树中的具体位置,因为我们没有一开始就建好线段树,所以我们没有办法直接维护编号。
2.我们没有办法确定一个点的数值,因为我们没有一开始就建好线段树,所以我们没有办法维护一开始的数值(
好熟悉的一句话),那么经过操作后就更没有办法维护了。
那么我们就单独考虑这两个麻烦。
一
我们沿用分块中维护块的大小的思想,我们在这个动态的线段树中维护一个size域,用来表示这个区间中存在的点的个数然后二分查找一下,确定询问点的位置。然后考虑一下如何在动态开点中维护一个size域。其实仔细想一下也是很简单的,如果我们这个区间之前一直没有访问过,说明这个区间内的点的个数是没有受到前面操作的影响的,那么点的个数就是区间长度了,如果这个区间是访问过得,那么我们在做删除操作的时候,size--,在做添加操作时,size++不就好了?需要注意一个细节,就是我们建的线段树应该是\(max(n,m)+q\)大小的啊,那么我们可能存在一开始就没有访问的区间,其右端点是大于\(max(n,m)\),那么在计算初始size域的时候,就不是区间长度了,而应该这么判断:如果其左端点小于\(max(n,m)\)那么初始的size域就是m-l,如果其左端点大于\(max(n,m)\)那么初始的size域就是0,这么做可能就会存在一个问题,也就是说,如果我们在n+1的位置添加了一个点,那么n+2的size域在初始化的时候是不是就会出现问题?庆幸的是,这是不会出现的。因为如果n+1和n+2是一个区间内的,那么这个区间就是访问过得了,那么size直接++--就行了,如果不是一个区间内的,那么n+2显然是0啊,因为我们维护的是区间内的总和,而不是整个的前缀和。因为如果维护整个的前缀和,我们在修改时,就避免不了\(O(n)\)的修改。这个麻烦就差不多了。
二
然后我们考虑一下具体数值的转移,在有上面的做法基础上,我们只需要在size域初始化的同时,直接搞一个赋为(x-1)*m+y就好了啊,然后单独弄个modify过程就好了,似乎就很简单了哈。
Tips
做法可能有点绕啊,所以考场上几乎想不出来呵,即使想出来了,也不一定码的出来呵。所以要想理好思绪再写。
Some expand
然后,好像有一个更好写,更高大上的做法(但是我太弱了,并不会写)——平衡树做法:
搞一个非旋treap(好像也是可持久化数据结构啊)或者就是splay去做类似线段树的操作,那样可以节省一点空间,但是常数就很玄学了,尤其是对于这类卡常变态级的题目,Luogu的机子可以过,但是觉着CCF的老爷就很蛋疼了哈。
参考程序:
#include<cstdio>
#include<algorithm>
#include<cstring>
#define rint register int
using namespace std;
typedef long long ll;
const int M=1e7+3;
const int N=3e5+5;
struct pdt{
int L,R,sum;
ll val;
}T[M];
int n,m,q,totn,sz,flag;
int root[N],tot[N];
inline int read(){
rint x=0,f=1;char ch=getchar();
while(ch<‘0‘||ch>‘9‘){if(ch==‘-‘)f=-1;ch=getchar();}
while(‘0‘<=ch&&ch<=‘9‘)x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x*f;
}
inline int getSum(int l,int r){
if (flag>n){
if (r<=n)return r-l+1;
if (l<=n)return n-l+1;
return 0;
}
if (r<m)return r-l+1;
if (l<m)return m-l;
return 0;
}
inline ll query(int&now,int k,int l=1,int r=totn){
if (!now){
now=++sz;
T[now].sum=getSum(l,r);
if (l==r){
if (flag>n) T[now].val=(ll)l*m;
else T[now].val=(ll)(flag-1)*m+l;
}
}
--T[now].sum;
if (l==r)return T[now].val;
rint mid=(l+r)>>1,tt=T[now].L?T[T[now].L].sum:(mid-l+1);
if (k<=tt)
return query(T[now].L,k,l,mid);
else
return query(T[now].R,k-tt,mid+1,r);
}
inline void modify(int&now,int k,ll v,int l=1,int r=totn){
if (!now){
now=++sz;
T[now].sum=getSum(l,r);
if (l==r)T[now].val=v;
}
++T[now].sum;
if (l==r) return;
rint mid=(l+r)>>1;
if (k<=mid) modify(T[now].L,k,v,l,mid);
else modify(T[now].R,k,v,mid+1,r);
}
int main(){
freopen("phalanx.in","r",stdin);
freopen("phalanx.out","w",stdout);
n=read(),m=read(),q=read();
totn=max(n,m)+q;
for (rint i=1;i<=q;++i){
ll ans;
rint x=read(),y=read();
if (y==m) flag=n+1,ans=query(root[n+1],x);
else flag=x,ans=query(root[x],y);
printf("%lld\n",ans);
flag=n+1,modify(root[n+1],n+(++tot[n+1]),ans);
if (y!=m){
flag=n+1,ans=query(root[n+1],x);
flag=x,modify(root[x],m-1+(++tot[x]),ans);
}
}
return 0;
}