洛谷題目頁面傳送門
題意見洛谷。
方法\(1\):平衡樹
不難發現,行與行之間的操作是獨立的,而都與最后一列有關。很自然地想到維護每一行的前\(m-1\)個元素和最后一列。
不難發現,一次\((x,y)\)離隊,可以分成兩種情況:
- \(y=m\)。可以看作將最后一列的第\(x\)個移到最后;
- \(y\neq m\)。可以看作:
- 將第\(x\)行第\(y\)個移到最后一列最后;
- 將最后一列第\(x\)個移到第\(x\)行最后。
這些維護序列的刪除、插入、查詢第\(k\)個,一看就是序列之王平衡樹的操作,這里使用fhq-Treap。
至於,如果暴力維護平衡樹的話,建樹就要MLE,是\(\mathrm O(nm)\)的。考慮用fbb的OJ那題的trick,任意時刻,所有沒有被拎出來的naive元素組成的極大區間(區間內編號連續),我們把它們縮成一個點。
代碼里操作能合並的合並,修改操作也可以返回原值減少操作量,來減小常數,畢竟這不是正解/cy
由於要刪除節點,我開了垃圾回收,其實根本不用,就當練習一下(
一開始覺得挺難,現在看來是個挺板的題啊。。時間復雜度\(\mathrm O(q\log n)\)(假設\(n,m\)同階)。
這里講一個我代碼里犯的稀有的錯誤:這是我第一次用vector
存節點寫平衡樹,其中建樹的時候我是這樣寫的:
if(l<mid)lson(p)=bld(l,mid-1);
if(r>mid)rson(p)=bld(mid+1,r);
以第一句為例,這里lson(p)
和bld(l,mid-1)
的執行順序直覺是先后者后前者,但其實不是,究竟是先前者后后者還是UB我也說不清,詳見這篇帖子。問題來了,bld
函數會調用nwnd
函數新建節點並調用vector
的push_back
函數,這會使vector
重新分配內存,導致lson(p)
的地址變了,進而導致值賦不進去。我調這個代碼的那天晚上還以為鬧鬼了,差點把我整哭了(捂臉
所以應該找個中間變量存下來:
int res;
if(l<mid)res=bld(l,mid-1),lson(p)=res;
if(r>mid)res=bld(mid+1,r),rson(p)=res;
代碼(開洛谷自帶O2才能過哦,毒瘤):
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
typedef long long ll;
mt19937 rng(20060617);
const int N=300000;
int n,m,qu;
struct fhq_treap{//平衡樹
int root;
struct node{unsigned key;int lson,rson,sz,real_sz;ll l,r;};
#define key(p) nd[p].key
#define lson(p) nd[p].lson
#define rson(p) nd[p].rson
#define sz(p) nd[p].sz
#define real_sz(p) nd[p].real_sz
#define l(p) nd[p].l
#define r(p) nd[p].r
vector<node> nd;//vector存節點
stack<int> bin;//垃圾桶
int nwnd(ll l,ll r){//新建節點
if(l>r)return 0;
if(bin.size()){//用垃圾
int p=bin.top();
bin.pop();
return nd[p]=node({rng(),0,0,1,int(r-l+1),l,r}),p;
}
return nd.pb(node({rng(),0,0,1,int(r-l+1),l,r})),nd.size()-1;
}
void sprup(int p){sz(p)=sz(lson(p))+1+sz(rson(p));real_sz(p)=real_sz(lson(p))+r(p)-l(p)+1+real_sz(rson(p));}
int bld(int l=1,int r=n){//建樹
int mid=l+r>>1,p=nwnd(1ll*mid*m,1ll*mid*m);
int res;
if(l<mid)res=bld(l,mid-1),lson(p)=res;
if(r>mid)res=bld(mid+1,r),rson(p)=res;//訥,錯誤就在這里
sprup(p);
return sprup(p),p;
}
void init(ll l=0,ll r=0){
nd.pb(node({0,0,0,0,0,0,0}));
if(l)root=nwnd(l,r);
else root=bld();
}
pair<int,int> split(int x,int p=-1){~p||(p=root);
if(!x)return mp(0,p);
pair<int,int> sp;
if(x<=sz(lson(p)))return sp=split(x,lson(p)),lson(p)=sp.Y,sprup(p),mp(sp.X,p);
return sp=split(x-1-sz(lson(p)),rson(p)),rson(p)=sp.X,sprup(p),mp(p,sp.Y);
}
int mrg(int p,int q){
if(!p||!q)return p|q;
if(key(p)<key(q))return rson(p)=mrg(rson(p),q),sprup(p),p;
return lson(q)=mrg(p,lson(q)),sprup(q),q;
}
pair<int,int> rk(int x,int p=-1){~p||(p=root);//在樹中的排名(算naive區間)
if(x<=real_sz(lson(p)))return rk(x,lson(p));
if(x<=real_sz(lson(p))+r(p)-l(p)+1)return mp(sz(lson(p))+1,x-real_sz(lson(p)));
pair<int,int> res=rk(x-(real_sz(lson(p))+r(p)-l(p)+1),rson(p));
return mp(sz(lson(p))+1+res.X,res.Y);
}
void recyc(int p){bin.push(p);}//垃圾回收
ll del(int x){//刪除點
pair<int,int> _rk=rk(x);
pair<int,int> sp=split(_rk.X-1),sp0=split(1,sp.Y);
node tmp=nd[sp0.X];
recyc(sp0.X);//垃圾回收
int l=nwnd(tmp.l,tmp.l+_rk.Y-2),r=nwnd(tmp.l+_rk.Y,tmp.r);
return root=mrg(sp.X,mrg(l,mrg(r,sp0.Y))),tmp.l+_rk.Y-1;
}
ll chg_mv_bk(int x,ll v=0){//修改並移到最后
pair<int,int> _rk=rk(x);
pair<int,int> sp=split(_rk.X-1),sp0=split(1,sp.Y);
ll res=l(sp0.X)+_rk.Y-1;
int l=nwnd(l(sp0.X),l(sp0.X)+_rk.Y-2),r=nwnd(l(sp0.X)+_rk.Y,r(sp0.X));
l(sp0.X)=r(sp0.X)=v?v:res,real_sz(sp0.X)=1;
return root=mrg(sp.X,mrg(l,mrg(r,mrg(sp0.Y,sp0.X)))),res;
}
void pb(ll v){//在最后壓入
root=mrg(root,nwnd(v,v));
}
}trp_r[N+1],&trp_c=trp_r[0];
int main(){
cin>>n>>m>>qu;
for(int i=1;i<=n;i++)trp_r[i].init(1ll*(i-1)*m+1,1ll*i*m-1);
trp_c.init();//最后一列要普通建樹,因為編號不連續/kk
while(qu--){
int x,y;
scanf("%d%d",&x,&y);
if(y==m){//情況1
printf("%lld\n",trp_c.chg_mv_bk(x));
}
else{//情況2
ll res=trp_r[x].del(y);
printf("%lld\n",res);
trp_r[x].pb(trp_c.chg_mv_bk(x,res));
}
}
return 0;
}
方法\(2\):動態開點線段樹
回想當年不會平衡樹的時候,想着咋用現有知識維護序列插入刪除。一個想法是:給每個位置設一個值\(0/1\),\(1\)表示還健在,\(0\)表示被刪了。刪除操作就賦\(0\),查詢第\(k\)個就二分出前綴和等於\(k\)的第一個位置,那么插入呢?就無能為力了。幸運的是,這題的插入操作只存在於末尾,於是我們在末尾直接新建節點即可。考慮到空間開不下,我們用動態開點線段樹維護,查詢的話線段樹二分。然后大概也是跟平衡樹一樣縮點。
代碼很難寫,不想寫了。常數應該小一點?
方法\(3\):BIT(正解)
考慮繼續縮小常數。注意到,單點查詢和在前綴和上二分(BIT倍增,這里由於BIT只能從左往右倍增最右值,而我們要查最左邊的\(\geq k\)的位置,只需要轉化為最右邊的\(<k\)的位置加一即可)剛好都是BIT能支持的,考慮使用BIT。
但是BIT不能動態開點,空間受不了怎么辦呢?
考慮這樣一個方法:將詢問離線下來,依次處理每行的前\(m-1\)個元素(每行內部按時間戳排序),把查詢的結果記下來,處理完之后撤銷影響處理下一個,這樣空間只需要開一個BIT,時間復雜度也是不變的。
由於沒有按原順序操作,我們並不能知道每個位置的具體編號。這樣再從頭按原順序來一遍,此時每個行都不需要BIT了,把BIT留給最后一列實時操作,每行及最后一列開個vector
記錄插入末尾的編號,就可以實時查詢任意合法位置的編號了。
代碼(不開O2也每個點在\(1\mathrm s\)內):
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
typedef long long ll;
int lowbit(int x){return x&-x;}
const int N=300000,QU=300000;
int n,m,qu;
struct query{int x,y,id;}qry[QU+1];
bool cmp(query x,query y){return x.id<y.id;}
vector<query> v[N+1];
struct bitree{//BIT
int sum[N+QU+1];
void init(){memset(sum,0,sizeof(sum));}
void add(int x,int v){//單點加
while(x<=max(n,m)+qu)sum[x]+=v,x+=lowbit(x);
}
int fd(int x){//BIT倍增
int res=0,now=0;
for(int i=20;~i;i--)if(res+(1<<i)<=max(n,m)+qu&&now+sum[res+(1<<i)]<x)res+=1<<i,now+=sum[res];
return res+1;
}
}bit;
int fd[N+1];//記錄查詢結果
vector<ll> bk_r[N+1],&bk_c=bk_r[0];
int main(){
cin>>n>>m>>qu;
for(int i=1;i<=qu;i++)scanf("%d%d",&qry[i].x,&qry[i].y),qry[i].id=i,qry[i].y<m&&(v[qry[i].x].pb(qry[i]),0);
bit.init();//初始化
for(int i=1;i<m;i++)bit.add(i,1);
for(int i=1;i<=n;i++){//離線操作
sort(v[i].begin(),v[i].end(),cmp);//按時間戳排序
for(int j=0;j<v[i].size();j++){
int y=v[i][j].y,id=v[i][j].id;
fd[id]=bit.fd(y);//查詢並記錄
bit.add(fd[id],-1);bit.add(m+j,1);//刪除、插入
}
for(int j=0;j<v[i].size();j++)bit.add(fd[v[i][j].id],1),bit.add(m+j,-1);//撤銷影響
}
bit.init();//重置
for(int i=1;i<=n;i++)bit.add(i,1);
for(int i=1;i<=qu;i++){
int x=qry[i].x,y=qry[i].y;
if(y==m){
int _fd=bit.fd(x);
ll res=_fd<=n?1ll*_fd*m:bk_c[_fd-n-1];//從vector里查
printf("%lld\n",res);
bk_c.pb(res);//壓入vector
bit.add(_fd,-1);bit.add(n+bk_c.size(),1);//刪除、插入
}
else{
ll res=fd[i]<m?1ll*(x-1)*m+fd[i]:bk_r[x][fd[i]-m];//從vector里查
printf("%lld\n",res);
int _fd=bit.fd(x);
bk_r[x].pb(_fd<=n?1ll*_fd*m:bk_c[_fd-n-1]);bk_c.pb(res);//壓入vector
bit.add(_fd,-1);bit.add(n+bk_c.size(),1);//刪除、插入
}
}
return 0;
}