快要APIO了。切切往年的APIO,如何呢?
UPD 2020.8.13:wtcl做不動APIO。把2019做了算了吧。(注:原標題是「APIO?~2019」)
APIO2019
T1 - 奇怪裝置
這應該是個人均會的題目吧(
好的,現在看來我應該不至於爆零了(
給定\(n,a,b\)以及\(n\)個區間\([l_i,r_i]\),求\(\left|\bigcup\limits_{i=1}^n\left\{\left(\left(x+\left\lfloor\dfrac xb\right\rfloor\right)\bmod a,x\bmod b\right)\mid x\in[l_i,r_i]\right\}\right|\)。
\(n\in\left[1,10^6\right],a,b\in\left[1,10^{18}\right],0\leq l_i\leq r_i\leq 10^{18}\)。
首先可以手玩或者寫個程序打個表,摸清楚\(\left(\left(x+\left\lfloor\dfrac xb\right\rfloor\right)\bmod a,x\bmod b\right)\)這個東西的規律。不難發現,隨着\(x\)加一,第二個數肯定是加一(在\(\bmod b\)意義下),第一個數先加上個一,然后如果\(b\mid x\)就再加個一(在\(\bmod a\)意義下)。
由於每步的二元組值只跟上一步有關,而且值域有限大,所以顯然是有循環節的。不妨找出這個循環節。
顯然,第二個滿足二元組等於\((0,0)\)的\(x\)就是循環節大小(第一個\(x=0\))。注意到,第一個數的增量與第二個數有關,而第二個數自己加自己的不受干擾,所以設\(x=yb\),其中\(y\)是整數。顯然,在從\(0\)開始的前\(x\)次增加中,第一個數正常加的是\(x\)次,額外增加了\(y\)次。在\(\bmod a\)意義下為\(0\),所以滿足\(a\mid x+y\),即\(a\mid y(b+1)\)。即\(\dfrac{a}{\gcd(a,b+1)}\mid y\dfrac{b+1}{\gcd(a,b+1)}\),顯然\(y=\dfrac{a}{\gcd(a,b+1)}\)時取得最小值,於是循環節為\(x=yb=\dfrac{a}{\gcd(a,b+1)}b\)。
接下來,把每個區間\(l,r\)都弄成\(\bmod x\)意義下的集合,分三種情況:
- \(r-l+1\leq x\),此時顯然它一個人就承包了值域里的所有元素,直接輸出\(x\)走人;
- 否則,\(l\bmod x>r\bmod x\),顯然是跨過了一個斷點,可以將它拆成\([l\bmod x,x-1],[0,r\bmod x]\)兩個區間;
- 否則,\(l\bmod x\leq r\bmod x\),就變成\([l\bmod x,r\bmod x]\)這一個區間。
接下來就是個區間並的事了。
代碼:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
#define ppb pop_back
#define mp make_pair
#define X first
#define Y second
const int inf=0x3f3f3f3f3f3f3f3f;
int gcd(int x,int y){return y?gcd(y,x%y):x;}
int n,a,b;
vector<pair<int,int> > rg,ans;
signed main(){
cin>>n>>a>>b;
int x=a/gcd(a,b+1);
x=x<LLONG_MAX/b?x*b:inf;
while(n--){
int l,r;
scanf("%lld%lld",&l,&r);
if(r-l+1>=x)return cout<<x,0;
else if(l%x>r%x)rg.pb(mp(l%x,x-1)),rg.pb(mp(0,r%x));
else rg.pb(mp(l%x,r%x));//分三種情況
}
sort(rg.begin(),rg.end());
for(int i=0;i<rg.size();i++)//區間並
if(ans.size()&&rg[i].X<=ans.back().Y)ans.back().Y=max(ans.back().Y,rg[i].Y);
else ans.pb(rg[i]);
// for(int i=0;i<ans.size();i++)printf("[%lld,%lld]\n",ans[i].X,ans[i].Y);
int ans0=0;
for(int i=0;i<ans.size();i++)ans0+=ans[i].Y-ans[i].X+1;//統計答案
cout<<ans0;
return 0;
}
T2 - 橋梁
題意見洛谷。
(以下認為所有\(\log\)同階,用\(\log n\)表示)
首先在我認知范圍內似乎稍微復雜一點的圖上詢問題都沒有polylog做法。先來考慮暴力。
先上一個sb都會的暴力:修改就直接改,查詢就把所有符合條件的邊連起來建出一張圖跑DFS。\(\mathrm O(qm)\)。
再考慮一個稍微經過大腦的想法。假如問題是靜態的話,我們可以將所有邊和所有詢問排個序,使得在之前有的邊之后一定有,這樣就可以two-pointers地遞推建圖了,並查集維護連通性即可。但是有修改怎么辦呢?依然排序,只將永遠不會被修改的邊排序按上述方法加,有修改的邊的話就把修改它的操作有序地存下來,每次查詢就二分查找出每條有修改的邊最后一次修改並符合條件就加入並查集,得出答案后撤銷。時間復雜度\(\mathrm O\!\left(m\log n+q^2\log n\right)\)。
事實上這兩種暴力並不能視作並列。不難發現,暴力一單次操作復雜度僅與\(m\)相關,暴力二單次操作復雜度僅與\(q\)相關,但是暴力二還有一個單獨的\(m\),而且這個\(m\)和暴力一里的\(m\)是做的同樣的工作:往圖里加邊。如果把暴力一的DFS也看成並查集的話,完全可以這樣理解:暴力二是一些連續的操作離線下來排序,可以將所有操作分成若干段抱團取暖,暴力二本質上是\(1\)段,而暴力一是\(q\)段。
顯然這兩種分段方式都太極端了。不妨強行令每段大小相等,這樣就有了分塊的想法:每段\(sz1\)個操作,共\(\mathrm O\!\left(\dfrac q{sz1}\right)\)段。這樣總時間復雜度顯然是\(\mathrm O\!\left(\dfrac q{sz1}(m\log n+sz1^2\log n)\right)=\mathrm O\!\left(\dfrac{qm\log n}{sz1}+q\cdot sz1\log n\right)\)。根據均值不等式,\(sz1=\sqrt m\)時時間復雜度為最優為\(\mathrm O(q\sqrt m\log n)\),如果你常數跟我一樣大就別想了老老實實優化吧,如果你常數跟fz一樣小可以考慮卡過去。
考慮優化。接下來分析復雜度的時候不考慮線性和線性乘以\(\log\)的操作,因為它們對復雜度的影響實在是微乎其微。把剩下來的操作都拎出來整理一遍。
- 將沒有修改的邊排序:每塊都要排一遍,\(\mathrm O\!\left(\dfrac{qm\log n}{sz1}\right)\);
- 將沒有修改的邊加入可撤銷並查集:每塊都要加一遍,\(\mathrm O\!\left(\dfrac{qm\log n}{sz1}\right)\);
- 將有修改的邊加入可撤銷並查集:\(\mathrm O\!\left(q\cdot sz1\log n\right)\)。
考慮操作\(1\)。注意到這一塊和上一塊都沒有修改的邊一定是排好序的了,我們只需要將上一塊有修改這一塊沒有修改的邊拎出來排序然后和之前那個歸並一下即可。而第二類那種邊單塊只能有\(\mathrm O(sz1)\)條,總共就是\(\mathrm O(q)\)條。於是操作\(1\)的\(\log\)沒了。然鵝復雜度沒有變,因為下面有個復雜度一樣的操作(悲)
考慮操作\(2\)。顯然這一部分是永遠不會被撤銷的,於是這一部分改到普通並查集模式,路徑壓縮+啟發式合並可以變\(\log\)為\(\alpha\)。至此總復雜度降到了\(\mathrm O\!\left(q\sqrt{m\alpha(n)\log n}\right)\)。(還是很大)
考慮操作\(3\)。可以並查集縮點,然后暴力連邊跑DFS。這樣復雜度顯然是少了一個\(\log\)了的。至此,令\(sz1=\sqrt{m\alpha(n)}\)即可擁有\(\mathrm O\!\left(q\sqrt{m\alpha(n)}\right)\)的總復雜度。
可把我給卡常卡死了。人傻常數大就是指我吧。鄰接表需要用鏈式前向星(而且這樣DFS還比並查集慢,我甚至懷疑我寫假了,發現我假掉了歡迎在評論區D死小編哦)。
(所以這個時間軸分塊到底是啥套路,不太摸得清)
下面貼代碼:
#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
const int N=50000,M=100000,QU=100000;
int n,m,qu;
int a[M+1],b[M+1],w[M+1];
struct query{int tp,x,y;}qry[QU+1];
int ans[QU+1];
vector<int> chged,unchged;
int las_unchged[M+1];
vector<int> chgid[M+1];
vector<int> ask;
bool cmp(int x,int y){return qry[x].y>qry[y].y;}
bool cmp0(int x,int y){return w[x]>w[y];}
struct ufset{
int fa[N+1],sz[N+1];
void init(){
for(int i=1;i<=n;i++)fa[i]=0,sz[i]=1;
}
int root(int x){return fa[x]?fa[x]=root(fa[x]):x;}
void mrg(int x,int y){
x=root(x),y=root(y);
if(x==y)return;
if(sz[x]>sz[y])swap(x,y);
fa[x]=y,sz[y]+=sz[x];
}
int _sz(int x){return sz[x];}
}ufs;
struct addedge{
int sz,head[N+1],nxt[2*M+1],val[2*M+1];
void init(){sz=0;}
void ae(int x,int y){
val[++sz]=y;nxt[sz]=head[x];head[x]=sz;
}
}nei;
vector<int> cc;
bool vis[N+1];
int dfs(int x){
vis[x]=true;
cc.pb(x);
int res=ufs._sz(x);
for(int i=nei.head[x];i;i=nei.nxt[i]){
int y=nei.val[i];
if(vis[y])continue;
res+=dfs(y);
}
return res;
}
int main(){
// freopen("C:\\Users\\chenx\\OneDrive\\桌面\\06.in","r",stdin);
cin>>n>>m;
for(int i=1;i<=m;i++)scanf("%d%d%d",a+i,b+i,w+i);
cin>>qu;
for(int i=1;i<=qu;i++)scanf("%d%d%d",&qry[i].tp,&qry[i].x,&qry[i].y);
int sz1=sqrt((m+1)*4);
for(int i=1,ie;i<=qu;i=ie+1){
ie=min(qu,i+sz1-1);
memset(las_unchged,0,sizeof(las_unchged));
for(int j=0;j<unchged.size();j++)las_unchged[unchged[j]]=1;
chged.clear();ask.clear();
for(int j=1;j<=m;j++)chgid[j].clear();
for(int j=i;j<=ie;j++)
if(qry[j].tp==1){
if(chgid[qry[j].x].empty())chged.pb(qry[j].x);
chgid[qry[j].x].pb(j);
}
else ask.pb(j);
sort(ask.begin(),ask.end(),cmp);
vector<int> v,v0;
for(int j=1;j<=m;j++)if(chgid[j].empty()){
las_unchged[j]++;
if(las_unchged[j]==1)v.pb(j);
}
sort(v.begin(),v.end(),cmp0);
for(int j=0;j<unchged.size();j++)if(las_unchged[unchged[j]]==2)v0.pb(unchged[j]);
unchged.clear();
int now1=-1,now2=-1;
while(now1+1<v.size()||now2+1<v0.size()){
if(now1+1==v.size()||now2+1<v0.size()&&cmp0(v0[now2+1],v[now1+1]))unchged.pb(v0[++now2]);
else unchged.pb(v[++now1]);
}
ufs.init();
int now=-1;
for(int j=0;j<ask.size();j++){
while(now+1<unchged.size()&&qry[ask[j]].y<=w[unchged[now+1]])
now++,ufs.mrg(a[unchged[now]],b[unchged[now]]);
nei.init();
for(int k=0;k<chged.size();k++){
int fd=lower_bound(chgid[chged[k]].begin(),chgid[chged[k]].end(),ask[j])-chgid[chged[k]].begin()-1;
int w0=fd==-1?w[chged[k]]:qry[chgid[chged[k]][fd]].y;
if(qry[ask[j]].y>w0)continue;
int ar=ufs.root(a[chged[k]]),br=ufs.root(b[chged[k]]);
nei.ae(ar,br),nei.ae(br,ar);
}
ans[ask[j]]=dfs(ufs.root(qry[ask[j]].x));
for(int k=0;k<chged.size();k++)nei.head[ufs.root(a[chged[k]])]=nei.head[ufs.root(b[chged[k]])]=0;
for(int k=0;k<cc.size();k++)vis[cc[k]]=false;
cc.clear();
}
for(int j=i;j<=ie;j++)if(qry[j].tp==1)w[qry[j].x]=qry[j].y;
}
for(int i=1;i<=qu;i++)if(qry[i].tp==2)printf("%d\n",ans[i]);
return 0;
}
T3 - 路燈
題意見洛谷。
首先考慮實時維護\(\forall i,j\in[1,n]\),目前有多少時刻滿足能從\(i\)到達\(j\)。
顯然,\(i\)能到達\(j\)當且僅當\(i,j\)在同一個亮燈連續段里。將當前狀態剖成若干個極大亮燈連續段,那么能否到達關於\(i,j\)兩維的函數應該是由這些連續段,每段分別在\(i\)軸和\(j\)軸上作為兩條鄰邊、\(j=i\)上的一段作為副對角線的一些充滿\(1\)的正方形組成的,其他地方都是\(0\)。(別問為啥不貼圖,問就是我不是良心博主)
那么在某一時刻的答案函數就是之前所有時刻的所對應的上述01函數之和。考慮實時維護這個答案函數(二維函數,即一個矩陣)。
一種很容易想到的方法是遞推,每次將當前時刻的01函數加到當前維護的答案函數里面去。當前時刻的01函數顯然是由上一時刻的01函數進行常數次矩形修改得來的(開燈就是連接兩邊接壤的\(1\)矩形並補全,關燈就是從所在\(1\)矩形斷開,至於如何維護這些連續段,set
即可,太簡單不多說),而將01函數加到答案函數里去又等價於對答案矩陣進行若干次(這里不是常數次了,而是連續段個數次,即\(1\)矩形個數次)矩形增加\(1\)。這里對01函數的更新顯然是力所能及的,而對答案函數的更新的復雜度就沒法保證了。
不難發現,這里對01函數的更新是若干次矩形加\(1\),其中\(1\)是個常數,而這些矩形隨時刻遞增又是常數差異的,就一臉可以優化成在這些常數級別的差異上增加非常數,從而保證復雜度。
考慮一個01函數關於時刻的三維函數。考慮當前答案函數的每一處在這個三維函數上的意義:顯然是從當前時刻斷開時間軸得到的縱切面的左邊的所有此處的和。這是一些\(0/1\)的和,\(0\)顯然不用考慮,剩下來就是一些\(1\)的區間,設為\([l_1,r_1],[l_2,r_2],\cdots,[l_k,r_k]\),那么這個和就是\(\sum\limits_{i=1}^k(r_i-l_i+1)\)。考慮在一個區間開始的時候給此處貢獻上\(-l_i\),在區間結束的時候給此處貢獻上\(r_i+1\)。此時增加量顯然不是常數了,我們來看看增加次數有沒有減少。區間開始和區間結束的時候,就是相鄰01函數差異對此處影響的時候,每次時刻的遞推都只有01函數差異的矩形量次矩形增加,哦吼,可以了。需要注意的是,若當前要查詢的那處在01函數中為\(1\)的話,那么第\(k\)個區間還未結束,我們要強行令它結束,即在答案函數在此處的值的基礎上再加上當前時刻。
(上面這一段重要的轉化是本題的瓶頸。個人感覺這個哲學思想理解的還不是很透徹,大概以后重點做DS的時候題做多了感覺就上來了吧。)
接下來就是個矩形增加、單點查詢的事了。看起來能夠線段樹套動態開點線段樹,但寫到一半才發現外層線段樹的懶標記無法\(\mathrm O(1)\)存儲。於是想到將修改和查詢范圍顛倒的方式:差分。考慮二維差分,這樣一次矩形增加轉化為\(4\)次單點增加,單點查詢轉化為前綴矩形求和。這就是個經典的二維數點模型,寫一發BIT套動態開點線段樹即可(萌新第一次寫這個,多多關照)。二維數點應該是有其他方法的(如cdq分治),然而我還沒有系統的學這一塊,不管,而且這題重點不在這里。
btw,這是我第二次寫vector
動態開點數據結構(第一次是列隊),犯了跟列隊同樣的錯誤(UB+vector
分配內存原理造成賦不進去值)。多錯幾次就不會犯了。
代碼:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int inf=0x3f3f3f3f;
int lowbit(int x){return x&-x;}
const int N=300000;
int n,qu;
char a[N+5];
struct segtree{//動態開點線段樹
struct node{int lson,rson,l,r,sum;};
#define lson(p) nd[p].lson
#define rson(p) nd[p].rson
#define l(p) nd[p].l
#define r(p) nd[p].r
#define sum(p) nd[p].sum
vector<node> nd;
int nwnd(int l=1,int r=n){return nd.pb(node({0,0,l,r,0})),nd.size()-1;}
void init(){//初始化
nd.pb(node({0,0,0,0,0}));
nwnd();
}
void add(int x,int v,int p=1){//單點增加
sum(p)+=v;
if(l(p)==r(p))return;
int mid=l(p)+r(p)>>1,res;
if(x<=mid){
if(!lson(p))res=nwnd(l(p),mid),lson(p)=res;
add(x,v,lson(p));
}
else{
if(!rson(p))res=nwnd(mid+1,r(p)),rson(p)=res;
add(x,v,rson(p));
}
}
int _sum(int l,int r,int p=1){//區間求和
if(!p)return 0;
if(l<=l(p)&&r>=r(p))return sum(p);
int mid=l(p)+r(p)>>1,res=0;
if(l<=mid)res+=_sum(l,r,lson(p));
if(r>mid)res+=_sum(l,r,rson(p));
return res;
}
};
struct bitree{//BIT
segtree segt[N+1];
void init(){//初始化
for(int i=1;i<=n;i++)segt[i].init();
}
void add(int x,int y,int v){
if(y>n)return;
while(x<=n)segt[x].add(y,v),x+=lowbit(x);
}
void add(int l1,int r1,int l2,int r2,int v){//矩形增加
add(r1+1,r2+1,v);add(l1,r2+1,-v);add(r1+1,l2,-v);add(l1,l2,v);//轉化為差分數組上的單點增加
}
int val(int x,int y){//單點查詢
int res=0;
while(x)res+=segt[x]._sum(1,y),x-=lowbit(x);//轉化為差分數組上的矩形求和
return res;
}
}bit;
int main(){
cin>>n>>qu;
scanf("%s",a+1);
set<pair<int,int> > st;
for(int i=1,las=0;i<=n+1;i++)
if(a[i]=='1')las=las?las:i;
else if(las)st.insert(mp(las,i-1)),las=0;
bit.init();
for(int i=1;i<=qu;i++){
char tp[10];int x,y;
scanf(" %s%d",tp,&x);
if(tp[0]=='t'){
set<pair<int,int> >::iterator fd=st.upper_bound(mp(x,inf));
if(fd!=st.begin()&&x<=(--fd)->Y){//關燈
int l=fd->X,r=fd->Y;
st.erase(fd);
bit.add(l,r,l,r,i);
if(l<x)st.insert(mp(l,x-1)),bit.add(l,x-1,l,x-1,-i);
if(r>x)st.insert(mp(x+1,r)),bit.add(x+1,r,x+1,r,-i);
}
else{//開燈
fd=st.upper_bound(mp(x,inf));
int l=x,r=x;
if(fd!=st.begin())fd--,fd->Y==x-1?bit.add(fd->X,fd->Y,fd->X,fd->Y,i),l=fd->X,st.erase(fd++):fd++;
if(fd!=st.end())fd->X==x+1&&(bit.add(fd->X,fd->Y,fd->X,fd->Y,i),r=fd->Y,st.erase(fd),0);
st.insert(mp(l,r));
bit.add(l,r,l,r,-i);
}
}
else{
scanf("%d",&y);y--;
set<pair<int,int> >::iterator fd=st.upper_bound(mp(x,inf));
if(fd!=st.begin()&&y<=(--fd)->Y)printf("%d\n",bit.val(x,y)+i);
else printf("%d\n",bit.val(x,y));
}
}
return 0;
}