P1088 [NOIP2004 普及組] 火星人 多種解法(庫函式,手寫實現庫函式,康拓展開+變進位制數)
題目描述
人類終於登上了火星的土地並且見到了神祕的火星人。人類和火星人都無法理解對方的語言,但是我們的科學家發明了一種用數字交流的方法。這種交流方法是這樣的,首先,火星人把一個非常大的數字告訴人類科學家,科學家破解這個數字的含義後,再把一個很小的數字加到這個大數上面,把結果告訴火星人,作為人類的回答。
火星人用一種非常簡單的方式來表示數字――掰手指。火星人只有一隻手,但這隻手上有成千上萬的手指,這些手指排成一列,分別編號為1,2,3…1,2,3…1,2,3…。火星人的任意兩根手指都能隨意交換位置,他們就是通過這方法計數的。
一個火星人用一個人類的手演示瞭如何用手指計數。如果把五根手指――拇指、食指、中指、無名指和小指分別編號為1,2,3,41,2,3,4
三進位制數
123123123
132132132
213213213
231231
312312312
321321321
代表的數字
111
222
333
444
555
666
現在你有幸成為了第一個和火星人交流的地球人。一個火星人會讓你看他的手指,科學家會告訴你要加上去的很小的數。你的任務是,把火星人用手指表示的數與科學家告訴你的數相加,並根據相加的結果改變火星人手指的排列順序。輸入資料保證這個結果不會超出火星人手指能表示的範圍。
輸入格式
共三行。
第一行一個正整數NNN,表示火星人手指的數目(1≤N≤100001 \le N \le 100001≤N≤10000)。
第二行是一個正整數MMM,表示要加上去的小整數(1≤M≤1001 \le M \le 1001≤M≤100)。
下一行是11
輸出格式
NNN個整數,表示改變後的火星人手指的排列順序。每兩個相鄰的數中間用一個空格分開,不能有多餘的空格。
輸入輸出樣例
輸入 #15 3 1 2 3 4 5輸出 #1
1 2 4 5 3
說明/提示
對於30%的資料,N≤15N \le 15N≤15;
對於60%的資料,N≤50N \le 50N≤50;
對於全部的資料,N≤10000N \le 10000N≤10000;
首先我們需要知道一個性質(這個規律很容易看出來):每一個全排列序列對應一個確定的數
題意可轉化為:求一個給定全排列序列在m次變化之後得到的新序列
兩種思路:
1.從指定序列開始向後進行m次全排列
2.將指定序列轉換為一個數,對這個數進行+m的操作,再將變化後的數字還原為序列
1.
c++自帶一個next_permutation()函式,用法為next_permutation(a+1,a+n+1);(和sort差不多),執行一次後會得到a[]中全排列序列的下一個序列並儲存在a[]中
for(int i=1;i<=m;++i) next_permutation(ord+1,ord+1+n);
以上過程結束後ord[]中的序列就是答案
也可以選擇手動模擬該過程,先寫一個正常的全排列,然後從指定位置出發往後找m次即可
for(int i=1;i<=n;i++)
{
if(flag==0)i=a[step];
if(s[i]==0)
{
s[i]=1;
a[step]=i;
dfs(step+1);
s[i]=0;
}
}
其中
if(flag==0)i=a[step]; 即為最關鍵的一步,將當前全排列過程轉化為指定序列,其本質是將列舉全排列的i直接轉化到對應的a[i],即給定序列,當轉化完後,必定會有step>n的邊界情況 ,此時令flag++即可從當前序列開始正常的全排列
if(flagx==1)//當找到了目標序列後逐層退回
return;
if(step>n)
{
flag++;
if(flag==m+1)
{
for(int j=1;j<=n;j++)
printf("%d ",a[j]);
printf("\n");
flagx=1;
}
return;
}
2.
由於每一個全排列序列都對應這一個確定的數,且這個數的大小和序列的字典序大小相同,所以可以考慮先將序列轉化為一個數,對這個數進行+m,然後再將其還原為序列
拓展:全排列的字典序大小,以12345和12543為例,我們從第一位挨個比較過去,第一位都是1,第二位都是2,第三位5》3,所以12543的字典序比12345要大
引入一個概念:康拓展開
模擬一下康拓展開的過程,以45231為例
先看第一位4,有三個數{1,2,3}比4要小,以這三個數開頭的五位數顯而易見的全部小於以4開頭的五位數,後面還有四個位置可以放剩下四個數字,所以這些五位數的個數是:3*4!
再看第二位5,有四個數{1,2,3,4}比5要小,但是前面已經放了個4,所以只有剩下三個數,後面還剩下三個位置,所以這些五位數的個數是:3*3!
接下來同理
最後把所有個數相加,再加上自己這一種情況,得到的結果就是當前序列在所有全排列序列中的字典序排名
由此我們也可以得到康拓展開的本質:將一個數組轉換成一個數,康拓展開在hash中也有應用
那怎麼通過排名和序列長度反推出當前序列是多少呢?
引入一個概念:逆康拓展開
也就是把上面的過程反過來
以54321為例,已知長度是5,排名是120
首先把排名-1,減去自己的情況,得到119
119/(4!)=4....23,說明第一位的數字比4個數字要大,是5
23/(3!)=3....5,說明第二位的數字比3個數字要大,是4
5/(2!)=2....1,說明第三位的數字比2個數字要大,是3
1/(1!)=1....0,說明第四位的數字比一個數要大,是2
最後一個數字就是沒有用過的1
由此得到序列54321
但是容易發現,康拓展開是需要算階乘的,因此對於21!以下的範圍是比較好用的,再往上走就需要高精度或其他方法
那麼還有什麼方法呢?這裡引入一個概念:變進位制數
以54321為例,第一位有5種選擇,第二位就只有四種選擇(第一位的數字不能選了),以此類推,不難發現,對於第i位,一共有著n-i+1種選擇,那麼我們可以認為,對於第i位,他是n-i+1進位制的
接下來要做的就是把給定序列轉化為一個變進位制數,然後對這個數進行+m的操作,再將其還原為序列
以53142為例
第一位的5是{1,2,3,4,5}中的第五個選擇,所以是4(對於R進位制的一位,這一位的所有數字只能是從0到(R-1),滿R就要進位了)
第二位的3是{1,2,3,4}中的第三個選擇,所以是2
第三位的1是{1,2,4}中的第一個選擇,所以是0
第四位的4是{2,4}中的第二個選擇,所以是1
最後一位的2是{2}的第一個選擇,所以是0
由此我們得到了一個變進位制數42010,用(42010)unknown表示
接下來是+m的操作,我們以+3為例
42010+3=42013
最後一位(1進位制)向前進位得到42040
倒數第二位(2進位制)向前進位得到42200
第三位(3進位制)沒有問題,進位結束
此時我們得到了+m之後的結果(42200)unknown
接下來我們把這個數字還原為序列
(42200)unknown
第一位是4,代表著是{1,2,3,4,5}中的第5個選擇,所以是5
第二位是2,代表室{1,2,3,4}中的第3個選擇,所以是3
第三位是2,代表著{1,2,4}中的第3個選擇,所以是4
第四位是0,代表著{1,2}中的第1個選擇,所以是1
最後一位自然是沒用過的2
這樣我們就得到了序列53412
由於每一位的進位制和每一位可選擇的數字數量是相同的,所以不會出現明明只有3個數字可以選擇這一位卻出現4這種情況,換個說法,進位制數等於選擇數