0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ABC436Eを解いた【サイクルの計算】

Last updated at Posted at 2025-12-14

筆者はレート800前後の茶~緑コーダ

ABC436当日にACできなかったE問題を解いていく

実装コード

解説によると

与えられた順列Pを使い、
頂点iと頂点P[i]を結ぶ辺を考えて、そのグラフの連結成分の個数Cを考えれば

最小回数はK=N-Cで求められるらしい…

そこからK回辺を結ぶ作業に言い換えられるようで、
どの辺を結べばいいのかというと、同じ連結成分にある2頂点を選ぶらしい

あとはそれぞれのサイクルの大きさを求めればOK…のようだが

上記の解説では全くわからないのでGPTに解説してもらった。

GPTの解説

main.py がやってることは、**「最小手数で直すとき、1手目として選べる swap (i, j) の個数」**を サイクル分解で数えてる、それだけです。以下、その「だから何?」を「なるほど」に変える説明いくね。


1 この問題、何を数えてる?

問題文はこう:P を swap だけで (1,2,...,N) にしたい。最小手数を K とする。その 最小手数 K を達成できる手順のうち、1手目としてあり得る (i, j) の数を出せ、という話。


2 「i と P[i] を結ぶグラフ」=置換のサイクルそのもの

解説が言ってるグラフ:

  • 頂点は 1..N
  • i に対して iP[i] を辺で結ぶ

P は置換(重複なし)だから、各頂点は

  • P[i] 側から見ても 1 回つながる
  • i -> P[i] も 1 回つながる

みたいな構造になって、**連結成分は全部「サイクル」**になります(解説にもそう書いてある)。

つまりここでいう連結成分数 C って、ふつうに

P をサイクル分解したときのサイクル個数
と同じ。


3 なぜ最小回数が K = N - C になるの?

ゴールの (1,2,...,N) は「全員が自分にいる」状態なので、サイクルが

  • (1)(2)...(N)N 個
    になります

だから、サイクル個数を C → N に増やす必要がある

ここで swap の効き方が (ここだけ分かれば勝ち):

  • i, j同じサイクルにいるとき swap すると、そのサイクルが 2つに割れて C+1
  • i, j別サイクルにいるとき swap すると、サイクルが 合体して C-1

つまり、1回の操作で C を増やせても 最大 +1
だから N まで増やすには最低でも N-C 回必要

さらに解説は「N > C の間は、同じ連結成分内の2頂点(例: (i, P[i]))が必ずあるから、C を 1 ずつ増やし続けられる」と言ってて、実際に達成できる。
よって 最小手数 K = N - C が確定。


4 で、「1手目としてあり得る swap」は結局どれ?

最小手数で行くなら、1手目で C を減らす(別サイクルを混ぜる)と、その分あとで取り返す必要が出て手数が増える。
だから 1手目は必ず「同じサイクル内の2頂点」を選ぶ必要がある。

そして解説の強い一言:

同じ連結成分にある 2 頂点であれば、どの組を選んでもよい

つまり答えは

各サイクルの中から 2 個選ぶ組の総数
です。

サイクルサイズが c なら、その中のペア数は c*(c-1)/2
よって答えは
$$
\sum_{\text{cycle}} \binom{c}{2}
$$
これ。


5 main.py がやってること(これで完全に一致)

コードは

  • まだ見てない頂点 i から P[i] を辿ってサイクル長 c を数える
  • そのサイクルに属する全頂点に ans[v] = c を入れる
  • 最後に print((sum(ans)-N)//2)

ここが気持ち悪く見えるポイントだと思うので変形すると:

  • 1つのサイクル(長さ c)では、anscc 個入る

    • だからそのサイクルの ans 合計は c*c
  • 全体で sum(ans) = Σ c^2

  • sum(ans) - N = Σ (c^2 - c) = Σ c(c-1)

  • それを //2 すると Σ c(c-1)/2 = Σ C(c,2)

つまり出力は、狙い通り **「同じサイクル内の (i<j) の総数」**になってる。


6 5秒で腑に落ちるミニ例

例:サイクルが (1 2)(3 4 5) の2個だとする(C=2N=5

  • 最小手数は K = 5 - 2 = 3

  • 1手目に選べるのは「同じサイクル内のペア」

    • (1 2) からは 1 通り
    • (3 4 5) からは 3 通り((3,4),(3,5),(4,5))
  • 合計 4 通り

式でも
2*1/2 + 3*2/2 = 1 + 3 = 4 で一致。

main.py
from bisect import bisect_left, bisect_right, insort_left, insort_right
from collections import defaultdict, Counter, deque
from functools import reduce, lru_cache
from itertools import product, accumulate, groupby, combinations
import sys
import os
def rI(): return int(sys.stdin.readline().rstrip())
def rLI(): return list(map(int,sys.stdin.readline().rstrip().split()))
def rI1(): return (int(sys.stdin.readline().rstrip())-1)
def rLI1(): return list(map(lambda a:int(a)-1,sys.stdin.readline().rstrip().split()))
def rS(): return sys.stdin.readline().rstrip()
def rLS(): return list(sys.stdin.readline().rstrip().split())
IS_LOCAL = int(os.getenv("ATCODER", "0"))==0
err = (lambda *args, **kwargs: print(*args, **kwargs, file=sys.stderr)) if IS_LOCAL else (lambda *args, **kwargs: None)

def main():
    N = rI()
    P = rLI1()

    ans = [0] * N
    
    for i in range(N):
        if ans[i]:continue
        c = 0
        p = i
        while True:
            c += 1
            p = P[p]
            if p == i: break
        while True:
            ans[p] = c
            p = P[p]
            if p == i: break
    print((sum(ans)-N)//2)
    
if __name__ == '__main__':
    main()

感想

順列を並び替える作業を
グラフに変換して辺を結ぶ作業に言い換えるっていう点から
なかなか高度な話だし

そこからサイクルを考えて計算しようという話も
正直あまりわかってない。

今回は昨日のABCの解き直しみたいな感覚で取り組んだが、
はっきり言って写経に近い実装になってしまったので、
今の地力を考えて解く問題を選んでいきたい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?