トップページ -> AOJの解答例 -> ALDS1_6の解答例

ALDS1_6の解答例(Python)

並び替えに関する問題です. アルゴリズムについて詳しく知りたい方は参考文献などをご覧ください.

1. ALDS1_6_A: Counting Sort

計数ソートに関する問題です. 疑似コードの通りに書きました.

# ALDS1_6_A
def CountingSort(A, B, k):
    # カウント用のリストを用意する
    C = [0 for _ in range(k+1)]
    
    # C[i] に i の出現数を記録する
    n = len(A)
    for j in range(n):
        C[A[j]] += 1
    
    # C[i] に i 以下の数の出現数を記録する
    for i in range(1,k+1):
        C[i] = C[i] + C[i-1]
        
    for j in range(n):
        j = n-j-1
        B[C[A[j]]-1] = A[j]
        C[A[j]] -= 1
        
n = int(input())
A = list(map(int,input().split(" ")))
B = [0 for _ in range(n)] # 並び替え後のリスト
k = max(A) # Aの最大値が必要
CountingSort(A, B, k)

print(*B)

2. ALDS1_6_B: Partition

リストの分割をする問題です. pからrまでの各要素がr番目の要素より大きいか小さいかを判定することで分割を行います. iはr番目の要素より大きい位置で止まり,j番目がr番目の要素より小さくなる位置に来たときに2つの要素の交換が起こります.(厳密には疑似コードはiはr番目の要素より大きい位置の1つ前で止まっています) 好みの問題ですが,説明の簡略化のために「iがr番目の要素より大きい位置で止まる」ようにiの始点とインクリメントの位置を変えています.
具体例
A=[1,3,6,5,2,4], p=0, r=5を考えます. つまり,A全体を4で分割します.
ステップ1.i=0, j=0: A[0]=1は4より小さいのでA[0]とA[0]の交換(実際には何も起こらない)をした後にiを1つ進めます.
ステップ2.i=1, j=1: ステップ1と同様 A[1]とA[1]の交換の後にiを1つ進めます.
ステップ3.i=2, j=2: A[2]=6は4より大きいので,交換やiのンクリメントは起きずiはこの位置に留まります.
ステップ4.i=2, j=3: A[3]=5は4より大きいので何も起きません.
ステップ5.i=2, j=4: A[4]=2は4より小さいのでA[i]とA[j]の交換が起こります.A=[1,3,2,5,6,4] 交換が成立したのでiを1つ前に進めます.
--------- ここでループは終了 ------------
ステップ6.i=3: j=0からr-1までの全てのA[r]より小さい要素は交換によりiより左側に来ている状態です.iはA[r]より大きい要素ですから,最後にA[r]とA[i]を交換します. A=[1,3,2,4,6,5]

# ALDS1_6_B
# A[p:r] を A[r]を基準に分割する
def partition(A, p, r):
    x = A[r]
    # i = p-1 # 疑似コードはこっち
    i = p # i=pに変えた
    for j in range(p,r):
        if A[j] <= x:
            # i = i+1 # 疑似コードはこっち
            # A[i] と A[j] を交換
            tmp = A[i]
            A[i] = A[j]
            A[j] = tmp
            i = i+1 # インクリメントの起こる位置を変えた
            
    # A[i+1] と A[r] を交換 疑似コードはこっち
#     tmp = A[i+1]
#     A[i+1] = A[r]
#     A[r] = tmp
#     return i+1

    # A[i] と A[r] を交換 i(xより大きい位置)とxを最後に交換
    tmp = A[i]
    A[i] = A[r]
    A[r] = tmp
    return i

n = int(input())
A = list(map(int,input().split(" ")))
r = len(A)-1 # rは配列 A の最後の要素を指す添え字
p = 0 # 全体を分割するのでp=0
partition_idx = partition(A, p, r)
for i in range(n):
    if i == partition_idx:
        print(f"[{A[i]}]",end="")
    else:
        print(f"{A[i]}",end="")
        
    # 最後のインデックス以外なら空白を入れる
    if i != n-1:
        print(" ",end="")
    
print() # 最後は改行

3. ALDS1_6_C: Quick Sort

クイックソートに関する問題です. Bの分割を使ってソートを行います. 分割の枢軸(partitionの戻り値)の左と右で再帰的にpartitionを呼び出すことでソートします. quickSort(A, p, q-1)が左側の処理,quickSort(A, q+1, r)で右側の処理を行います.
動作例
A=[1,3,6,5,2,4]をクイックソートします.
ステップ1: p=0, r=5: 分割の結果,A=[1,2,3,4,6,5]になりq=3です. 左側[1,2,3]と右側[6,5]です.quickSort(A, 0, 2)とquickSort(A, 4, 5)が起こります.
ステップ2(右 quickSort(A, 0, 2)): p=0, r=2: すでに分割が完了しているのでAは変わらず,q=2です. quickSort(A, 0, 1) と quickSort(A, 3, 2) が起こります.
ステップ2(左 quickSort(A, 4, 5)): p=4, r=5: [6,5]の5での分割は[5,6]です.A=[1,2,3,4,5,6] q=4です.quickSort(A, 4, 3) と quickSort(A, 5, 5) が起こります.
-------- 既にソートは完了しました -------------
ステップ3.quickSort(A, 4, 3), quickSort(A, 5, 5), quickSort(A, 3, 2)は p<r を満たさないのでそのまま終了です.
ステップ3(quickSort(A, 0, 1)): p=0, r=1: [1,2]の分割の結果 q=1で quickSort(A, 0, 0), quickSort(A, 2, 1) が呼び出されますがいずれもp<rを満たさないので終了です.
平均計算量O(n log(n2))と高速ですが,動作例から分かるように分割の枢軸が端に来てしまうと,リストの長さが1ずつしか短くなっていかず効率が悪くなります. 最悪計算量はO(n^2)となるため,改善方法としては分割を行う部分配列の左端,中央,右端の3点の中央値で分割をするなどがあるようです.(今回の問題には関係ありません)

# ALDS1_6_C
# A[p:r] を A[r]を基準に分割する
def partition(A, p, r):
    x = A[r][1] # カード用に[1]を付けた
    i = p-1
    for j in range(p,r):
        if A[j][1] <= x: # カード用に[1]を付けた
            i = i+1
            # A[i] と A[j] を交換
            tmp = A[i]
            A[i] = A[j]
            A[j] = tmp
            
    # A[i+1] と A[r] を交換
    tmp = A[i+1]
    A[i+1] = A[r]
    A[r] = tmp
    return i+1

def quickSort(A, p, r):
    if p < r:
        q = partition(A, p, r)
        quickSort(A, p, q-1)
        quickSort(A, q+1, r)
        
n = int(input())
card_list = []
for _ in range(n):
    card = input().split(" ")
    card = [card[0],int(card[1])]
    card_list.append(card)
    
# 順序を記録しておく
order_dict = {}
for card in card_list:
    if card[1] in order_dict.keys():
        order_dict[card[1]].append(card)
    else:
        order_dict[card[1]] = [card]

quickSort(card_list, 0, n-1)

# 並べ替え後の順序を調べる
order_dict2 = {}
for card in card_list:
    if card[1] in order_dict2.keys():
        order_dict2[card[1]].append(card)
    else:
        order_dict2[card[1]] = [card]

# 並べ替えの前後で順序が同じならStable
if order_dict == order_dict2:
    print("Stable")
else:
    print("Not stable")

for card in  card_list:
    print(*card)

4. ALDS1_6_D: Minimum Cost Sort

ソートを最小のコストで行う問題です. 自力で解くことができず,test caseと見比べながら考えたためおかしいかもしれません. 思考の過程も書いておきます.

試したこと1

問題が起きない場合もあるのですが,分かりやすいパターンですと[2,1,60,50,30,40]などの場合に
[1,2,60,50,30,40](cost=3)→[1,2,60,30,50,40](cost=80)→[1,2,60,40,50,30](cost=70)→[1,2,30,40,50,60](cost=90) 合計コスト243
[2,30,60,50,1,40](cost=31)→[2,30,60,1,50,40](cost=51)→[2,30,60,40,50,1](cost=41)→[2,30,1,40,50,60](cost=61)→[2,1,30,40,50,60](cost=31)→[1,2,30,40,50,60](cost=3) 合計コスト218
のように問題が起こります.最小要素が途中で正しい位置に収まってしまうと問題が起きるように見えたので次のことを試しました.

試したこと2

こうすると上の場合でも1と30が最初に交換されるため正しい解答を出すことができました.
----------- 以下,手詰まりでテストケースを確認 -------------
いくら考えてもできそうになかったので通らなかったテストケースを確認したところ[0 62 33 29 72 52 43 19 5 93 51 32 8 11 56 47 66 69 30 17 68]で詰まっていました. よく考えてみれば[1,3,2,60,50,30,40]の場合に正しい位置にある1を動かそうとしないため問題が起こります.通らなかったテストケースでも0を動かそうとしないために問題が起きているのだと思いコードを変えたのですが,それでも解答が一致しませんでした. 「1個しか揃わないような次の最小の要素と入れ替える」に問題があったようで,それ以外の要素と入れ替えると改善される場合があることが分かりました. さらに次のことを試しました.

試したこと3

最小の値の移り変わりを見ると,0→5→8→17と移り変わっていました. 一番最初の方法で交換を行うと2個揃って最小値が移り変わるような場合にその最小値を使って揃えることができるようなグループで分けることができます.(最後まで最小値が変わらずに並び替えを起こることができる部分配列にグループ分けできる) [32, 47, 5, 62]は0と5を入れ替えてもコストが減らないのに対して[51, 43, 29, 11, 52, 19, 30, 69, 68, 93, 8, 33]は0と8を入れ替えてから処理するとコストが減ります. また,いずれの場合もソートが完了した後に0(最初に交換を行った最小値)は元の位置に戻ってきます. よく考えると違うグループの任意の要素同士を交換することで1つのグループに結合することができます. 例えば,グループ1:[32, 47, 5, 62], グループ2:[51, 43, 29, 11, 52, 19, 30, 69, 68, 93, 8, 33] は5と8を入れ替えることでコスト13で1つにまとめることができます. 下のコードのgroup_cost通りに場合分けをしてコストを手計算すると結合しないほうがコストが少なくなることが分かります.
結合しないほうがコストが少なくなることの証明 (PDF)
今まで出てきた「グループ」とは実は巡回置換のことなのですが,数学が分かる方は以下のサイトのほうが分かりやすいかもしれません(?)
【外部サイト】最小費用ソート(ALDS1_6_D)の解法の正当性(メモ)
一応,以下のコードで通りますが,これまでの思考の過程に間違っている部分があるかもしれません. あまりにも私の能力を超える問題だったので途中から元気が無くなっています...
特にswap_min()が試行錯誤の名残で無駄なことをしていますが,グループ分け(巡回置換の積に書き直す)をしているだけです.

# ALDS1_6_D
n = int(input())
W = list(map(int,input().split(" ")))
W_sorted = sorted(W)

cost = 0
rest_W = W[:]

group_list = []

def group_cost(group,min_w):
    # 追加しない場合 (最小値が移動する回数) * (最小値) + (他の要素の1回ずつの移動)
    cost1 = (len(group)-1) * min(group) + sum(group) - min(group)
    # 追加する場合 (最小値が移動する回数) * (最小値) + (グループの最小を最初に交換するコスト) + (他の要素の1回ずつの移動)
    cost2 = min_w * (len(group)+1) + min(group) + sum(group)
    return min(cost1,cost2)

# 試行錯誤の名残で最小値を探しているだけ
def swap_min(group=[]):
    min_w = min(rest_W) # 最小の物を探す
    min_pos = rest_W.index(min_w)
    w_pos = W.index(W_sorted[min_pos]) # その位置に収まるべき物を探す

    # 入れ替えを行う
    tmp = W[w_pos]
    W[w_pos] = min_w
    W[min_pos] = tmp
    
    rest_W[min_pos] = float("inf") # min_posには必ず正しいものが入る
    if min_pos == w_pos:
        group.append(tmp)
        group_list.append(group)
        group = []
        return group
    
    # 入れ替えの結果 w_posも正しくなった場合
    if W_sorted[w_pos] == W[w_pos]:
        rest_W[w_pos] = float("inf")
        group.append(min_w)
        group.append(tmp)
        group_list.append(group)
        group = []
        return group
    else:
        rest_W[w_pos] = min_w
        group.append(tmp)
        return group
    
# グループ分けをする(巡回置換の積に書き直す)
group = []
while len(set(rest_W)) > 1:
    group = swap_min(group) 
    
cost = 0
min_w = min(W)
for group in group_list:
    # print(group,group_cost(group,min_w))
    cost += group_cost(group,min_w)
    
print(cost)

<- 前へ戻る 【目次に戻る】 次へ進む ->