1. 程式人生 > 其它 >P1088 [NOIP2004 普及組] 火星人 多種解法(庫函式,手寫實現庫函式,康拓展開+變進位制數)

P1088 [NOIP2004 普及組] 火星人 多種解法(庫函式,手寫實現庫函式,康拓展開+變進位制數)

題目描述

人類終於登上了火星的土地並且見到了神祕的火星人。人類和火星人都無法理解對方的語言,但是我們的科學家發明了一種用數字交流的方法。這種交流方法是這樣的,首先,火星人把一個非常大的數字告訴人類科學家,科學家破解這個數字的含義後,再把一個很小的數字加到這個大數上面,把結果告訴火星人,作為人類的回答。

火星人用一種非常簡單的方式來表示數字――掰手指。火星人只有一隻手,但這隻手上有成千上萬的手指,這些手指排成一列,分別編號為1,2,3…1,2,3…1,2,3。火星人的任意兩根手指都能隨意交換位置,他們就是通過這方法計數的。

一個火星人用一個人類的手演示瞭如何用手指計數。如果把五根手指――拇指、食指、中指、無名指和小指分別編號為1,2,3,41,2,3,4

1,2,3,4555,當它們按正常順序排列時,形成了555位數123451234512345,當你交換無名指和小指的位置時,會形成555位數123541235412354,當你把五個手指的順序完全顛倒時,會形成543215432154321,在所有能夠形成的120120120555位數中,123451234512345最小,它表示111123541235412354第二小,它表示222543215432154321最大,它表示120120120。下表展示了只有333根手指時能夠形成的666333位數和它們代表的數字:

三進位制數

123123123
132132132
213213213
231231

231
312312312 321321321

代表的數字

111
222
333
444
555
666

現在你有幸成為了第一個和火星人交流的地球人。一個火星人會讓你看他的手指,科學家會告訴你要加上去的很小的數。你的任務是,把火星人用手指表示的數與科學家告訴你的數相加,並根據相加的結果改變火星人手指的排列順序。輸入資料保證這個結果不會超出火星人手指能表示的範圍。

輸入格式

共三行。
第一行一個正整數NNN,表示火星人手指的數目(1≤N≤100001 \le N \le 100001N10000)。
第二行是一個正整數MMM,表示要加上去的小整數(1≤M≤1001 \le M \le 1001M100)。
下一行是11

1NNNNNN個整數的一個排列,用空格隔開,表示火星人手指的排列順序。

輸出格式

NNN個整數,表示改變後的火星人手指的排列順序。每兩個相鄰的數中間用一個空格分開,不能有多餘的空格。

輸入輸出樣例

輸入 #1
5
3
1 2 3 4 5
輸出 #1
1 2 4 5 3

說明/提示

對於30%的資料,N≤15N \le 15N15

對於60%的資料,N≤50N \le 50N50

對於全部的資料,N≤10000N \le 10000N10000

首先我們需要知道一個性質(這個規律很容易看出來):每一個全排列序列對應一個確定的數

題意可轉化為:求一個給定全排列序列在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這種情況,換個說法,進位制數等於選擇數