🔗 UVA-941 Permutations

tags: 數學(Math) 遞迴(Recursion) 回溯(Backtracking)

範例程式碼已於UVA瘋狂程設(CPE)ZeroJudge 上皆測試通過。

題意

給定一個字串 SS 和一個整數 NN,找到字串 SS 的所有排列中的第 N+1N+1 個最小元素(按照字典序)。這些排列是對字串的所有可能組合,並且按照字典序排序。

注意,給定的 SS 可能沒有事先排序。

約束

  • 1S201 \leq |S| \leq 20
  • 1N+120!1 \leq N + 1 \leq 20!

思路:數學(Math) + 縮小問題範圍

由於 S20S \leq 20,而 20!=243290200817664000020! = 2432902008176640000,顯然生成所有排列不是一個好方法。因此應該使用數學方法計算排列的位置,而不是生成所有排列。

舉個🌰,假設 S="abcde"S = \rm{"abcde"}N=15N = 15,改成用 0indexed0\rm{-indexed} 表示。

  • 由於需要按照字典序排序,故前 [0,23][0, 23] 個排列的第 11 個元素都是 aa;前 [24,47][24, 47] 個排列的第 11 個元素都是 bb,以此類推。而在確定了第一個元素後,也能按照相同的方法繼續確定之後的元素,但後續就不用考慮已經確定的字元。
  • 確定答案的第 11 個元素為 "a"\rm{"a"} 後,剩餘的元素為 "bcde"\rm{"bcde"}。且 N=15mod4!=15N = 15 \mod 4! = 15 ,故接下來是找到在 "bcde"\rm{"bcde"} 的排列中的第 1515 個排列。
  • "bcde"\rm{"bcde"} 的排列中,前 [0,5][0, 5] 個排列的第 11 個元素都是 bb;前 [6,11][6, 11] 個排列的第 11 個元素都是 cc;前 [12,17][12, 17] 個排列的第 11 個元素都是 dd;前 [18,23][18, 23] 個排列的第 11 個元素都是 ee。由於 N=15N = 15 ,因此答案的第 22 個元素是 dd ,剩餘的元素為 "bce"\rm{"bce"}。且 N=15mod3!=3N = 15 \mod 3! = 3 ,故接下來是找到在 "bce"\rm{"bce"} 的排列中的第 33 個排列。
  • "bce"\rm{"bce"} 的排列中,前 [0,1][0, 1] 個排列的第 11 個元素都是 bb;前 [2,3][2, 3] 個排列的第 11 個元素都是 cc;前 [4,5][4, 5] 個排列的第 11 個元素都是 ee。由於 N=3N = 3 ,因此答案的第 33 個元素是 cc ,剩餘的元素為 "be"\rm{"be"}。且 N=3mod2!=1N = 3 \mod 2! = 1 ,故接下來是找到在 "be"\rm{"be"} 的排列中的第 11 個排列。
  • "be"\rm{"be"} 的排列中,前 [0,0][0, 0] 個排列的第 11 個元素都是 bb;前 [1,1][1, 1] 個排列的第 11 個元素都是 ee。由於 N=1N = 1 ,因此答案的第 44 個元素是 ee ,剩餘的元素為 "b"\rm{"b"}。且 N=1mod1!=0N = 1 \mod 1! = 0
  • 由於只剩下一個元素,故答案的第 55 個元素是 bb。答案為 "adceb"\rm{"adceb"}

理解上述範例後,可以將其拆解成以下步驟:

  1. SS 排序
  2. 逐位確定每個元素,由於相同字元不能重複使用,因此每次確定一個元素後,要將其從 SS 中刪除
    • 透過計算階乘來計算每個元素的位置
    • 透過取餘數來計算下一個元素的位置
  3. 重複步驟 22 直到 SS 為空

複雜度分析

  • 時間複雜度:O(m2)\mathcal{O}(m^2) ,其中 mmSS 的長度。
    • 計算 factorial\rm{factorial} 的時間複雜度為 O(m)\mathcal{O}(m)
    • 排序 SS 的時間複雜度為 O(mlogm)\mathcal{O}(m \log m)
    • 每次確定元素時,需要刪除被確定的元素,需要 O(m)\mathcal{O}(m) 的時間,因此總共的時間複雜度為 O(m2)\mathcal{O}(m^2)
  • 空間複雜度:O(m)\mathcal{O}(m)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from math import factorial

t = int(input())

for _ in range(t):
s = sorted(input()) # 記得排序
n = int(input())

ans = ""
while len(s) > 0:
x = factorial(len(s)-1)
i = n // x # 回溯法要逐個確定,但其實可以直接計算
ans += s[i]
s = s[:i] + s[i+1:] # 刪除已經用過的字元
n %= x
print(ans)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 20;
#define endl '\n'

LL fact[N]; // 預處理 N! 的值

void init() {
fact[0] = 1;
for (int i=1; i<N; i++) {
fact[i] = fact[i-1] * i;
}
}

int main() {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
LL t, n, x, i;
cin >> t;
init();
string s, ans;
while (t--) {
cin >> s >> n;
ans = "";
sort(s.begin(), s.end());
while (s.size() > 0) {
x = fact[s.size()-1];
i = n / x;
ans += s[i];
s.erase(i, 1);
n %= x;
}
cout << ans << endl;
}
return 0;
}

類題

寫在最後

Cover photo is remixed from @Ice maker, thanks for their work!

感覺自己的說明還是不夠清楚,這題建議直接從範例下去理解。