寿司:函数单调性
n2还要讲吗?提一下吧,毕竟考场上我也只能想到这个。
我的思路和下发的题解所讲的思路又不太一样。如果你不想看我的思路而想直接跟着正解走,可以直接从下面的分界线以下开始阅读。
我的想法也是化环为序列,枚举所有可能的n长的序列。(这个应该没人不知道吧?)
然后,我的思路是:对于每个序列,枚举j表示把所有的颜色聚集到位置j附近。
我们假设我们移动的颜色为1,而不动的颜色为0。那么我们现在需要聚集的数字是1。
最后的效果大概是00000111111111000这样的
对于一个位置,需要的变换总数是多少?
101010010(1)001011
假如我们需要把两边所有的1移动到带括号的1附近,所有数从左往右下标是1~16,这是j是10。
对于第4个1:(10-1)-8=1步
对于第3个1:(10-2)-5=3步
对于第2个1:(10-3)-3=4步
对于第1个1:(10-4)-1=5步
对于这些在分界线左边的1,观察上面的式子:被减数形成等差数列,减数是这个数字1所在的位置
等差数列求和可以O(1)搞定,而位置的求和可以用前缀和维护。
对于第6个1:13-(10+1)=2步
对于第7个1:15-(10+2)=3步
对于第8个1:16-(10+3)=3步
在分界线右边的这些1其实也同理,一个等差数列一个前缀和。
而括号里的那个10就是枚举的j,把它们提出来乘以个数很好算,剩下的就是以1为首项1为公比的等差数列了。
我这样是为了方便求,当然也可以不提出来,反正我的代码实现里是这样的。
规律大概找到了,把通式写出来就不难了吧。
特别的,如果你选中的j位置上是一个0,想把那些1移过来,会是这样的效果:
0000001111(0)110000
这肯定没完啊,它没连成一块。接下来的操作就很好想了:要么把左边的都右移,要么右边的左移。
额外的操作步数就是min(左边1的个数,右边1的个数)。
至于怎么O(1)找到一段上1的个数。。前缀和呗。
(我的代码中j的含义是整个序列的起点位置,i是数字1聚集的位置,和上面说的不同,注意区分)
(a[i]是[下标小于等于i的所有数字1的下标前缀和],c[i]是[下标小于等于i的所有数字1的个数前缀和])
1 #include<cstdio> 2 #include<iostream> 3 using namespace std; 4 int c[2000005],n,x[2000005],t;long long a[2000005],ans; 5 #define r register 6 inline void read(){ 7 r char ch=getchar();n=0; 8 while(ch!='R'&&ch!='B')ch=getchar(); 9 while(ch=='R'||ch=='B')x[++n]=(ch=='R'?1:0),ch=getchar(); 10 } 11 int main(){ 12 scanf("%d",&t); 13 while(t--){ 14 read();ans=(long long)1e15; 15 const r int nn=n,nnn=n*2; 16 for(r int i=1;i<=nn;++i)x[i+n]=x[i]; 17 for(r int i=1;i<=nnn;++i)c[i]=c[i-1]+x[i],a[i]=a[i-1]+i*x[i]; 18 for(r int i=1;i<=nnn;++i){ 19 const r int mx=min(i,n); 20 for(r int j=max(i-n+1,1);j<=mx;++j){ 21 const r int ll=c[i-1]-c[j-1],rr=c[j+n-1]-c[i]; 22 ans=min(ans,(ll-rr)*i+a[j+n-1]+a[j-1]-a[i]-a[i-1]-(ll*(ll+1ll)+rr*(rr+1ll))/2+(x[i]?0:min(ll,rr))); 23 //printf("%d %d %d %d %lld\n",i,j,ll,rr,(ll-rr)*i+a[j+n-1]+a[j-1]-a[i]-a[i-1]-(ll*(ll+1ll)+rr*(rr+1ll))/2+(x[i]?0:min(ll,rr))); 24 } 25 } 26 printf("%lld\n",ans); 27 } 28 }
接下来开始讲正解。。的n2
正解所说的思路是找到分界线,分界线左边的直接扔到左边界,右边的往右扔。
基础的式子的话大致思路和我上面的那个差不多,也是一个[区间的位置和]和[一个等差数列]做差。
const r int ll=c[jj-1]-c[i-1],rr=c[i+n-1]-c[jj-1];//分别表示分界线左右边各有多少个1 const r long long rans=-ll*(ll-1ll)/2-rr*(rr-1ll)/2+a[jj-1]*2-a[i-1]-ll*i+rr*(i+n-1)-a[i+n-1]; //a和c数组含义同上。jj是分界线位置,i是序列起始位置
还是比较好理解的。不细讲了。
接下来开始对复杂度进行优化。
性质1.在同一个序列中,关于分界线位置j的移动次数的函数是一个单峰函数。(其实是谷,U型)
证明的话可能不太严谨,skyh给我yy半天也没说明白。所以先不瞎说了。
转一个大神yxs的证明。
暂且。。。凭感觉?反正这不是重要结论。
然后我们就可以愉快的三分了。三分的过程还是比较简单的吧。
但毕竟之前没见过,所以还是大概的说一说。
如果我们现在能够确定一个函数的最值(以本题最小值为例)就在某个特定区间(l,r)中
我们把这个区间分成等宽的3段,(l,m1),(m1,m2),(m2,r)
求出对于每一个点的函数值。
若f(m1)>f(m2),那么最小值不可能出现在m1的左端,否则从最小值到m1是增加的,从m1到m2是减小的,不符合单峰函数性质。
同理若f(m1)<f(m2)那么就能排除m2右边的区间。
这样每次可以缩小区间的1/3,复杂度是log级别。
那么对于每一个枚举的i所限定的特定区间,其操作次数为单峰函数,通过三分找到它的值,n log1.5 n。
据说mikufun/yxm有二分的做法(nlog2能AC),但据说是用了下面的性质,故不再赘述。
所以接下来就是O(n)算法咯!
性质2.决策具有单调性,即随着i的增大,j一定不会减小。
有了这条性质我们就可以愉快的做题了,快,去切了它!
咳,我们需要客观严谨的证明好伐?(虽说我证明也不是很严谨)
考虑我们决策的方法:不断枚举i,j是全局变量全程只增不减,也就是i++时j从上一个i所枚举的最后一个j开始。
只要j++能够使操作次数变小或不变,那么就使j++,否则break不再继续枚举j。
我们现在要证明它的正确性。
一共两种情况,一个是i++使一个0从区间最左边移动到了最右边,另一种移动的是1
先讨论第一种,考虑n=8,i=3->4,j=8,[]内是目前考虑的区间,|是分界线(位置任取),x可以是0或1任意
原区间:xxx[0xxxx|xxxx]0xxx
新区间:xxx0[xxxx|xxxx0]xxx
因为按照我们的决策方法,分界线向右移动当且仅当这么做会使答案更优或不变
那么反过来,只要我们严格按照这样的决策方式操作,分界线向左移一定不会使答案更优(可不变)
这样的话,我们把一个0从左端移动到右端,要动的所有的1并没有变化,故新区间对于这个分界线的答案和原区间一样。
故上述结论仍然成立,分界线左移不会使答案更优。
再考虑把1从左端移动到右端。
原区间:xxx[1xxxx|xxxx]1xxx
新区间:xxx1[xxxx|xxxx1]xxx
原区间里,第一个1不动还在位置4,对于分界线左边的其他所有的1分别要移动到位置5,6,7...右边的分别要移动到位置12,11,10...
在新区间里,区间的最左端位置是5了,除了位置4上的那个1以外,分界线左端的1的个数与位置均未发生变化。
现在,它们需要移动到的位置还是5,6,7...。
那个1跑到了新区间的最右端,没有关系,除了它以外,分界线右边的数字1的个数和位置也不变。
虽然区间最右端点扩展到了13,但是这个位置已经被那个1占据了,所以其他的分界线右边的数字1现在需要移动到的位置是12,11,10...
所以,可以发现,虽然有一个1的位置发生了变化,但是对于任意一条分界线,除了新的这个1以外,每一个原区间里的数字1它们的个数,位置,目标位置均没有发生变化,而在新旧区间当中,那个变化了的数字1一直都在端点上无需额外操作,故新旧区间对于同一个分界线,它们的答案值相同。
所以区间右移之后仍然满足“分界线左移不会使答案更优”的性质。
这样无论区间是如何移动的,分界线始终不会左移,证毕。
那么我们只要像证明里所说的那样进行决策,就能够保证最优答案一定会被枚举到,正确性有保证。
所以就是[在能够右移分界线j而不使答案变差时右移分界线,否则就右移区间i]。
完事!
对了。看数据范围了么?
咳,我要提醒什么,你应该知道了吧。。
如果你看到你的错解比别人内存小一半。。咳咳。。。
1 #include<cstdio> 2 #define int long long 3 int c[2000005],n,x[2000005],t;long long a[2000005],ans; 4 #define r register 5 inline long long min(const register long long a,const register long long b){return a<b?a:b;} 6 inline void read(){ 7 r char ch=getchar();n=0; 8 while(ch!='R'&&ch!='B')ch=getchar(); 9 while(ch=='R'||ch=='B')x[++n]=(ch=='R'?1:0),ch=getchar(); 10 } 11 signed main(){ 12 scanf("%d",&t); 13 while(t--){ 14 read();ans=(long long)1e15; 15 for(r int i=1;i<=n;++i)x[i+n]=x[i]; 16 for(r int i=1;i<=n*2;++i)c[i]=c[i-1]+x[i],a[i]=a[i-1]+i*x[i]; 17 r int j=1; 18 for(r int i=1;i<=n;++i){ 19 r long long bans=(long long)1e15; 20 for(r int jj=j;jj<=i+n+1;++jj){ 21 const r int ll=c[jj-1]-c[i-1],rr=c[i+n-1]-c[jj-1]; 22 const r long long rans=-ll*(ll-1ll)/2-rr*(rr-1ll)/2+a[jj-1]*2-a[i-1]-ll*i+rr*(i+n-1)-a[i+n-1]; 23 if(rans<=bans)bans=rans,j=jj;//,printf("%d %d %d %d %lld\n",i,j,ll,rr,rans); 24 else break; 25 ans=min(ans,bans); 26 } 27 } 28 printf("%lld\n",ans); 29 } 30 }