寿司:函数单调性

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 }
给出考场上的40分代码

 

接下来开始讲正解。。的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 }
981B
posted @ 2019-07-25 20:03  DeepinC  阅读(393)  评论(0编辑  收藏  举报