【經常使用算法總結——康託展開&逆展開】

康託展開

  咳咳,首先咱們來看看康託展開的創始人c++

  

  沒錯,就是這個老爺子。數組

  他創造這個康託展開,通常用於哈希(可是我通常用的哈希字符串)在本篇隨筆中,它將用來求某排列的排名。(真神奇)優化

康託展開實現

  首先來一個柿子spa

  看不懂不要緊,咱們來一個例子:code

    首先,有三個數(1,2,3)它的組合以及排名和康託展開值以下blog

排列組合 排名 展開值
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,加上它本身就是第三位。排序

暴力康託

  在這裏,咱們每比較一位,都要向後(或者向前)遍歷,找到比這一位小而且還沒用過的數有多少個。而這也是最基本的康託展開。ci

  咱們來看看具體的代碼實現(以洛谷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一半,緣由是超時了,因此咱們就須要優化。get

樹狀數組&康託

  經過前面的介紹,咱們發現,若是該位置的數是還沒出現的數中的第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\n",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 }
相關文章
相關標籤/搜索