GYM 102979 I. Integer Array Shuffle 题解

这题是很强的思维题,下面分阶段介绍:

定义1:最关键的一点就是把这题转化为'升降向量'(指整个序列的升和降的构成的序列,例如1 4 3 2 5的升降向量就是升(1~4),降(4~2),升(2~5), 下文将用01串表示升降向量,例如前例的升降向量为:101)

发现 1: 最终要得到的是递增的序列,对应的升降向量是1,那么对于原序列,我们直观地将问题转化为将原序列的升降向量一步一步转化(实际上是缩短升降向量的长度)为升降向量1

发现2 : 任何序列的升降向量都是01交替出现的

引理1: shuffle过程中,我们可以将升降向量的首尾合并,结果为首元素,

    例如升降向量01(假设原序列是4 3 1 2 5),我们可以先将向新序列插入5,4,3,2,1,这样新序列的升降向量就是0,

    (消去升降向量的首尾元素后,第2和倒数第2元素(若存在)顺势成为升降向量的首尾元素)

    更一般地说:本题的shuffle指的是可选择从原序列首或尾取出元素,添加到新序列的队尾,

    对于首为降,尾为升的序列,我们从原序列首或尾取出元素总能将这两块序列(开头的降(上例的4 3),和末尾的升(上例的1 3 5))合并为降的一块序列

    对于首为升,尾为降的序列,我们从原序列首或尾取出元素总能将这两块序列合并为升的一块序列

引理2: 经过完整的一次shuffle后,我们可以将升降向量的长度n减小为n/2(向上取整)

    具体操作为:不断地进行升降向量的首尾合并(引理1),

    特别地,n为奇数时升降向量首尾相同,可以先将尾放到头(此时是进行了翻转,所以0变1,1变0),使倒数第二个成为尾;或保留首,使第二个成为首

 

例子:升降向量是010101010,

第1次shuffle:1 01010101(尾放到头)->首尾合并->10101

第2次shuffle:1 0101(保留首)->首尾合并->101

第3次shuffle:1 01(保留首)->首尾合并->10

第4次shuffle:首尾合并->1

 

大体做法如上述,过程中的一些证明由读者自己体会,我没精力给严格的证明

 

所以算法的流程为:

1.将序列转化为升降向量

2.合并升降向量(偶数时直接首尾合并,其实从结果看就是取升降向量的一半;奇数时考虑两种策略),直到升降向量是1为止

3.输出操作次数

 

tip:对于01010这种升降向量,

错误做法为01010->010(留首)->01(留首)->0->1,花费4步

正确做法为01010->101(翻尾)->10(留首)->1或01010->010(留首)-->10(翻尾)-->1,花费3步

最终结论是:只有n为二幂数(即为2^k,一种等价定义是没有奇因子(1除外)的数,二幂数也被我称作纯偶数)且升降向量以0开头时答案为log2n(向上取整)+1,其他情况答案为log2n(向上取整); 这个结论留给读者思证

 

优化1: 关于1.将序列转化为升降向量, 可以通过另一种途径实现:

数 序列的谷数,一个谷代表一个降升对,最后再考虑首尾的升降性即可,例如4 1 3 2 5 1有2个谷,首降尾降,故升降向量为01010,前四位由谷得出,最后一位由尾的升降性得出.

  ps:关于这点,要小心原序列是可能有重复元素的,4 1 1 1 3这种序列是有一个谷的!

 

//虚空debug,没考虑元素可以相同导致数谷出了问题,这谁看得出来啊
//数谷法对于统计升降向量很有用
//①处的统计第一个是上山还是下山冗余了,注意一下

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<string>
#include<string.h>
#include<cmath>

#define rep(i,a,b) for(ll i=a;i<=b;++i)
#define per(i,a,b) for(ll i=a;i>=b;--i)
#define fi first
#define se second
#define mp make_pair
#define all(x) x.begin(),x.end()
#define debug(x) cout<<# x <<" is "<<x<<endl;

using namespace std;
typedef long long ll;
typedef pair<ll,ll> PII;
const int maxn=3e5+10,inf=0x3f3f3f3f,mod=1e9+7;

int a[maxn]={0};
int main()
{
    int n;cin>>n;

    int flag0=1;        
    rep(i,1,n) {
        cin>>a[i];
        if(a[i]<a[i-1]) flag0=0;
    }
    if(flag0) {cout<<0<<endl;return 0;}    //单升


    //消去相同元素
    int nn=1;
    rep(i,2,n){
        if(a[i]!=a[i-1]) a[++nn]=a[i];
    }
    n=nn;

    int countgu=0;
    rep(i,2,n-1){
        if(a[i]<a[i-1]&&a[i]<a[i+1]) {
            countgu++;    //谷的数量,代表降升对的数量
        }
    }

    int flag1,flag2;
    rep(i,1,n-1){
        if(a[i]<a[i+1]) {flag1=1;break;}
        else if(a[i]>a[i+1]) {flag1=0;break;}    //①降
    }
    per(i,n,2){
        if(a[i]<a[i-1]) {flag2=0;break;}    //
        if(a[i]>a[i-1]) {flag2=1;break;}
    }    //表示第一个,最后一个的升降

    int count_updown=2*countgu+flag1+!flag2;    //一升,尾降各加一

    if(count_updown<=2)    {    //必要条件:只有一个谷或0个谷
        if(countgu==1) cout<<2<<endl;    //降升
        else    cout<<1<<endl;    //升降和单降
        return 0;
    }

    int ans=0;
    bool flag_noji=!flag1;    //首降,之后若是二幂数则需要加一次
    while(count_updown>1){ 
        if(count_updown&1) flag_noji=0;
        ans++;
        count_updown=(count_updown+1)/2;
    }

    cout<<ans+flag_noji<<endl;
    return 0;
}
/*

7
1 2 1 2 1 2 1
ans=3

7 
2 1 2 1 2 1 2
ans=3


17
2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 
ans:5
33
2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 
ans:6
9
2 1 2 1 2 1 2 1 2
ans:4

10
8 20 4 4 18 18 12 9 5 5
ans=2

10
13 13 8 8 10 11 7 13 19 3 
ans=3

*/
View Code

 

posted @ 2021-03-20 12:23  Laozhu1234  阅读(90)  评论(0编辑  收藏  举报