康托展開
咳咳,首先我們來看看康托展開的創始人
沒錯,就是這個老爺子。
他創造這個康托展開,一般用於哈希(但是我一般用的哈希字符串)在本篇隨筆中,它將用來求某排列的排名。(真神奇)
康托展開實現
首先來一個柿子
看不懂沒關系,我們來一個例子:
首先,有三個數(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大,那么就有(k−1)*(N−i)!種方案比這個排列小,也就是總排列比這個排列小,所以我們用一個樹狀數組來維護在剩下的數中,這一位是第幾大。
在最開始的時候,所有數都沒有出現,所以我們先初始化我們的樹狀數組,把比自己大的數的名次全部向上升一格
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(摘自百度)
在上面這道例題中,就是典型的康托逆展開,不過因為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 }