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 }