zoukankan      html  css  js  c++  java
  • 【康托展开&逆展开(康康康)】

    康托展开

      咳咳,首先我们来看看康托展开的创始人

      

      没错,就是这个老爷子。

      他创造这个康托展开,一般用于哈希(但是我一般用的哈希字符串)在本篇随笔中,它将用来求某排列的排名。(真神奇)

    康托展开实现

      首先来一个柿子

      看不懂没关系,我们来一个例子:

        首先,有三个数(1,2,3)它的组合以及排名和康托展开值如下

    排列组合 排名 展开值
    123 1 0*2!+0*1!+0*0!
    132 2 0*2!+1*1!+0*0!
    213 3 1*2!+0*1!+0*0!
    231 4 1*2!+1*1!+0*0!
    312 5 2*2!+0*1!+0*0!
    321 6 2*2!+1*1!+0*0!

      看其中的213,我们要想计算它的排名,首先看首位比它小的排列,只有1一个,所以就是1*2!,再看首位相等的第二位,没有比1小的,就是0*1!,最后看前两位相等第三位,虽然比3小的有1,2,但是前面用了,所以是0*0!,所以展开值就是2,加上它自己就是第三位。

    暴力康托

      在这里,我们每比较一位,都要向后(或者向前)遍历,找到比这一位小并且还没用过的数有多少个。而这也是最基本的康托展开。

      我们来看看具体的代码实现(以洛谷P5367 【模板】康托展开为例)

     1 #include<bits/stdc++.h>
     2 #define mod 998244353
     3 using namespace std;
     4 long long cantor[1000001]; //记录排列的每一位 
     5 long long fac[1000001];//阶乘 
     6 long long n,ans;
     7 int main()
     8 {
     9     cin>>n;
    10     for(long long i=1;i<=n;i++)
    11     {
    12         cin>>cantor[i];//输入排列的每一位 
    13     }
    14     fac[1]=1;
    15     for(long long i=2;i<=n;i++)
    16     {
    17         fac[i]=(fac[i-1]*i)%mod;//预处理阶乘,并且注意模一下 
    18     }
    19     for(long long i=1;i<=n;i++)
    20     {
    21         long long sum=0;
    22         for(long long j=i+1;j<=n;j++)
    23         {
    24             if(cantor[j]<cantor[i])sum++;
    25             //找到比这一位小的,并且前面没用过(也可以枚举前面的,sum=i-1,比这一个大sum--就行) 
    26         }
    27         ans=(ans+(sum*fac[n-i]))%mod;//累计康托展开值 
    28     }
    29     cout<<ans+1;//因为是比自己小的数量,还要加上自己 
    30 } 

      嗯,但是不难发现,照着这个打出来的代码交上去,会WA一半,原因是超时了,所以我们就需要优化。

    树状数组&康托

      通过前面的介绍,我们发现,如果该位置的数是还没出现的数中的第k大,那么就有(k1)*(Ni)!种方案比这个排列小,也就是总排列比这个排列小,所以我们用一个树状数组来维护在剩下的数中,这一位是第几大。

      在最开始的时候,所有数都没有出现,所以我们先初始化我们的树状数组,把比自己大的数的名次全部向上升一格

    1 for(int i=1;i<=n;i++)
    2     {
    3         update(i,1);
    4     }

      当每有一个数被选了,我们就要把比它大的数的名次降一格,来维护这些数在剩下的数中的排名

      所以整体的代码如下

     1 #include<bits/stdc++.h>
     2 #define mod 998244353
     3 using namespace std;
     4 long long n,ans;
     5 long long t[1000001];//树状数组 
     6 long long fac[1000001]; //阶乘 
     7 long long lowbit(long long x)
     8 {
     9     return x&-x;
    10 }
    11 void update(long long x,long long v)//区间修改 
    12 {
    13     while(x<=n)
    14     {
    15         t[x]+=v;
    16         x+=lowbit(x);
    17     }
    18 }
    19 long long sum(long long x)//单点查询 
    20 {
    21     long long p=0;
    22     while(x>0)
    23     {
    24         p+=t[x];
    25         x-=lowbit(x);
    26     }
    27     return p;
    28 }
    29 int main()
    30 {
    31     cin>>n;
    32     fac[0]=1;
    33     for(long long i=1;i<=n;i++)//预处理,初始化 
    34     {
    35         fac[i]=(fac[i-1]*i)%mod;
    36         update(i,1);
    37     }
    38     for(long long i=1;i<=n;i++)
    39     {
    40         long long a;
    41         scanf("%lld",&a);//long long一定是lld 
    42         ans=(ans+((sum(a)-1)/*因为是名次,加上了自己,所以要减一*/*fac[n-i])%mod)%mod;
    43         update(a,-1);//维护操作 
    44     }
    45     cout<<ans+1;//输出 
    46 } 

     康托逆展开

      既然有康托展开,那就有康托逆展开啊,所以我们接下来来了解了解一下康托逆展开吧。

      其实康托展开就是把一个字符串映射成一个整数k,而这个k就是这个字符串排列的名次。而这个展开又是一个双射,可以给你一个字符串输出k,也可以给你n(位数)和k,让你求出这个字符串。

    康托逆展开实现

      类似于进制转换的思想,

      {1,2,3,4,5}的全排列,并且已经从小到大排序完毕

      找出第96个数

      首先用96-1得到95

      用95去除4! 得到3余23

      有3个数比它小的数是4

      所以第一位是4

      用23去除3! 得到3余5

      有3个数比它小的数是4但4已经在之前出现过了所以第二位是5(4在之前出现过,所以实际比5小的数是3个)

      用5去除2!得到2余1

      有2个数比它小的数是3,第三位是3

      用1去除1!得到1余0

      有1个数比它小的数是2,第二位是2

      最后一个数只能是1

      所以这个数是45321(摘自百度)

     UVA11525 Permutation

      在上面这道例题中,就是典型的康托逆展开,不过因为n太大,所以它分别给你了遍历到每一位数时,在剩下未便利的数中有几个数比自己小,我们还是用树状数组来维护,但是这里又加上了一个二分查找的方法,代码就成形了。

     

     1 #include<bits/stdc++.h>
     2 using namespace std;
     3 int k,n,ans[1000001];//记录排列 
     4 int t[1000001];//树状数组 
     5 int lowbit(int x)
     6 {
     7     return x&-x;
     8 }
     9 void update(int x,int v)//区间修改 
    10 {
    11     while(x<=n)
    12     {
    13         t[x]+=v;
    14         x+=lowbit(x);
    15     }
    16 }
    17 int sum(int x)//单点查询 
    18 {
    19     int p=0;
    20     while(x>0)
    21     {
    22         p+=t[x];
    23         x-=lowbit(x);
    24     }
    25     return p;
    26 }
    27 int main()
    28 {
    29     cin>>k;
    30     while(k--)//样例个数 
    31     {
    32         scanf("%d",&n);
    33         for(int i=1;i<=n;i++)
    34         {
    35             update(i,1);//初始化 
    36         }
    37         int val;
    38         for(int i=1;i<=n;i++)
    39         {
    40             scanf("%d",&val);//一位一位的遍历 
    41             int l=1,r=n;
    42             while(l<r)//二分查找这个数 
    43             {
    44                 int mid=(l+r)/2;
    45                 int q=sum(mid)-1;
    46                 if(q<val)l=mid+1;
    47                 else r=mid;
    48             }
    49             update(r,-1);//并实锤这个数 
    50             ans[i]=r;//记录 
    51         }
    52         for(int i=1;i<n;i++)
    53         {
    54             printf("%d ",ans[i]);//输出控制一下格式 
    55         }
    56         printf("%d
    ",ans[n]);
    57     }
    58 } 

     

    展开&逆展开运用

       让我们来看看这一道(变态)CF501D Misha and Permutations Summation

      这道题正好是我们上面讲到的康托展开和逆展开的一个典型运用,题目大意就是给你两个排列,让你求出两个排列名次的总和,并对这个排列的所有可能取模,最后输出这个名次代表的排列。

      首先它可能脑子有问题,对于从0开始的排列,每一位加一就行了,最后输出减一即可。

      首先是得到名次的操作,刚开始肯定想着直接算出排列名次,但是看看n的范围(n≤200000)

      显然不行,我们就退一步,先想一想怎么得出的康托展开值

      拿213做例子,它的康托展开值等于1*2!+0*1!+0*0!

      我们再看看之前做逆康托展开的代码,不就是根据每个阶乘前面的数字(1,0,0)得到吗(可以用逆展开的代码试一下),所以我们就不用求出具体的排名了,只需要开一个数组(f[n])来记录每一个阶乘的数量就可以,然后从第一位开始,一位一位的进位,但是记住f[n]可以不用管,因为它对n!取模就没了.....最后就简化成康托逆展开的模板了。

     1 #include<bits/stdc++.h>
     2 using namespace std;
     3 int n;
     4 int a[2000001];//第一个排列 
     5 int b[2000001];//第二个排列 
     6 int f[2000001];//记录康托展开值 
     7 int t[2000001];//树状数组 
     8 int lowbit(int x)
     9 {
    10     return x&-x;
    11 }
    12 void update(int x,int v)//区间修改 
    13 {
    14     while(x<=n)
    15     {
    16         t[x]+=v;
    17         x+=lowbit(x);
    18     }
    19 }
    20 int sum(int x)//单点查询 
    21 {
    22     int ans=0;
    23     while(x>0)
    24     {
    25         ans+=t[x];
    26         x-=lowbit(x);
    27     }
    28     return ans;
    29 }
    30 void init()//初始化树状数组 
    31 {
    32     memset(t,0,sizeof(t));
    33     for(int i=1;i<=n;i++)
    34     {
    35         update(i,1);
    36     }
    37 }
    38 int main()
    39 {
    40     scanf("%d",&n);
    41     //记录排列,每一位加一方便计算 
    42     for(int i=1;i<=n;i++)
    43     {
    44         scanf("%d",&a[i]);
    45         a[i]++;
    46     }
    47     for(int i=1;i<=n;i++)
    48     {
    49         scanf("%d",&b[i]);
    50         b[i]++;
    51     }
    52     /*分别记录康托展开每一位的值,并把两个排列的累计起来 ,
    53     因为最后一位的康托展开值一定就是0,所以没必要*/ 
    54     init();
    55     for(int i=1;i<n;i++)
    56     {
    57         int ans=sum(a[i])-1;
    58         f[n-i]+=ans;
    59         update(a[i],-1);
    60     }
    61     init();
    62     for(int i=1;i<n;i++)
    63     {
    64         int ans=sum(b[i])-1;
    65         f[n-i]+=ans;
    66         update(b[i],-1);
    67     }
    68     
    69     for(int i=1;i<n;i++)//进位操作,就像3!*4=4!一样,只操作到n-1 
    70     {
    71         f[i+1]+=f[i]/(i+1);
    72         f[i]=f[i]%(i+1);
    73     }
    74     //康托逆展开 
    75     init();
    76     for(int i=n-1;i>=1;i--)//从高到低一位一位的输出 
    77     {
    78         int l=1,r=n,mid;
    79         while(l<r)
    80         {
    81             mid=(l+r)/2;
    82             if(sum(mid)-1<f[i])l=mid+1;
    83             else r=mid;
    84         }
    85         cout<<r-1<<" ";//因为之前加了一,所以后面减一即可 
    86         update(r,-1);
    87     }
    88     //输出最后一位,因为只剩一个数的排名是一,其他的都被搜到了,排名没有了 
    89     for(int i=1;i<=n;i++)
    90     {
    91         if(t[i])
    92         {
    93             cout<<i-1<<endl;
    94             return 0;
    95         }
    96     }
    97 }

     

     
     
  • 相关阅读:
    ubuntu12.04 安装eclipse
    loj 1429(可相交的最小路径覆盖)
    loj 1406(状态压缩)
    mysql数据库表插入单条数据/批量插入数据
    window7安装MongoDB详细步骤
    django中的setting全局变量的导入
    django.core.exceptions.ImproperlyConfigured: Requested setting DEFAULT_INDEX_TABLESPACE的解决办法(转)
    DjangoDRF序列化组件使用
    python并发编程知识点总结
    协程+IO切换实现并发
  • 原文地址:https://www.cnblogs.com/hualian/p/11233053.html
Copyright © 2011-2022 走看看