@Cyani
2020-02-03T16:09:44.000000Z
字数 13096
阅读 1010
OI
欢迎来到Codeforces小课堂第3期,我是consecutivelimit,今天讲的题目比较简单:
http://codeforces.com/contest/966/problem/F
题目大意:给一个上下界网络流图,每条边 的上下界 都是关于全局变量 的一次函数,保证 时恒有 ,问均匀随机取一个 ,图中存在可行流的概率。
判断是否存在上下界可行流,可以新建一个源S和一个汇,然后对每条边 ,连容量为 的边 ,并将 的上界改为 ,下界改为 ,求新图的 最大流是否满流。
这样做的原理相当于预先把所有边的上界先流满,然后用一个最大流处理残余的流量。
最大流满流这个条件似乎不好直接入手,尤其是当所有 都是关于t的一次函数的情况下。这时候,我们需要一个定理——
最小割最大流定理!
嗯对,最大流等于最小割,也就是 ,其中 是点集 的子集, 表示满足 的边 的容量之和。于是满流的条件是 。(QQ空间似乎会把空集符号吃了)
因为每条边的容量都是关于 的一次函数,所以对于一个 , 也是关于 的一次函数, 则是一个上凸的半平面交,并且不会出现在 轴上方,和 轴重合的部分一定是一段区间。我们要求的是该函数与t轴的重合部分长度。
令 ,显然对于,可以通过一遍网络流把 求出来。如果存在 ,,那么不难通过二分法,找到最大的 使得 ;如果 也类似。
然而现在的情况是可能 ,,且满足 的 区间非常短,通过猜测 的方法很难找到满足 的 。怎么办呢?
注意到函数是上凸的,所以利用导函数 会很方便:对于 的情况,令 ,如果 ,则令 ,否则令 ,这样就可以二分了。至于导函数 是啥?就是
对于 ,跑网络流把最小割的方案 求出来之后,算一下一次项系数和咯。
这样,如果满足 的 存在,通过二分一定能找到满足此条件的 ,然后对左右两部分二分出满足 的 区间的左右端点,进而得到答案。
怎么样,是不是很简单?当然可能要注意精度问题就是了。
(讲个笑话,这场比赛我还是因为卡前面的题没开这题)
代码: http://codeforces.com/contest/966/submission/41638894
那么今天consecutivelimit的Codeforces小课堂第4期就开始了。
链接: http://codeforces.com/contest/997/problem/E
题目大意:给一个1..n的排列p[1..n],q次询问某个区间[l,r]内有多少个子区间[l',r'](l≤l'≤r'≤r),满足p[l'..r']出现了所有[min{p[l'..r']},max{p[l'..r']}]内的整数。n,q≤120000。
先考虑一个询问怎么做,这是一个经典问题,可以用分治或者线段树来解决。
csl在考场上尝试将分治做法扩展到多组询问,结果失败了,考后又推了一会儿才推出一个根号log的做法,很不优秀。
先剧透一下,这题复杂度是O((n+q)logn)的哦~
于是我们换一种思路——“枚举右端点,维护左端点”。
设f(l,r)为将p[l..r]排序后,形成了多少个连续段。显然f(r,r)=1,当l
f(l,r)=f(l,r-1)+1-[p[r]-1∈p[l..r-1]]-[p[r]+1∈p[l..r-1]]
按r=1,2,...,n的顺序枚举r,用一个数据结构维护f(,r),可以发现,r增加1,f(,r)的变化是一段区间加1,一段区间减1,不难联想到线段树。可是我们要求的是满足f(l,r)=1的l的个数,怎么办呢?
注意到如果存在f(l,r)=1,那么1一定是f(l,r)的最小值,于是用线段树维护区间最小值以及最小值个数即可。总复杂度O(nlogn)。
不错,我们接着考虑q个询问的做法。对于一个询问[l,r],通过以上线段树做法,我们可以O(logn)得到满足l≤l'≤r的合法区间[l',r]个数,却难以直接得到l≤r'≤r的合法区间[l',r']个数。当然,脑洞大开一下总是没有什么坏处的——
“把线段树做个前缀和?”
我们可以这么大胆想象:设α(l,r)为一个列向量,除了α(l,r)[f(l,r)]=1以外,其余所有α(l,r)[i]=0。然后用线段树维护α(*,r)的区间和,(α(l,r)+α(l+1,r)+...+α(r,r))[1]就是满足l≤l'≤r的合法区间[l',r]个数。现在给α(l,r)新增一维α(l,r)[0]表示l这个位置的所有历史版本的α(l,r')[1](r'≤r)之和,那么每个操作都可以表示为区间乘一个矩阵(以n=3为例):
区间加1(矩阵记为P):
1 0 0 0
0 0 0 0
0 1 0 0
0 0 1 0
区间减1(矩阵记为M):
1 0 0 0
0 0 1 0
0 0 0 1
0 0 0 0
更新前缀和(即α[0]+=α[1],矩阵记为A):
1 1 0 0
0 1 0 0
0 0 1 0
0 0 0 1
这样就可以用线段树描述前缀和操作了。当然,实现的时候显然不能用矩阵,否则比暴力还慢。。。
只有一次询问的情况相当于只有矩阵P和M,存储时只需存储向量中第一个非0位置及其值(其实就是f(l,r)的最小值及最小值个数),线段树标记也只记录P和M的次数差(其实就是区间加的标记,显然不可能爆出上下界)。
为了处理多次询问,需要在每个r处理完之后把[1,r]区间乘上矩阵A,于是现在的线段树结点标记长这样:P^?*M^?*A*P^?*M^?*A*P^?*M^?...,就是把向量平移若干位,然后把1位置上的数加到0位置,再平移若干位,再加……
同样,对于向量,只要存α[0]以及除α[0]外最低的非0位置及值。考虑如何把标记进行简化存储。显然,只要记录平移到最低点的信息(即后缀M的个数减P的个数最大的A,只需记录一个A后面M的个数减P的个数的最大值,及这样的A的个数),就能把标记简化成P^?*A^?*M^?的形式。
那么线段树做前缀和就搞定了,问题也就得以解决了,复杂度O((n+q)logn)。
代码(如果有问题欢迎hack): http://codeforces.com/contest/997/submission/40677876
欢迎收看Codeforces小课堂第5期,我是consecutivelimit,今天我们讨论的又是一道数学题哦。
链接: http://codeforces.com/contest/947/problem/E
题目大意:随机变量x的值域为{0,1,...,N},给出x的分布列,问进行M次将x变为{0,1,...,x}内的均匀随机数之后,x的分布列。输入和输出均对998244353取模。N≤10^5,M≤10^18。
先讲一个NOIP难度算法:设f(i,j)为经过i次操作之后,x=j的概率,则f(i,j)=Σ[k≥j]f(i-1,k)/(k+1),f(0,j)就是输入的x的分布列,f(M,j)就是答案分布列,复杂度O(NM)(前缀和优化)。
可是M是10^18级别,做一遍都炸了,怎么办?
好的,再讲一个省选级别的算法:注意到f(i,)是f(i-1,)乘上这样一个矩阵A(以N=3为例):
1 1/2 1/3 1/4
0 1/2 1/3 1/4
0 0 1/3 1/4
0 0 0 1/4
所以f(M,*)=A^M*f(0,),用矩阵快速幂计算,复杂度O(N^3logM)。
现在M的瓶颈是没了,但出现了一个新的瓶颈N^3,怎么办?
不妨找一找这种变换有啥性质。
一种思路是把它转化成分层图的路径来考虑,嗯,本csl考场上就是这么想的,以为它就是一个M+1层网格图从第一行到最后一行的路径条数,可惜这样概率就不均匀了,于是辛辛苦苦写完FFT最后才发现它是错的。。。
发散一下思维想想还有没有别的转化途径。如果不是矩阵乘法,而是多项式乘法,有什么更好的做法吗?对,把f(x)乘上g(x),就是先把f(x)和g(x)做一个DFT,然后把f(x)的每一项对应乘上g(x)的每一项,最后再IDFT回来。那么,f(i,j)=Σ[k≥j]f(i-1,k)/(k+1)这个变换,能不能认为是先对f(i-1,)做一个变换,得到一个序列[a(i-1,0),a(i-1,1),...,a(i-1,N)],接着每一项乘一个常数b[i],变成[a(i-1,0)b[0],a(i-1,1)b[1],...,a(i-1,N)b[N]],再把这个序列逆变换回来得到f(i,)呢?
如果存在这样的变换,就只要对f(0,)做这样的变换,再乘上b[i]^M,得到序列[a(0,0)b[0]^M,a(0,1)b[1]^M,...,a(0,N)b[N]^M],再把它逆变换回来就好了。
开始尝试这个构造吧!
其实就只要提取出f(i,)中的N+1个线性组合a(i,0),...,a(i,N),每个线性组合使得无论f(i-1,)如何取值,a(i,j)都是a(i-1,j)的常数倍,这个常数就是b[j]。
显然,由于f(i,N)=f(i-1,N)/(N+1),很容易提取出一个线性组合,就是f(i,N)单独一项,即:a(i,N)=f(i,N),并且常数b[N]=1/(N+1)。
尝试构造下一个组合。由于f(i,N-1)=f(i-1,N-1)/N+f(i-1,N)/(N+1),所以f(i,N-1)并不是f(i-1,N-1)的常数倍,但可以构造一个系数k,使得f(i,N-1)+k*f(i,N)是f(i-1,N-1)+k*f(i-1,N)的常数倍,由于
f(i,N-1)+k*f(i,N)=f(i-1,N-1)/N+(k+1)/(N+1)*f(i-1,N)
所以必有常数b[N-1]=1/N,于是(k+1)/(N+1)=k/N,得k=N。这样就得到了下一个线性组合:a(i,N-1)=f(i,N-1)+N*f(i,N),并且常数b[N-1]=1/N。
类似地,我们可以构造第三个组合。设这个组合为f(i,N-2)+k1*f(i,N-1)+k2*f(i,N)=f(i-1,N-2)/(N-1)+(1+k1)/N*f(i-1,N-1)+(1+k1+k2)/(N+1)f(i-1,N)=bN-2,得b[N-2]=1/(N-1),k1=N-1,k2=N(N-1)/2。
以此类推,可以归纳得到每个线性组合(j=0,1,...,N):
b[j]=1/(j+1)
a(i,j)=Σ[k≥j]C(k,j)f(i,k)
很棒,我们已经有一个O(N^2+NlogM)的做法了!
1、根据a(0,j)=Σ[k≥j]C(k,j)f(0,k)将输入的分布列转化为序列a(0,);
2、令a(M,j)=a(0,j)/(j+1)^M;
3、利用二项式反演逆变换f(M,j)=Σk≥j^(k-j)C(k,j)a(M,k),得到答案分布列f(M,*)。
至于最后一步的优化,也很简单了,就是把第1步和第3步的变换用FFT优化就行了。以第1步为例:
a[j]=ΣC(k,j)f[k]=Σk!f(k)/j!(k-j)!
a[j]/j!=Σk!f(k)/(k-j)!
看出来a[j]/j!是j!f(j)和1/(-j)!的卷积了吗?FFT就好了。第3步也类似。
这样我们就以O(NlogN+NlogM)的复杂度解决了本题!完结撒花!
没错,今天的CF小课堂第8期并没有鸽!虽然题目很简单就是了。
链接: http://codeforces.com/contest/1037/problem/H
题目大意:给定字符串s和q个询问,每次询问给出整数1≤l≤r≤|s|和字符串x,要求输出s[l..r]的所有子串s[l'..r'](l≤l'≤r'≤r)中字典序比x大的最小的一个。|s|≤10^5,Σ|x|≤2*10^5。
这类子串问题一般都可以尝试用后缀数组解决。
首先,建出s的后缀数组sa[],然后对于每个询问,将x在后缀数组上二分,可以O(|x|log|s|)找到后缀数组中,第一个字典序比x大的后缀sa[i]。
考虑先选一个字典序大于x的后缀t,再选t的一个字典序大于x的最短前缀t'作为答案,不难证明t'去掉最后一个字符后是x的前缀。
显然,后缀数组中,在sa[i]前面的后缀sa[j](j
问题来了,如何快速再后缀数组中找到满足sa[j]≥l,sa[j]+LCP(x,s[sa[j],n])≤r的最小的j呢?
直接考虑似乎不太容易……?不,一定是想复杂了。
p=LCP(x,s[sa[j],n])可以暴力枚举!
我们暴力枚举这个p,由于后缀数组中这个LCP就是sa[i]到sa[j]的height最小值,它是关于j递减的,所以满足LCP(x,s[sa[j],n])=p的j一定是后缀数组中的一段连续区间,可以用单调栈二分或者线段树找出这个区间。
接着需要求区间内最小的j使得l≤sa[j]≤l+p。可以直接二分j,然后裸上二维数点,用可持久化线段树做,这样询问复杂度是O(|x|log^2n)。可以做到更好的复杂度吗?答案是肯定的。
按l递减的顺序,用线段树维护sa[j]≥l的位置集合,然后在线段树上二分找出sa[j]的最小值不超过l+p的最小的j。这样询问复杂度就是O(|x|logn)了。
最后,总复杂度O((n+Σ|x|)logn)。
代码: http://codeforces.com/contest/1037/submission/42409646
大家好,我回来了。。。这里是上午下午都在训练累到爆的consecutivelimit。。。Codeforces小课堂第9期开始啦。
链接: http://codeforces.com/contest/1007/problem/D
(画外音:CSL你又拿你考场上没调出来的题来毒害大家了?)
题目大意:有一棵n个结点的有根树,还有m只蚂蚁,每只蚂蚁有2条备选路径(a[i],b[i])和(c[i],d[i]),你需要给每只蚂蚁选其中一条备选路径,使得不同蚂蚁的路径没有公共边。判断是否有解,如果有解给出一组解。n≤10^5,m≤10^4。
暴力做法是2^m枚举每只蚂蚁选哪条路径,然后判断是否存在一条边被两条路径覆盖。
用x[i]∈{0,1}表示第i只蚂蚁选的路径,容易发现,如果存在一条边同时在第i只蚂蚁的路径a和第j只蚂蚁的路径b上,那么x[i]=a和x[j]=b不能同时满足,当所有约束都满足时就是一个合法的方案。
这样就得到了一个2-SAT模型:枚举两只蚂蚁的各一条路径i,a和j,b,如果相交,就新建约束“x[i]=b或x[j]=a”,用强连通分量算法求解2-SAT即可。
该2-SAT的边数为O(m^2),显然既会TLE也会MLE。
我们来优化建图。一种显然的思路是:枚举一条树边e,把过e的蚂蚁路径都取出来,然后两两建一个约束。
设过边e的路径有k条,第i条是第p[i]∈{1,...,m}只蚂蚁的第q[i]∈{0,1}条路径,用(x[i],a)表示2-SAT图中代表x[i]=a的结点,那么我们需要对所有的i≠j,建立约束“x[p[i]]=1-q[i]或x[p[j]]=1-q[j]”。
这也是本人考场上的想法。
至于怎么建,我们建一个辅助变量z[e],当至少有一个x[p[i]]=q[i]成立时,z[e]=1,当z[e]=1时,所有x[p[j]]=1-q[j]成立,即2-SAT中这样建立约束。注意咯,2-SAT要记得建对称边:
(x[p[i]],q[i])->(z[e],1)
(z[e],0)->(x[p[i]],1-q[i])
(z[e],1)->(x[p[j]],1-q[j])
(x[p[j]],q[j])->(z[e],0)
这就只需要一个点和一个子树内所有结点连相同方向的边了。求出树的DFS序,设e子树的DFS序区间为[l,r],那么需要把一个点和“一端在[l,r]内,一端小于l或大于r”的所有路径连边。
显然可以用可持久化线段树优化建图:将路径按端点DFS序加入可持久化线段树,加到l-1以后往[l,r]就可以连向一段在[l,r]内,一端小于l的路径。另一端大于r的处理方法类似。复杂度O((n+m)logn)。
对,看起来是一个log,但常数巨大。
讲个故事,本人在考场上看这题的时候只剩不到1h了,想到做法以后赶紧写,好不容易写完了,然后一直调不过样例。
考后继续调又调了好久,最后发现……
这个做法!
根!
本!
就!
是!
错!
的!
为啥呢?回到一开始的连边,对于树边e,(x[p[i]],q[i])->(x[p[j]],1-q[j])的要求是i≠j,否则自相矛盾。
可是新建一个点z[e]让其它点和它相连,相当于把i≠j这个限制去掉了。
这就很尴尬了,一时不知道怎么改。。。
看来要换思路了,可能还得用每条链去和除它以外的其它链连边,而不是基于中间结点两两互连。
考虑对于第i只蚂蚁的第j条链u-v,如何找出和它相交的所有链?
对于另一条链u'-v',令a=LCA(u,v),a'=LCA(u',v'),那么当a和a'没有祖先关系时,一定不相交;当a是a'的祖先时,只需a'->u'或a'->v'的第一条边在u-v上;a'是a的祖先情况类似;a=a'时,按任意一种处理。
这样,不妨把所有链u-v拆成两条有祖先关系的链a-u和a-v,再考虑建2-SAT。
对于每条链a-u,取出a->u的第一条边e,令过e的第i条链对应的约束为(x[p[i]],q[i]),就然后把a-u链对应的约束(x[p[i]],q[i])和过e的所有链对应的约束(x[p[j]],q[j])(j≠i)都连一组对称边:
(x[p[i]],q[i])->(x[p[j]],1-q[j])
(x[p[j]],q[j])->(x[p[i]],1-q[i])
现在就可以直接按DFS序建可持久化线段树,将所有链a-u按照a的DFS序从小到大依次加入可持久化线段树,和边e(子树DFS区间[l,r])的子树连边时在DFS序小于l的链加入完之后的可持久化线段树版本上连区间[l,r]的边,即可和过e的所有链相连。
这样就完成对每条链往可持久化线段树的区间连边了。复杂度O(mlogn),常数非常大。
代码(如果有问题欢迎hack): http://codeforces.com/contest/1007/submission/41623820
这里是用一点残存的spirit写题解的consecutivelimit。
【Codeforces小课堂第10期】
题目链接: http://codeforces.com/contest/1012/problem/F
题目大意:你准备去N个国家旅行各一次,第i个国家的旅行计划在第s[i]天早上出发,第s[i]+len[i]-1天回家。你有P本护照,在每次旅行前必须让其中一本护照办理该国的签证,如果在第x天开始对某本护照办理第i个国家的签证,那么第x天不能在旅行,且第x+t[i]天中午可完成签证拿回该护照(允许在旅行时拿到)。判断旅行计划能否完成,如果能,给出一种签证方案(时间及哪本护照)。N≤22,P≤2。
分析:
实际上我们需要对每个旅行确定一个护照p[i]和签证开始时间x[i],使得:
(1)x[i]
(2)不存在j使s[j]≤x[i]
(3)不存在j使a[i]=a[j]且x[j]≤x[i]
(4)不存在j使a[i]=a[j]且x[i]≤s[j]≤x[i]+t[i]。
先考虑P=1怎么做。
对于a[i]相同的i,如果x[i]之间的相对大小关系一定,那么只要从小到大贪心确定x[i]即可。
因为N的范围只有22,所以不妨用状压DP解决排列问题。
令f(S,t)表示能否在t天内完成S集合内的旅行?
可惜t的范围太大,不行。但我们发现这样的状态实在是太浪费了,不如改改:
令f(S)表示完成集合S内的旅行,最晚的旅行的结束时间的最小值。
那么f(S)=min{t(f(S-{i}),i)|t(f(S-{i}),i)
至于t怎么求呢?首先上面式子中超过s[i]就舍去满足了(1),同时i安排在之前所有旅行之后,满足了(3)。
因此只需在x之后没有旅行的时间段内选一端长度至少为t[i]+1的,就能满足(2)和(4)。
这个直接做是O(2^N*N^2)的,不过可以优化:用顺推式更新f(S)→f(S∪{i}),i按照t[i]从小到大更新,这样就能单调性扫来更新DP值了。当f(U)<+inf时说明有解,复杂度O(2^N*N)。
再考虑P=2怎么做。
对于大多约束,可以把旅行分成两个护照,然后每个护照负责的旅行都要满足上述条件,也就是f(S)定义为在其中一个护照上处理完S集合的旅行的最小时间。
所以仍然用顺推式转移,对于每个S,将第f(S)天以后的天去掉S中的旅行时间段以后剩下的段提取出来,那么转移仍然可以按t[i]从小到大的顺序进行f(S)→f(S∪{i})的更新。
那么当f(S)<+inf且f(U-S)<+inf时,把S放护照1,U-S放护照2是合法的。
时间复杂度仍然是O(2^N*N)。
代码: http://codeforces.com/contest/1012/submission/41557815
【Codeforces小课堂第11期】
题目链接: http://codeforces.com/contest/1023/problem/G
题目大意:n个湖由n-1条河连通,每条河连接两个湖,河有长度l[i],一只鱼游过河i需要l[i]天,在一段时间内,给出k个形如"d[j]天至少有f[j]只鱼在湖p[j]内"的描述,问整个系统至少有多少只鱼。
分析:
首先,看到题目,不难建出一个网络流图:
· 对于每个树上结点i,建一条容量无穷大的链S→v[i,1]→v[i,2]→...→v[i,2D]→T,其中D=max{d[i]};
· 对于每条树边(i,j),对所有k连容量无穷大的边(v[i,2k],v[j,2(k+l(i,j)-1)]),(v[j,2k],v[i,2(k+l(i,j))-1]);
· 对于每个1≤i≤k,对边(v[p[i],2d[i]-1],v[p[i],2d[i]])设置流量下界f[i]。
求下界最小流就是答案。
下界最小流显然可以转成最大流来做:考虑判断是否有答案ans≤x,只需连容量为x的边(T,S),新建源S'和汇T',然后对每个1≤i≤k去掉(v[p[i],2d[i]-1],v[p[i],2d[i]])的下界并新建容量为f[i]的边(S',v[p[i],2d[i]]),(v[p[i],2d[i]-1],T')。那么当S到T的最大流为Σf[i]时说明满流,即ans≤x。
很好,已经迈出了解决这题的第一步。
下一步应该是被用烂的一个套路了:根据最小割最大流定义,考虑把每条链S→v[i,1]→v[i,2]→...→v[i,2D]→T切成两部分,前一部分属于T'集,后一部分属于S'集,那么问题转成了这么一个玩意:
· 给每个点v分配一个整数x[v],使得对每条边(u,v),2(ceil(x[u]/2)-l(u,v))≤x[v]≤2([x[u]/2]+l(u,v)),最大化满足x[p[i]]=2d[i]-1的i的f[i]之和。
不错,推到这一步,就可以充分利用图是树的性质了。
显然,可以用一个O(nD)的DP来做这个东西:令f(i,j)为当x[i]=j时,i子树的最大和是多少。显然这个是TLE的。
怎么办呢?我们不妨试试用数据结构优化这个DP!
(嗯没错,我考场上想到了这一步,然后就没时间了。。。事实证明这题考场上几乎不可能有时间写完)
这一步我绕了好几个弯,最后回到最初的那种思路上才做出来。。。
来,我们用数据结构来维护之前的DP数组。注意到DP数组实际上是若干段连续区间,每段的DP值一样。并且转移有两种:
1. f(u,j)=max{f(v,j')|2(ceil(j/2)-l(u,v))≤j'≤2([j/2]+l(u,v))}
一些区间会变长,一些区间会变短甚至消失。
在数据结构中,我们可以每次暴力删掉将最早消失的区间。
2. f(u,v)=f(v,j)+f(w,j)
在数据结构中,我们可以启发式合并f(v,)和f(w,),用插入和区间加的操作即可完成。
很棒了。如果能用单次操作O(logn)的数据结构维护f,那么总复杂度就是O(n+klogk),就可以AC这题了。
由于有插入和区间加,我放弃了用STL的set,写了个Splay树的启发式合并。。。这种做法不仅代码巨长而且细节巨多,所以我花了2天时间才写完调完。。。当然,复杂度是O(n+klogk),这是没有问题的。
另外,在KAN的提醒下这题其实可以用STL set的,做法是打差分标记。
代码: http://codeforces.com/contest/1023/submission/41908179
【Codeforces小课堂第14期】
题目链接: http://codeforces.com/contest/1039/problem/E
题目大意:给定一个由n个长度均为w的区间构成的序列,和q次询问,每次询问一个k,求至少要将这个区间序列分成多少段,才能使每段内区间的交集长度不小于k。n,q≤10^5,k≤w≤10^9,时限7s。
分析:
先考虑一个询问怎么做。用S表示区间序列[x1,x1+w],[x2,x2+w],...,[xn,xn+w],那么不难发现,如果S[l,r]内区间交集长度不小于k,那么S[l+1,r]内区间交集长度也不小于k。这样就可以做一个贪心:
* 一开始i=1;
* 每次找一个最小的j使得S[i,j]内的区间交集长度小于k,然后分出一段[i,j-1],令i=j;
* 当i>n时结束。
很好,那么多次询问怎么搞?
考虑维护这个贪心的过程。对于每个i,令f[i]为最小的满足S[i,j]内区间交集长度小于k的j,把i向f[i]连边,就得到了一个树。以结点n+1为根,那么答案就是结点1的深度。
当k变化时,f[i]也会相应发生变化。考虑按k从大到小处理询问,k减小时,f[i]只会增加。把f[i]增加的事件点记下来,它们和询问放在一起排序。处理事件点时,看起来可以用LCT维护这棵树。等等……
“f[i]变化的事件点有多少个啊?”
“糟了,是O(n)级别的。。。”
于是还是维护不了……
所以呢?不会做就分块吧!
我们不记录下所有的事件点,只记录增加后f[i]≤i+b(b是块大小)的事件点,这样事件点就只有O(nb)个了。
不过只处理这些事件,当有f[i]≥i+b时,f[i]的值可能是不正确的,所以这时候把f[i]设成i,即把i设为根,需要另外计算。
查询怎么办?从i=1出发,不断往上走i=f[i],直到走到根,然后想办法查询出i的实际f[i],继续往上走。这时候,一种O(n√n log n)的做法已经出来了:
* 暴力预处理f[i]≤i+b的事件点并排序;
* 用LCT维护i连向f[i]形成的森林,修改f[i]时在LCT上cut、link;
* 查询时,令i=1,不断将i移到根,然后用线段树/二分+Sparse Table都可以O(logn)求出f[i];直到i=n+1,答案就是i移动的边数;由于每次查询i至少增加b,所以这步总复杂度O(nqn/b)。
最终复杂度O(n(b+q/b)logn),档b取√n时取得最优复杂度O(n√q log n),可惜由于LCT常数太大,不太能过。。。
还有救吗?
继续分块!
只不过这回不是对树分块,而是对序列S分块。
设块大小为b,对于每次修改操作,我们只在块内维护f[i]与i在同一块的f[i]。这样,事件点个数仍然是O(nb),每个事件点需要更新一整个块,复杂度O(b)。
查询时,考虑跳到i和f[i]不在同一块的i时怎么办,这时候块内并没有维护f[i]。如果仍用线段树或者二分+ST,复杂度O(logn)。
等等……总复杂度是啥?
O(nb+qnlogn/b),当b=√(qlogn)时复杂度最优,为O(nq^(2/3)√logn)。
能不能想办法去掉log?
有个技巧:对每个点维护上次查询出的f[i],那么查询时设新的f[i]为原来的f[i]+t,用ST+倍增法求t,即先不断地试t=2^0,2^1,2^2...,再二分,复杂度O(logt)而不是O(logn)。
看起来只是个常数优化,实际上已经把log去掉了。为什么?
因为满足f[i]≤i+n^(2/3)的f[i]之和有均摊保证,总共增加不超过n^(5/3),当f[i]>i+n^(2/3)时,查询时这个情况出现次数不超过qn^(1/3)。
所以总共就是O(n^(5/3)+qn^(1/3)logn),即O(n^(5/3))。
这个做法可以通过,不过比较慢,需要注意常数。
代码: http://codeforces.com/contest/1039/submission/42741582
【Coodeforces小课堂第15期】
题目链接: http://codeforces.com/contest/956/problem/F
题目大意:求l到r之间有多少个整数x,满足可以将x的数位分成两组,两组数位和之差不超过k。多组数据,数据组数n≤50000,1≤l≤r≤10^18,0≤k≤9。
分析:
这题感觉是CF小课堂系列中比较简单的题目之一了。
先想想给你一个x,如何求x点数位分成两组的数位和之差的最小值。
很显然有一个DP,f(i,j)表示能否把前i位分组使两组差为j,则有:
f(i,j)=f(i-1,j+x)∨f(i-1,|j-x|)
那么最小的满足f(m,j)=1的j就是所求最小值,m是x的位数。
这个子问题解决以后呢,原来的问题只需要再套一层DP就做完了。
具体来说,我们设计一个DP:g(i,s)表示所有i位数x(可以有前导0)中,满足对x做之前的DP以后,DP数组的f(i,)状态为s(也就是说s是一个01串),这样的x有多少个。
因为f(i,)只由f(i-1,*)决定,所以:
g(i,s)=∑[0≤x≤9]g(i-1,s')
其中s'是满足插入一个x以后能转移到s的状态。
看起来状态的s这一维很爆炸,不过我们可以进行剪枝:因为最后只有当s的前10位有1时才有用,所以对于g(i,s),s只有前9i+1位是有用的,前9i+1位相同而后面不同的状态看成同一个状态。
用记忆化搜索可以搜出所有状态。这样剪完以后,实测总状态数不到10^5,可喜可贺。
可是新的问题来了,这题有多组数据,每次做一遍DP是不现实的。
想象一下,如果你要多次询问DAG中某个点到T的路径条数,那么每次跑一遍DP肯定不优秀。优秀的做法是从T反着做一遍DP,就可以直接询问了。
对这题也是同样道理。我们现在记g(i,s)表示x做到第i位之前的f数组为s,确定后i位有多少种方案。等等,这个定义不明确?
嗯,应该这样:g(i,s,k)表示x做到第i位之前的f数组为s,确定后i位有多少种方案使得数位可以分成差不超过k的两组。
这样询问[l,r]的时候,拆成[0,r+1)和[0,l)。在询问[0,n)时枚举x和n的LCP,就可以直接用DP数组回答了。
bonus:
这题官方题解里提到l,r≤10^100也是能做的。没错,做法仍然很简单。
在之前用记忆化搜索搜状态的基础上,当两个状态对于x=0,1,...,9的后继都相同时,把这两个状态合并。
合并以后状态数骤减,只有715个。那么在现在的状态上DP就可以了。
为了进一步优化,可以对g(i,s,k)做前缀和:g(i,s,k,x)表示在之前的意义基础上,要求下一个数位不超过x。这样预处理复杂度不变,查询复杂度是O(log r)的。
代码: http://codeforces.com/contest/956/submission/42773766