快要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;
}