題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。

🔗 ABC437E Sort Arrays

rating: 1245

Problem Statement

題意簡述

N+1N+1 個序列 A0,A1,,ANA_0, A_1, \ldots, A_N,定義如下:

  • A0A_0 是一個空序列 ()
  • 對於 i=1,,Ni = 1, \ldots, N,序列 AiA_i 是將整數 yiy_i 追加到序列 AxiA_{x_i} 的末尾 (0xi<i0 \le x_i < i)。
    • 例如:若 Axi=(2,3)A_{x_i} = (2, 3)yi=5y_i = 5,則 Ai=(2,3,5)A_i = (2, 3, 5)

請將這 NN 個序列 (A1,,ANA_1, \ldots, A_N) 依照字典序由小到大排序後,給出由 AiA_i 的下標 ii 構成的一個排列 P=(P1,,PN)P = (P_1, \ldots, P_N)

  • 如果兩個序列內容完全相同 (Ai=AjA_i = A_j),則索引較小的排在前面(即若 i<ji < j,則 ii 排在 jj 之前)。
字典序 (Lexicographical Order) 定義

序列 SS 字典序小於 TT (S<TS < T),若滿足以下任一條件:

  1. SSTT 的前綴,且 SS 的長度小於 TT (例如 (1, 2) < (1, 2, 3))。
  2. 在兩者第一個數值不同的位置 kkSk<TkS_k < T_k (例如 (1, 2, 5) < (1, 4, 1),因為在第 2 位 2<42 < 4)。

Constraints

約束條件

  • 1N3×1051 \le N \le 3 \times 10^5
  • 0xi<i0 \le x_i < i
  • 1yi1091 \le y_i \le 10^9

Example

Input 1

1
2
3
4
5
4
0 2
0 1
2 2
0 1

Output 1

1
2 4 3 1
解釋

  • A1=(2)A_1 = (2) (接在 A0A_0 後)
  • A2=(1)A_2 = (1) (接在 A0A_0 後)
  • A3=(1,2)A_3 = (1, 2) (接在 A2A_2 後)
  • A4=(1)A_4 = (1) (接在 A0A_0 後)

排序後:

  1. A2=(1)A_2 = (1)
  2. A4=(1)A_4 = (1) (與 A2A_2 相等,索引較大排後)
  3. A3=(1,2)A_3 = (1, 2)11 開頭,長度較長)
  4. A1=(2)A_1 = (2)

故順序為 2,4,3,12, 4, 3, 1

思路:在 Trie 上進行 DFS 遍歷

Trie 結構模擬

這道題目的序列生成過程本質上就是在構建一棵 Trie (字典樹)。

  • A0A_0 是 Trie 的根節點。
  • 每個操作 (xi,yi)(x_i, y_i) 代表從 Trie 節點 xix_i 延伸出一條邊,邊權為 yiy_i,到達一個新的狀態(節點)。
  • 由於題目保證 xi<ix_i < i,這棵樹是隨著輸入依序建立的,不會有環。
直觀理解:樹結構

這個生成過程實際上構成了一棵以 00 為根的樹(Trie):

  • 每個索引 ii 代表樹上的一個節點。
  • AiA_i 是由 AxiA_{x_i} 延伸而來,這相當於樹上從父節點 xix_i 到子節點 ii 有一條權重為 yiy_i 的邊。
  • 序列 AiA_i 的內容,就是從根節點 00 走到節點 ii 的路徑上所有邊權組成的序列。

比較兩個序列 Au,AvA_u, A_v 的字典序:

  1. 前綴關係:若節點 uu 是節點 vv 的祖先,則 AuA_uAvA_v 的前綴,故 Au<AvA_u < A_v
  2. 分歧點:若兩者在某個節點分岔,則比較分岔邊的權重 yy 大小。

處理相同序列

題目要求當序列相同時,索引小的排在前面。
在我們的 Trie 結構中,每個節點代表一個獨一無二的序列內容。
我們可以讓每個 Trie 節點維護一個列表 nodes,儲存所有「終止於此」的序列編號 ii
由於我們是按照 i=1Ni = 1 \sim N 的順序讀入並建立 Trie,對於同一個節點,其 nodes 列表中的編號自然是遞增的。

DFS 遍歷求字典序

字典序的排序對應到 Trie 上,就是 前序遍歷 (Pre-order Traversal) 的過程,但在訪問子節點時,必須按照邊權 (yy) 由小到大的順序進行。

具體流程如下:

  1. 建樹:讀取輸入,利用 mp 陣列紀錄每個編號 ii 對應到的 Trie 節點。若該節點不存在則建立。將 ii 加入該節點的 nodes 列表。
  2. DFS:從根節點開始遍歷。
    • 當到達節點 uu 時,首先輸出 u.nodes 中的所有編號。這是因為 AuA_u 是其所有子樹中序列的前綴,根據字典序定義,前綴必定排在前面。
    • uu 的所有子邊按照權重 yy 排序。
    • 依序遞迴訪問子節點。

實作細節

  • 由於 NN 高達 3×1053 \times 10^5,Python 的預設遞迴深度可能不足,需使用 sys.setrecursionlimit 或改寫為迭代形式 (Iterative DFS)。
  • 參考程式碼中使用了迭代方式(Stack)來模擬 DFS,避免遞迴過深的問題。
  • 使用 defaultdict 來動態建立 Trie 的邊,節省空間。

複雜度分析

  • 時間複雜度O(NlogN)\mathcal{O}(N \log N)
    • 建立鄰接表(樹結構)需要 O(N)O(N)
    • 對於每個節點 uu,若其有 dud_u 個子節點,我們需要對這些子節點進行排序,耗時 O(dulogdu)O(d_u \log d_u)
    • 總排序時間為 dulogdu\sum d_u \log d_u。由於 du=N\sum d_u = N,在最壞情況下(星狀圖),這會達到 O(NlogN)O(N \log N)
    • DFS 遍歷每個節點一次,耗時 O(N)O(N)
  • 空間複雜度O(N)\mathcal{O}(N)
    • 需要儲存鄰接表(NN 個節點,NN 條邊),空間為 O(N)O(N)
    • DFS 的遞迴深度或迭代堆疊在最壞情況下(鏈狀圖)為 O(N)O(N)

Code

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
37
38
39
40
41
42
43
44
45
46
from collections import defaultdict
import sys
sys.setrecursionlimit(int(3e5 + 5))

class TrieNode:
__slots__ = ['child', 'nodes']
def __init__(self):
self.child = defaultdict(TrieNode)
self.nodes = []

def solve():
n = int(input())

root = TrieNode()

mp = [None] * (n + 1)
mp[0] = root
for u in range(1, n + 1):
fa, y = map(int, input().split())
mp[u] = mp[fa].child[y]
mp[u].nodes.append(u)

ans = []

# def dfs(u: TrieNode):
# ans.extend(u.nodes) # u.nodes 必定有序
# for _, v in sorted(u.child.items(), key=lambda x: x[0]):
# dfs(v)
# dfs(root)

st = [(root, 0)]
while st:
u, i = st.pop()
if i == 0:
ans.extend(u.nodes)
u.child = sorted(u.child.items(), key=lambda x: x[0])
for j in range(i, len(u.child)):
v = u.child[j][1]
st.append((u, i + 1))
st.append((v, 0))
break

print(*ans)

if __name__ == "__main__":
solve()

寫在最後

PROMPT

這裡什麼都沒有。