AtCoder Beginner Contest 386 (ABC386) G - Many MSTの勉強

問題

G - Many MST

解法

Editorial - AtCoder Beginner Contest 386

kyopro_friendsさんのユーザ解説の方法で解く。 ユーザ解説同様、問題文中の  M K でおき直す。

無向グラフ  G の連結成分数を  C(G) とする。 また、全ての辺の重みが  1 以上  K 以下の整数である頂点ラベル付き完全グラフ全体の集合を  S _ K とする。  G\in S _ K について、辺の重みが  k より大きい辺のみを全て削除したグラフを  G _ k とおく。  G\in S _ K についてのクラスカル法を考えると、辺の重みが  k 以下である辺について処理を終えた時点のグラフの連結成分数は、  C(G _ k) と等しいことがわかる。  G最小全域木の辺の重みの総和は  \sum _ {k=1} ^ {K} k(C(G _ {k-1})-C(G _ k)) で表せることもわかる。  C(G _ 0)=N,\ C(G _ K)=1 に注意して総和を計算すると、  \sum _ {k=1} ^ {K} k(C(G _ {k-1})-C(G _ k))=N-K+\sum _ {i=1} ^ {K-1} C(G _ k) となる。 求めたいのはこの総和であるから、  (N-K)K ^ {N(N-1)/2}+\sum _ {i=1} ^ {K-1}\sum _ {G\in S _ K} C(G _ k) である。

 \sum _ {G\in S _ K} C(G _ k) を求めることを考える。 これは「全ての辺の重みが  1 以上  K 以下の整数である頂点ラベル付き  N 頂点完全グラフについての、辺の重みが  k より大きい辺のみを全て削除したグラフの連結成分数の総和」であるが、「頂点ラベル付き  N 頂点単純無向グラフについて、連結成分数と『隣接している異なる頂点の組には  1 以上  k 以下の整数、隣接していない異なる頂点の組には  k+1 以上  K 以下の整数を割り当てる場合の数』の積の総和」と言い換えられる。

ここで、頂点ラベル付き  n 頂点  m 辺単純無向グラフの個数を考え、  n について指数型、  m について通常型である母関数  f(X;Y) を考える。 頂点ラベル付き  n 頂点  m 辺単純無向グラフの個数は  \binom{n(n-1)/2}{m} であるから、  f(X;Y) は以下のように計算できる。
 \begin{align}
    f(X;Y)=\sum _ {n\geq 0,\ m\geq 0} \binom{n(n-1)/2}{m} \frac{X ^ n}{n!} Y ^ m=\sum _ {n\geq 0} (1+Y) ^ {n(n-1)/2} \frac{X ^ n}{n!}\end{align}
 n=0 のときの係数が  1 である点に注意が必要である。 また、頂点ラベル付き  n 頂点  m 辺単純連結無向グラフの個数を考え、  n について指数型、  m について通常型である母関数  g(X;Y) を考える。 すると、連結成分数が  c 個である頂点ラベル付き  n 頂点  m 辺単純無向グラフの個数の母関数は  g ^ c/c! となる。 なお、各連結成分について順列ではなく組み合わせを考えるため、  c! で割る必要がある。 少し難しい内容であるが、以下の記事が参考になる。 指数型母関数入門 – 37zigenのHP

したがって、  f=\sum _ {c\geq 0} g ^ c/c!=\exp g\ \therefore\ g=\log f がわかる。 さらに、連結成分数が  c 個である頂点ラベル付き  n 頂点  m 辺単純無向グラフについての連結成分数の総和の母関数は  c\times g ^ c/c!=g ^ c/(c-1)! となる。 したがって、頂点ラベル付き  n 頂点  m 辺単純無向グラフについての連結成分数の総和を考え、  n について指数型、  m について通常型である母関数  h(X;Y) を考えると、  h=\sum _ {c\geq 1} g ^ c/(c-1)!=g\sum _ {c\geq 0} g ^ c/c!=g\exp g=f\log f となる。

 a _ {n,m} h X ^ nY ^ m/n! の係数とする。  \sum _ {G\in S _ K} C(G _ k) は以下のように変形できる。 ただし、  [X ^ N] X ^ N/N! の係数でなく、  X ^ N の係数を表すことに注意。
 \begin{align}
    \sum _ {G\in S _ K} C(G _ k)&=\sum _ {m\geq 0} a _ {N,m}k ^ m(N-k) ^ {N(N-1)/2-m}\\
    &=(N-k) ^ {N(N-1)/2}N!\sum _ {m\geq 0} \frac{a _ {N,m}}{N!}\left(\frac{k}{N-k}\right) ^ m\\
    &=(N-k) ^ {N(N-1)/2}N![X ^ N]h\left(X;\frac{k}{N-k}\right)\end{align}

 h\left(X;k/(N-k)\right) O(N\log N) で求まるので、答えは全体で  O(KN\log N) で求まる。 実装する際には、  f(X;Y) ではなく直接  f\left(X;k/(N-k)\right) を計算してよい。

提出

Nyaan’s Libraryを使用した。 Submission #61222053 - AtCoder Beginner Contest 386 Nyaan’s Library | This documentation is automatically generated by online-judge-tools/verification-helper

ITF.PC 2024 M - Does My Favored Team Have a Chance? 完全解説

問題概要

勝ち、引き分け、負けのポイントがそれぞれ  2 1 0 のリーグ戦について、各チームの現在のポイントと残りの試合数が与えられるので、単独優勝できる場合および優勝できる場合があるかを判定する問題である。

解法

あるチームの優勝可能性の判定

チーム  i\ (1\leq i\leq N) が優勝する場合がある条件と、その高速な判定法を考える。

 T=\{1,2,\ldots,N\},\ T'=T\setminus\{i\} とする。 また、空でない  R\ (\subseteq T') について  P(R)=\sum _ {k\in R} P _ k,\ G(R)=\sum _ {\{j,k\}\subseteq R} G _ {j,k} と定義する。 そして、  a(R)=(P(R)+2G(R))/|R| と定義する。

空でないある  R\ (\subseteq T') が存在して  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j} が成立するならば、チーム  i が優勝する場合はないことが次のようにいえる。 チーム  j\ (\in T') とチーム  k\ (\in T') が今後対戦することにより、  G _ {j,k} G _ {k,j} 1 小さくなり、試合結果によって  P _ j または  P _ k にポイントが加わると考える。 すべての試合が終了した後の  P _ k,\ a(R) をそれぞれ  P' _ k,\ a'(R) とおくと、  a(R)\leq a'(R) がいえる。 また、  a'(R) P' _ k\ (k\in R) の平均であることもいえる。 平均の性質より、あるチーム  k\ (\in R) が存在して、  a(R)\leq a'(R)\leq P' _ k が成立する。  P' _ i\leq P _ i+2\sum _ {j\in T'} G _ {i,j} と仮定より、空でないある  R\ (\subseteq T') k\ (\in R) が存在して  P' _ i\lt P' _ k となるから、チーム  i はどの場合においても優勝できない。

次に、  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j} を満たす、空でない  R\ (\subseteq T') が存在するかどうかは次のように判定できる。  \{s\}\cup T'\cup\{t\} を頂点集合とし、  s から各  j\ (\in T') に容量  2\sum _ {j\lt k} G _ {j,k} の有向辺を、各  j\ (\in T') から  k\ (\in T',\ j\lt k) に容量  2G _ {j,k} の有向辺を、各  k\ (\in T') から  t に容量  P _ i+2\sum _ {j\in T'} G _ {i,j}-P _ k の有向辺を張る。 なお、ある  k\ (\in R) について  P _ i+2\sum _ {j\in T'} G _ {i,j}-P _ k が負になる場合は、明らかにチーム  i は優勝できない。 このネットワークの  s から  t への最小カットの  s 側の頂点集合を  S としたとき、ある  R\ (\subseteq T') が存在して  S=\{s\}\cup R となる。 そして、  s 側の頂点集合が  \{s\} となるようなカットの容量は  2\sum _ {\{j,k\}\subseteq T'} G _ {j,k} であるから、  R が空でないとき最小カットの容量は  2\sum _ {\{j,k\}\subseteq T'} G _ {j,k} 未満である。 このとき、  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j} を満たすことが次のようにいえる。
 \begin{gathered}
    2\sum _ {\{j,k\}\subseteq T'} G _ {j,k}\gt 2\sum _ {j\notin R}\sum _ {j\lt k} G _ {j,k}+2\sum _ {j\in R}\sum _ {k\notin R,\ j\lt k} G _ {j,k}+|R|\left(P _ i+2\sum _ {j\in T'} G _ {i,j}\right)-\sum _ {k\in R} P _ k\\
    \therefore\ 2G(R)=2\sum _ {\{j,k\}\subseteq R} G _ {j,k}\gt |R|\left(P _ i+2\sum _ {j\in T'} G _ {i,j}\right)-P(R)\\
    \therefore\ a(R)=\frac{P(R)+2G(R)}{|R|}\gt P _ i+2\sum _ {j\in T'} G _ {i,j}\end{gathered}
また、最小カットの  s 側の頂点集合が  \{s\} となる場合、最大フローに注目すると、すべてのチーム  k\ (\in T') について今後チーム  i 以外との対戦で得るポイントの合計が  P _ i+2\sum _ {j\in T'} G _ {i,j}-P _ k 以下となる場合が構築できている。 このような場合において、チーム  i は今後のすべての試合に勝てば優勝できる。

よって、チーム  i\ (1\leq i\leq N) の優勝可能性の判定は上のネットワークについて最大流問題を解くことで行える。 このネットワークは  \Theta(N) 頂点  \Theta(N ^ 2) 辺であるため、Push-Relabel Algorithmによる時間計算量は  O(N ^ 3) となる。 Dinic法でも、この問題に対しては十分高速である。

あるチームの単独優勝可能性の判定

チーム  i\ (1\leq i\leq N) が単独優勝する場合がある条件と、その高速な判定法を考える。

同様に、  T=\{1,2,\ldots,N\},\ T'=T\setminus\{i\} とする。 また、空でない  R\ (\subseteq T') について  P(R)=\sum _ {k\in R} P _ k,\ G(R)=\sum _ {\{j,k\}\subseteq R} G _ {j,k} と定義する。 そして、  a(R)=(P(R)+2G(R))/|R| と定義する。

空でないある  R\ (\subseteq T') が存在して  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j}-1 が成立するならば、チーム  i が優勝する場合はないことが次のようにいえる。 すべての試合が終了した後の  P _ k P' _ k とおくと、あるチーム  k\ (\in R) が存在して、  a(R)\leq P' _ k が成立するのであった。  P' _ i\leq P _ i+2\sum _ {j\in T'} G _ {i,j} と仮定より、空でないある  R\ (\subseteq T') k\ (\in R) が存在して  P' _ i-1\lt P' _ k となる。 ポイントの整数性より  P' _ i\leq P' _ k が成立するから、チーム  i はどの場合においても単独優勝できない。

 \{s\}\cup T'\cup\{t\} を頂点集合とし、  s から各  j\ (\in T') に容量  2\sum _ {j\lt k} G _ {j,k} の有向辺を、各  j\ (\in T') から  k\ (\in T',\ j\lt k) に容量  2G _ {j,k} の有向辺を、各  k\ (\in T') から  t に容量  P _ i+2\sum _ {j\in T'} G _ {i,j}-1-P _ k の有向辺を張る。 ある  k\ (\in R) について  P _ i+2\sum _ {j\in T'} G _ {i,j}-1-P _ k が負になる場合は、明らかにチーム  i は優勝できない。 また、優勝可能性と同様の不等式変形により、最小カットの  s 側の頂点集合が  \{s\} でない場合、  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j}-1 を満たす。 また、最小カットの  s 側の頂点集合が  \{s\} となる場合、最大フローに注目すると、すべてのチーム  k\ (\in T') について今後チーム  i 以外との対戦で得るポイントの合計が  P _ i+2\sum _ {j\in T'} G _ {i,j}-1-P _ k 以下となる場合が構築できている。 このような場合において、チーム  i は今後のすべての試合に勝てば単独優勝できる。

よって、チーム  i\ (1\leq i\leq N) の単独優勝可能性の判定は上のネットワークについて最大流問題を解くことで行える。 優勝可能性と同様に、Push-Relabel Algorithmによる時間計算量は  O(N ^ 3) となり、Dinic法でもこの問題に対しては十分高速である。

すべてのチームの優勝可能性の高速な判定

 T=\{1,2,\ldots,N\},\ T'=T\setminus\{i\},\ T''=T\setminus\{k\} とおく。 チーム  i\ (\in T) が優勝する場合はないとする。 このとき、チーム  k\ (\in T) について  P _ i+2\sum _ {j\in T'} G _ {i,j}\geq P _ k+2\sum _ {j\in T''} G _ {k,j} が成立するならばチーム  k が優勝する場合もないことが次のようにいえる。

まず、仮定より空でないある  R\ (\subseteq T') が存在して  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j}\geq P _ k+2\sum _ {j\in T''} G _ {k,j} が成立する。  R\subseteq T'' でもあるならば明らかにチーム  k が優勝する場合はない。  R\not\subseteq T'' であることは  k\in R であることと同値である。  a(\{k\})=P _ k\leq P _ k+2\sum _ {j\in T''} G _ {k,j} より  R\neq\{k\} であるから、そのような場合は  R\setminus\{k\} は空でない。  a(R\setminus\{k\}) を求める。
 \begin{align}
    a(R\setminus\{k\})&=\frac{P(R\setminus\{k\})+2G(R\setminus\{k\})}{|R|-1}\\
    &=\frac{P(R)-P _ k+2G(R)-2\sum _ {j\in R} G _ {k,j}}{|R|-1}\\
    &\geq\frac{P(R)-P _ k+2G(R)-2\sum _ {j\in T''} G _ {k,j}}{|R|-1}\\
    &\gt \frac{P(R)+2G(R)-a(R)}{|R|-1}\\
    &=\frac{|R|a(R)-a(R)}{|R|-1}\\
    &=a(R)\\
    &\gt P _ k+2\sum _ {j\in T''} G _ {k,j}\end{align}
となり、この場合もチーム  k が優勝する場合はないことが示された。

条件が等号付きであるため、  P _ i+2\sum _ {j\in T'} G _ {i,j} が等しければ優勝可能かどうかも等しい。 よって、各チームを  P _ i+2\sum _ {j\in T'} G _ {i,j} でソートして二分探索を行うことにより、全体としての時間計算量を  O(N ^ 3\log N) にすることができる。 また、他のチームと今後対戦しない仮のチームを追加して、そのチームが各チームの  P _ i+2\sum _ {j\in T'} G _ {i,j} と等しいポイントを持っているとして二分探索してもよい。

すべてのチームの単独優勝可能性の高速な判定

同様に  T=\{1,2,\ldots,N\},\ T'=T\setminus\{i\},\ T''=T\setminus\{k\} とおく。 チーム  i\ (\in T) が単独優勝する場合はないとし、チーム  k\ (\in T) について  P _ i+2\sum _ {j\in T'} G _ {i,j}\geq P _ k+2\sum _ {j\in T''} G _ {k,j} が成立する場合を考える。

まず、仮定より空でないある  R\ (\subseteq T') が存在して  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j}-1\geq P _ k+2\sum _ {j\in T''} G _ {k,j}-1 が成立する。  R\subseteq T'' でもあるならば明らかにチーム  k が単独優勝する場合はない。 一方、  k\in R である場合、  \sum _ {j\in T''} G _ {k,j}=0 ならば  a(\{k\})=P _ k\gt P _ k+2\sum _ {j\in T''} G _ {k,j}-1 より  R=\{k\} である可能性がある。 ただし、  \sum _ {j\in T''} G _ {k,j}=0 を満たすチーム  k が単独優勝するためには、任意のチーム  k'\ (\in T'') について  P _ k\gt P _ {k'} が成り立つ必要がある。 そのようなチームが存在すれば、そのチームについて判定を行えばよい。

 k\in R かつ  \sum _ {j\in T''} G _ {k,j}\gt 0 の場合は  R\setminus\{k\} は空でないとしてよい。 また、ポイントの整数性を考慮して  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j}-1\geq P _ k+2\sum _ {j\in T''} G _ {k,j}-1 a(R)-1/|R|\geq P _ i+2\sum _ {j\in T'} G _ {i,j}-1\geq P _ k+2\sum _ {j\in T''} G _ {k,j}-1 と言い換えておく。
 \begin{align}
    a(R\setminus\{k\})&=\frac{P(R\setminus\{k\})+2G(R\setminus\{k\})}{|R|-1}\\
    &=\frac{P(R)-P _ k+2G(R)-2\sum _ {j\in R} G _ {k,j}}{|R|-1}\\
    &\geq\frac{P(R)-P _ k+2G(R)-2\sum _ {j\in T''} G _ {k,j}}{|R|-1}\\
    &\geq\frac{P(R)+2G(R)-(a(R)+1-1/|R|)}{|R|-1}\\
    &=\frac{|R|a(R)-a(R)-(|R|-1)/|R|}{|R|-1}\\
    &=a(R)-\frac{1}{|R|}\\
    &\geq P _ k+2\sum _ {j\in T''} G _ {k,j}-1\end{align}
となり、  a(R)-1/|R|=P _ i+2\sum _ {j\in T'} G _ {i,j}-1=P _ k+2\sum _ {j\in T''} G _ {k,j}-1 かつ  \sum _ {j\in R} G _ {k,j}=\sum _ {j\in T''} G _ {k,j} が成立する場合のみ、  a(R\setminus\{k\})\gt P _ k+2\sum _ {j\in T''} G _ {k,j}-1 がいえない。

他のチームと今後対戦しない仮のチームを追加する二分探索を行いながら、上のコーナーに対処する。 まず、  p 点のチームを追加して優勝可能性の判定をすることは、  (p+1) 点のチームを追加してそのチームの単独優勝可能性の判定をすることと等価である。 優勝可能と判定された最小のポイントを  p _ m 点とおく。  P _ i+2\sum _ {j\in T'} G _ {i,j}\lt p _ m ならば明らかにチーム  i は優勝不可能である。  P _ i+2\sum _ {j\in T'} G _ {i,j}\gt p _ m ならばチーム  i は単独優勝も可能であることが次のようにいえる。  P _ i+2\sum _ {j\in T'} G _ {i,j}\gt p _ m かつチーム  i が単独優勝不可能であることを仮定する。 すると、ある  R\ (\subseteq T') が存在して  a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j}-1 が成立する。  T p _ m 点を持ち今後試合がないチーム  k を追加すると、明らかに  R\subseteq T a(R)\gt P _ i+2\sum _ {j\in T'} G _ {i,j}-1\geq p _ m が成立し、チーム  k が優勝可能と判定されたことと矛盾する。

 P _ i+2\sum _ {j\in T'} G _ {i,j}=p _ m の場合、  (p+1) 点のチームを追加してそのチームの単独優勝可能性の判定を行う。 明らかに単独優勝は不可能と判定され、最小カットの容量が  2\sum _ {\{j,k\}\subseteq T'} G _ {j,k}-1 でない場合はコーナーでないことが次のようにいえる。
 \begin{gathered}
    2\sum _ {\{j,k\}\subseteq T'} G _ {j,k}-1\gtreqless 2\sum _ {j\notin R}\sum _ {j\lt k} G _ {j,k}+2\sum _ {j\in R}\sum _ {k\notin R,\ j\lt k} G _ {j,k}+|R|\left(P _ i+2\sum _ {j\in T'} G _ {i,j}-1\right)-\sum _ {k\in R} P _ k\\
    \therefore\ 2G(R)-1=2\sum _ {\{j,k\}\subseteq R} G _ {j,k}-1\gtreqless |R|\left(P _ i+2\sum _ {j\in T'} G _ {i,j}-1\right)-P(R)\\
    \therefore\ a(R)-\frac{1}{|R|}=\frac{P(R)+2G(R)-1}{|R|}\gtreqless P _ i+2\sum _ {j\in T'} G _ {i,j}-1\end{gathered}
ただし、不等号は複号同順。 最小カットの容量が  2\sum _ {\{j,k\}\subseteq T'} G _ {j,k}-1 である場合について考える。  R に属し  p _ m=P _ i+2\sum _ {j\in T'} G _ {i,j} を満たす各頂点  i について、フローを押し戻して  s から  i へのパスにフローを  1 流せるようにできる。  i から  t へフローを流せるならば得られたのが最大フローではないということになってしまうから、  i から  t へ張った辺にはフローを流しきっている。  i を経由する  s から  t へのパスに無理矢理フローを  1 流すことを考えると、すべてのチーム  k\ (\in T') について今後得るポイントの合計が  p _ m-1-P _ k 以下であり、かつチーム  i の今後得るポイントの合計が  p _ m-P _ i である場合が構築できている。 これは、チーム  i が単独優勝できている場合である。

よって、単独優勝可能性も判定する場合、二分探索をした後、境界について最小カットを求めることが必要となる。 最大流問題を解く回数を  \Theta(\log N) にする場合、優勝可能となる最小の  P _ i+2\sum _ {j\in T'} G _ {i,j} が優勝可能となる最小のポイントではない可能性に注意が必要である。 また、Push-Relabel Algorithmの場合、実装によっては残余ネットワークだけでなく残存量を考慮して最小カットを求める必要があることに注意が必要である。

基になった論文

優勝判定については、KD Wayne, "A New Property and Faster Algorithm for Baseball Elimination", 1999に記載がある。

おわりに

作問の際、計算量解析および単独優勝判定のコーナーの対処をtatyamさんに教えていただきました。 また、問題原案を出した際にYu_さんにも助言をいただきました。 ここに感謝の意を表します。

AtCoder Regular Contest 185 (ARC185) E - Adjacent GCDの勉強

問題

E - Adjacent GCDatcoder.jp

素朴なDP

単純な考察により、答え R _ 1,R _ 2,\ldots,R _ Nは、 R _ 0=0として R _ i=2R _ {i-1}+\sum _ {j=1} ^ {i-1} 2 ^ {j-1} \gcd{(A _ j, A _ i)}で表されることがわかる。

この問題では、このDPを高速化することが求められている。

高速化1

解法

Nyaanさん、maspyさんの公式解説による方法である。

 \max A _ i=Mとおく。  \delta _ {n,g}\ (1\leq n,g\leq M)クロネッカーのデルタとする。
 \begin{align}
    \gcd{(A _ j, A _ i)}=\sum _ {n=1} ^ M n\delta _ {n,\gcd{(A _ j, A _ i)}}\end{align}
が成立する。 また、数列 a _ {n,g}
 \begin{align}
    a _ {n,g}=\sum _ {n|m,\ m\leq M} \delta _ {m,g}\end{align}
で定義する。  \gcd{(A _ j, A _ i)} a _ {n,\gcd{(A _ j, A _ i)}}で表したいと考える。
 \begin{align}
    \gcd{(A _ j, A _ i)}=\sum _ {n=1} ^ M x _ {n,\gcd{(A _ j, A _ i)}}a _ {n,\gcd{(A _ j, A _ i)}}\end{align}
が成り立つと仮定すると
 \begin{align}
    \gcd{(A _ j, A _ i)}&=\sum _ {n=1} ^ M n\delta _ {n,\gcd{(A _ j, A _ i)}}\\
    &=\sum _ {n=1} ^ M x _ {n,\gcd{(A _ j, A _ i)}}a _ {n,\gcd{(A _ j, A _ i)}}\\
    &=\sum _ {n=1} ^ M x _ {n,\gcd{(A _ j, A _ i)}}\sum _ {n|m,\ m\leq M} \delta _ {m,\gcd{(A _ j, A _ i)}}\\
    &=\sum _ {m=1} ^ M\left(\sum _ {n|m,\ n\leq M} x _ {n,\gcd{(A _ j, A _ i)}}\right)\delta _ {m,\gcd{(A _ j, A _ i)}}\end{align}
となり、 x _ {n,\gcd{(A _ j, A _ i)}} \gcd{(A _ j, A _ i)}に依存しないことがわかる。 そして、
 \begin{align}
    n=\sum _ {m|n,\ m\leq M} x _ m\end{align}
となる (x _ n)は存在する。  x _ n=\varphi(n)であるが、それがわからなくても約数メビウス変換で十分高速に事前計算できる。

ここで、最大公約数の性質より、アイバーソン括弧を用いると a _ {n,\gcd{(A _ j, A _ i)}}=[n|\gcd{(A _ j, A _ i)}]=[n|A _ j][n|A _ i]と表せる。 したがって、
 \begin{align}
    \sum _ {j=1} ^ {i-1} 2 ^ {j-1} \gcd{(A _ j, A _ i)}
    &=\sum _ {j=1} ^ {i-1} 2 ^ {j-1} \sum _ {n=1} ^ M x _ n[n|A _ j][n|A _ i]\\
    &=\sum _ {j=1} ^ {i-1} 2 ^ {j-1} \sum _ {n|A _ i,\ n\leq M}x _ n[n|A _ j]\\
    &=\sum _ {n|A _ i,\ n\leq M}x _ n\sum _ {j=1} ^ {i-1} 2 ^ {j-1} [n|A _ j]\end{align}
と変形できる。 DPとともに、 A _ iの約数の列挙と各 nについての \sum _ {j=1} ^ {i-1} 2 ^ {j-1} [n|A _ j]の更新を行うことで、十分高速に答えが求まる。

提出

Submission #58928300 - AtCoder Regular Contest 185atcoder.jp

Nyaan’s Libraryを使用

倍数変換・約数変換 | Nyaan’s Librarynyaannyaan.github.io

高速化2

解法

noshi91さんのユーザ解説による方法である。

数列 (a _ i),(b _ i)について、 c _ k=\sum _ {\gcd(i,j)=k} a _ i b _ jで定まる数列 (c _ i) (a _ i),(b _ i)のGCD畳み込みという。 また、 f(x) _ k=\sum _ {k|i} x _ i (x _ i)は有限列とする)で定まる数列 (f(x) _ i)について、 (c _ i) (a _ i),(b _ i)のGCD畳み込みであれば、 f(c) _ i=f(a) _ i f(b) _ iが成立する。 以下の記事が詳しい。 添え字 gcd での畳み込みで AGC038-C を解く - noshi91のメモnoshi91.hatenablog.com

 \max A _ i=Mとおく。単位行列の第 j列をとった列ベクトルを \boldsymbol{\delta} _ jと表すことにする。 それぞれの \boldsymbol{\delta} _ jを数列と見ると、 \boldsymbol{\delta} _ {\gcd(A _ j,A _ i)} \boldsymbol{\delta} _ {A _ j},\boldsymbol{\delta} _ {A _ i}のGCD畳み込みになっている。 線形変換 Zを、 Z(\boldsymbol{\delta} _ j) _ k=\sum _ {k|i,\ i\leq M} (\boldsymbol{\delta} _ j) _ iで定めると、逆変換 Z ^ {-1}が存在する。 また、列ベクトル \boldsymbol{n} \boldsymbol{n} _ i=iで定め、演算 \circアダマール積とする。
 \begin{align}
    \gcd{(A _ j, A _ i)}&=\sum _ {n=1} ^ M n(\boldsymbol{\delta} _ {\gcd(A _ j,A _ i)}) _ n\\
    &=\langle\boldsymbol{n},\boldsymbol{\delta} _ {\gcd(A _ j,A _ i)}\rangle\end{align}
が成立する。 したがって、
 \begin{align}
    \sum _ {j=1} ^ {i-1} 2 ^ {j-1} \gcd{(A _ j, A _ i)}
    &=\sum _ {j=1} ^ {i-1} \langle\boldsymbol{n},2 ^ {j-1} \boldsymbol{\delta} _ {\gcd(A _ j,A _ i)}\rangle\\
    &=\sum _ {j=1} ^ {i-1} \langle\boldsymbol{n},2 ^ {j-1} \boldsymbol{\delta} _ {\gcd(A _ j,A _ i)}\rangle\\
    &=\sum _ {j=1} ^ {i-1} \langle\boldsymbol{n},Z ^ {-1}(Z(2 ^ {j-1}\boldsymbol{\delta} _ {A _ j})\circ Z\boldsymbol{\delta} _ {A _ i})\rangle\\
    &=\sum _ {j=1} ^ {i-1} \langle (Z ^ {-1}) ^ \top\boldsymbol{n},Z(2 ^ {j-1}\boldsymbol{\delta} _ {A _ j})\circ Z\boldsymbol{\delta} _ {A _ i}\rangle\\
    &=\left\langle (Z ^ {-1}) ^ \top\boldsymbol{n},\left(\sum _ {j=1} ^ {i-1} Z(2 ^ {j-1}\boldsymbol{\delta} _ {A _ j})\right)\circ Z\boldsymbol{\delta} _ {A _ i}\right\rangle\\
    &=\sum _ {n=1} ^ M \left((Z ^ {-1}) ^ \top\boldsymbol{n}\right) _ n\left(\sum _ {j=1} ^ {i-1} Z(2 ^ {j-1}\boldsymbol{\delta} _ {A _ j})\right) _ n \left(Z\boldsymbol{\delta} _ {A _ i}\right) _ n\end{align}
と変形できる。  \left(Z\boldsymbol{\delta} _ {A _ i}\right) _ nが掛かるから、 n A _ iの約数である場合のみ計算すればよい。 また、 \sum _ {j=1} ^ {i-1} Z(2 ^ {j-1}\boldsymbol{\delta} _ {A _ j})も十分高速に更新できる。 そして、 Zが倍数ゼータ変換にあたることを考えると、 (Z ^ {-1})が倍数メビウス変換で、 (Z ^ {-1}) ^ \topが約数メビウス変換にあたることがわかる。 結局、高速化1の方法と同じ実装を行うことになり、さらに、結局maspyさんのユーザ解説で軽く触れられている転置原理からの導出を行っていたことがわかる。 提出は省略する。

【自分のための備忘録】競プロで精進した問題一覧【2024/9~】

※ネタバレ注意



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
メモ:一見\binom{100}{5}\lt 10^8なのかよといらつく問題だが、より難しい問題で合同算術の積の衝突可能性について慣れていると、そういう意図であろうという見通しが立ちやすい



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:進数変換
メモ:2,8,10,16進数以外を標準ライブラリで扱える言語は意外と少ない。今回はHaskellを使用したが、普通にC++Pythonで直書きしたほうが早く書き終わる気がする。
qiita.com
scrapbox.io
https://www.tumblr.com/sarabandejp/101471543023
blog.sarabande.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
メモ:do-while文のcontinueのためにgotoを使う場合、ラベルの位置をdoの前に置くと無限ループになる危険性がある



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:多倍長整数
メモ:C++の符号付き128bit整数型は__int128_t



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
https://atcoder.jp/contests/typical90/submissions/57678345atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
https://atcoder.jp/contests/typical90/submissions/57683673atcoder.jp



atcoder.jp
atcoder.jp
メモ:符号付き64bit整数型で10^18を10倍しないように気をつける、符号なし64bit整数型を使うのも手



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



https://atcoder.jp/contests/typical90/tasks/typical90_catcoder.jp
atcoder.jp
キーワード:木の直径
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
https://atcoder.jp/contests/typical90/submissions/58296187atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:尺取り法



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:01 BFS
メモ:01 BFSの説明
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:ダブリング
メモ:繰り返し二乗法と根本は同じ



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
メモ:hwのループなどの場合、continueは普通にcontinueで内側のループのみ抜ければよい



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



https://atcoder.jp/contests/abc373/tasks/abc373_fatcoder.jp
atcoder.jp
キーワード:Convex Hull Trick (Li Chao Tree) + DP
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
キーワード:ワーシャル・フロイド法 更新クエリ
メモ:コンテスト中のデバッグは、アルゴリズムが間違っている可能性の検証とアルゴリズムを正しく実装できていない可能性の検証を切り分けて行ったほうがよいかもしれない



atcoder.jp
atcoder.jp
メモ:操作に関する問題を具体例で検証する場合は、具体例の性質をなるべく気にせずに操作の性質に注目したほうがよいことが多い
また、今回は有限回で行えなくなる不可逆な操作で、かつ目的の状態からも行える可能性がある操作である
このような場合、操作が行えなくなる状態について考察するのも手



atcoder.jp
atcoder.jp
キーワード:倍数変換・約数変換(ゼータ変換・メビウス変換)、GCD畳み込み
メモ:まとめた
not-leonian.hatenablog.com
ライブラリ:
nyaannyaan.github.io



atcoder.jp
atcoder.jp
メモ:



atcoder.jp
atcoder.jp
キーワード:01 on Tree



atcoder.jp
atcoder.jp
キーワード:01 on Tree
メモ:「数列の、位置の重み付きの総和」は「01の数列の列の転倒数」に言い換えられる



atcoder.jp
https://atcoder.jp/contests/abc380/submissions/60013248atcoder.jp
メモ:ゲームの勝敗をメモ化再帰で判定する場合、先手の場合は初期値をfalseにして次の後手の勝敗との論理和をとり、後手の場合は初期値をtrueにして次の先手の勝敗との論理積をとるのが定石



atcoder.jp
atcoder.jp
キーワード:2-SAT



https://atcoder.jp/contests/arc188/tasks/arc188_aatcoder.jp
atcoder.jp
キーワード:Zero-Sum Ranges



atcoder.jp
atcoder.jp
キーワード:フィボナッチ数列多項式の多点評価(Multipoint Evaluation)
ライブラリ:
nyaannyaan.github.io
メモ:Nyaan's Libraryの多点評価は2点以上でないと実行時エラーが発生することに注意、Nyaan's Libraryの998244353を法とするmodintはLazyMontgomeryModInt<998244353>



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:ダイクストラ
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
キーワード:強連結成分分解



atcoder.jp
atcoder.jp
キーワード:最大値取得・アフィン変換区間更新クエリの遅延セグ木



atcoder.jp
atcoder.jp
キーワード:乗法的関数
ライブラリ:
nyaannyaan.github.io
メモ:和を積として解釈 上のライブラリはbundleするとtemplateが衝突したり、enumurateを誤字っていたりとそのままでは使いにくい

atcoder.jp
キーワード:エラトステネスの篩
メモ:2以上$N$以下の整数について各素因数ごとに処理を行うときの計算量は、エラトステネスの篩で考えられる
algo-method.com



atcoder.jp
atcoder.jp
キーワード:マンハッタン距離、45度回転



atcoder.jp
atcoder.jp
キーワード:区間chminクエリのSegment Tree Beats!
ライブラリ:
nyaannyaan.github.io
メモ:Segment Tree Beats!は時間/空間ともに定数倍に注意

atcoder.jp
キーワード:最大値取得のセグ木
メモ:もらうDPか送るDPかで必要なデータ構造が変わってしまう例 ただセグ木が得意なのは1点更新であることを考えると自然と思いつくはず 最大値または-1を出力する問題でセグ木を使う場合、e()の返す値に注意



atcoder.jp
atcoder.jp
キーワード:木DP、LCAによる木上の2頂点の距離の公式
メモ:
ダブリングによる木の最近共通祖先(LCA:Lowest Common Ancestor)を求めるアルゴリズム | アルゴリズムロジック

atcoder.jp
メモ:わざわざLCAによる公式を考えなくても、似たような主客転倒により各辺の寄与はその辺を切って得られる森の各連結成分の頂点数の積であることがわかる



atcoder.jp
atcoder.jp
キーワード:半分全列挙



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:最大値取得のセグ木によるLIS



https://atcoder.jp/contests/typical90/tasks/typical90_bnatcoder.jp
atcoder.jp
キーワード:区間和取得、アフィン変換区間更新クエリの遅延セグ木
メモ:制約に途中で気付いたが、そのまま時間\Theta(N\log\operatorname{max}A)/空間\Theta(\operatorname{max}A)で解ききってしまった



https://atcoder.jp/contests/typical90/tasks/typical90_bpatcoder.jp
atcoder.jp
キーワード:BIT
メモ:ac-libraryのfenwick_treeは1点更新ではなく1点加算であることに注意



atcoder.jp
atcoder.jp
キーワード:木DP



atcoder.jp
atcoder.jp
キーワード:平面走査



atcoder.jp
https://atcoder.jp/contests/typical90/submissions/60294254atcoder.jp
メモ:ビットごとの論理積論理和排他的論理和に関する問題はビットごとに分解して考えがち



atcoder.jp
atcoder.jp
キーワード:ワーシャル・フロイド法



atcoder.jp
atcoder.jp
キーワード:期待値DP
メモ:FPSで解いたが、結局出てきたのは公式解説と同じ式
すごろくDPは有理数modだとゼロ除算が発生するという理由で出力形式が小数である場合が多い



atcoder.jp
atcoder.jp
キーワード:最大値取得・アフィン変換区間更新クエリの遅延セグ木



https://atcoder.jp/contests/dp/tasks/dp_aatcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
メモ:DPの手法の1つに、「答えを添字にして、答えを達成する状態のうち最も良いものを求める」というものがある



atcoder.jp
atcoder.jp



atcoder.jp
https://atcoder.jp/contests/dp/submissions/60387189atcoder.jp
キーワード:DAG上のDP



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



https://atcoder.jp/contests/dp/tasks/dp_jatcoder.jp
atcoder.jp
キーワード:期待値DP、Sushi
メモ:DPの計算の順番に注意



atcoder.jp
https://atcoder.jp/contests/dp/submissions/60414911atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
メモ:落ち着いて式変形をする



atcoder.jp
atcoder.jp
キーワード:Union-Find
メモ:既に連結である場合に注意



https://atcoder.jp/contests/abc383/tasks/abc383_fatcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:最大値取得・アフィン変換区間更新クエリの遅延セグ木、セグ木上の二分探索
メモ:倍々になるので計算量がlogで抑えられるテク
ac-libraryのセグ木上の二分探索は追加の引数を取れないため、セグ木ではなく遅延セグ木にしないといけない場合がある



atcoder.jp
atcoder.jp
キーワード:最長共通部分列(LCS)、最長増加部分列(LIS)
メモ:distinctな2つの列のLCSは、片方の列の値を「もう片方の列でのその値の添字」に置き換えることでLISに落とし込まれる



atcoder.jp
atcoder.jp
キーワード:Grundy
メモ:コンテスト中、時間が少なかったとしてもGrundy数の問題は落ち着いて法則を確認しよう



atcoder.jp
atcoder.jp
キーワード:区間DP



https://atcoder.jp/contests/abc287/tasks/abc287_eatcoder.jp
atcoder.jp
メモ:長さの総和がSであるN個の文字列の配列のソートはO(S\log N)で行える



atcoder.jp
atcoder.jp
キーワード:区間ではない2次元クエリに対するMo's Algorithm、座標圧縮、BIT
メモ:Luzhiled's LibraryのMo's Algorithmの構造体は区間ではない(l>rになりうる)クエリに対応していないので注意
座標圧縮では、関係がある複数の配列を独立に座標圧縮しないように注意
oj-bundleを利用して展開する場合は、展開後のソースコードコンパイルできるかを確認すべき
ライブラリ:
nyaannyaan.github.io



atcoder.jp
atcoder.jp
メモ:安易にDPテーブルをコピーしない



atcoder.jp
atcoder.jp
キーワード:多倍長整数 中国剰余定理
メモ:Nyaan's Libraryの多倍長整数用のGarnerのアルゴリズムは、\{m\}が対ごとに素ではない場合に対応していないことに注意
使用するライブラリの候補の3個目くらいで通った
ライブラリ:
nyaannyaan.github.io
nyaannyaan.github.io
qiita.com



atcoder.jp
atcoder.jp
メモ:f_1(f_2(x))f_2(f_1(x))の大小はxによらず決まるので、使用するf_iはともかく作用させる順番は決まるのがポイント



atcoder.jp
atcoder.jp
メモ:薬を使った後のエネルギーが一定であることと、一度使ったら消えることをうまく考察に落としこめるかがポイント



atcoder.jp
atcoder.jp
メモ:自分は「シミュレーションの高速化が求められる問題で言い換えを考えてしまう」負けパターンがある



atcoder.jp
atcoder.jp
キーワード:浮動小数点数
メモ:解説の浮動小数点数の誤差に関する記述は有益



atcoder.jp
atcoder.jp
キーワード:挿入DP、FFT マージテク
メモ:FPSの除算やexp, logなどを計算しない場合はFPSのライブラリを持ち出さずにvectorで管理したほうが楽かもしれない



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
メモ:ちゃんとアルゴリズムが見えれば計算量も見える問題



atcoder.jp
atcoder.jp
キーワード:イベントソート



atcoder.jp
atcoder.jp
キーワード:食塩水(加比の最大化)、DAG上のDP



atcoder.jp
atcoder.jp
キーワード:離散対数問題(Baby Step Giant Step)
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
メモ:落ち着いて状態の同一視をしっかりとやると、場合分けがそこまで苦ではなくなる



atcoder.jp
atcoder.jp
キーワード:区間和取得または最大値取得および最小値取得、アフィン変換区間更新クエリの遅延セグ木、セグ木上の二分探索
メモ:コンテスト中の、添字ぶん座標を引いておくという発想は合っていた
遅延セグ木を3本持つ脳筋実装でシミュレーション



https://atcoder.jp/contests/arc134/tasks/arc134_batcoder.jp
atcoder.jp
キーワード:最小値取得のセグ木



atcoder.jp
atcoder.jp
キーワード:メモ化再帰



atcoder.jp
atcoder.jp
キーワード:multiset
メモ:実は自明な上界が達成可能であるが、考察で気付く前に貪欲法を書いてしまうのも手



atcoder.jp
atcoder.jp
メモ:C言語における%gは、C++においてはそのままcoutすればよい



https://atcoder.jp/contests/arc147/tasks/arc147_batcoder.jp
atcoder.jp
キーワード:バブルソート



atcoder.jp
atcoder.jp
キーワード:DFS
メモ:状態を同一視してメモ化をする必要があるという誤謬をしてしまった 状態数が十分抑えられるので、mapによるメモ化はむしろめちゃくちゃ遅くなってしまう



atcoder.jp
atcoder.jp



https://atcoder.jp/contests/arc159/tasks/arc159_batcoder.jp
https://atcoder.jp/contests/arc159/submissions/61105725atcoder.jp
キーワード:ユークリッドの互除法



https://atcoder.jp/contests/abc351/tasks/abc351_datcoder.jp
https://atcoder.jp/contests/abc351/submissions/61112079atcoder.jp
キーワード:DFS



atcoder.jp
atcoder.jp
キーワード:BFS



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:N個からK個選ぶ組み合わせの列挙
メモ:計算量解析が有益な内容



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:Make N(コストC_iW_i足せる操作が2種類程度与えられたときに、Nを作るコストの総和の最小値を求めるテク)、解法平方分割



atcoder.jp
atcoder.jp
キーワード:指数型母関数
メモ:まとめた
not-leonian.hatenablog.com
ライブラリ:
nyaannyaan.github.io
nyaannyaan.github.io



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
メモ:X\lt YX\leq Yだと思ってしまう誤読に注意
並び替えた列をオイラーツアーだと思うところが妙手



https://atcoder.jp/contests/abc298/tasks/abc298_hatcoder.jp
atcoder.jp
キーワード:LCA
メモ:std::functionは死ぬほど遅い 自分はやはりグローバル変数を宗教的に避けるタイプなので、構造体にすべき
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
キーワード:Union-Find、マージテク



atcoder.jp
atcoder.jp
メモ:最大値と最小値の差が不変量なのを活かして、最大値の最小化を最小値の最小値に落とし込む、最大値を軸とした操作を用いると何回でも同じ操作ができるようになる、など天才的な考察を何個もしなければならない



atcoder.jp
atcoder.jp
メモ:捻りなしに決め打ち二分探索の場合もある



atcoder.jp
atcoder.jp
メモ:STの部分列かどうかを判定するのは貪欲に時間\Theta(|S|+|T|)でできる



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



https://atcoder.jp/contests/arc141/tasks/arc141_batcoder.jp
atcoder.jp
メモ:ギャグ



atcoder.jp
atcoder.jp
メモ:約数の個数が入る時間計算量であり、想定解の時間計算量より悪い
atcoder.jp
メモ:これならN\leq 2\times 10^5のような制約でも解ける



atcoder.jp
atcoder.jp



https://atcoder.jp/contests/arc156/tasks/arc156_batcoder.jp
atcoder.jp
キーワード:重複組み合わせ



atcoder.jp
https://atcoder.jp/contests/arc152/submissions/61317726atcoder.jp



atcoder.jp
atcoder.jp
キーワード:重複組み合わせ、写像12相



https://atcoder.jp/contests/arc176/tasks/arc176_aatcoder.jp
atcoder.jp
メモ:ヒントのようにM=1の場合と1つ目の条件がない場合を考えると思いつける



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:木DP
メモ:同じものを含む順列で初めて登場する位置が単調増加である場合の数(厳密には「K種類のボールが合わせてN個あり、そのうち種類iのボールがa_i個あるときの並び替えであって、種類iのボールが初めて登場する位置をl_i番目とおいたとき、l_iiについて単調増加となる場合の数」)は\begin{align*}\frac{N!}{a_1!a_2!\ldots a_K!}\prod_{i=1}^K \frac{a_i}{N-\sum_{j=1}^{i-1} a_j}\end{align*}



atcoder.jp
atcoder.jp



https://atcoder.jp/contests/abc387/tasks/abc387_eatcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:DAG上のDP



atcoder.jp
https://atcoder.jp/contests/agc024/submissions/61423005atcoder.jp
メモ:ギャグ



atcoder.jp
atcoder.jp
キーワード:指数型母関数、連結成分を橋で繋いで連結にする場合の数
メモ:maspyさんの解説が有益。F内の{}/n!は成分ごと1点以外のラベルを剥がして成分のラベルにするイメージ、F^Mに掛かる{}/M!はそれぞれの成分のラベルを剥がすイメージ、答えに掛かるN!はラベルを各頂点につけるイメージ
ライブラリ:
nyaannyaan.github.io
nyaannyaan.github.io
nyaannyaan.github.io



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:ダイクストラ
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
メモ:C++のstructはコンストラクタを書かないとメンバ変数のコンストラクタは呼ばれない



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:期待値DP



https://atcoder.jp/contests/abc352/tasks/abc352_eatcoder.jp
atcoder.jp
キーワード:クラスカル



atcoder.jp
atcoder.jp
キーワード:ダブリング



atcoder.jp
atcoder.jp
キーワード:ダイクストラ法、超頂点
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:木DP、LCA
メモ:結局再帰DFSの参照は引数に渡して書くのが一番マシ
ある頂点とのLCAとその子とのLCAを比較したとき、異なるのは子の部分木内の頂点のみ
最小値として考えられる最大値に注意



https://atcoder.jp/contests/abc356/tasks/abc356_eatcoder.jp
atcoder.jp
キーワード:解法平方分割
atcoder.jp
キーワード:調和級数
メモ:この形なら調和級数に気付けるべき



atcoder.jp
atcoder.jp
メモ:DPの寄与に共通点がある場合にテーブルの添字を変更して計算量を削減するテク



atcoder.jp
https://atcoder.jp/contests/abc378/submissions/61457315atcoder.jp
キーワード:BIT
メモ:余りをとった累積和を考えることでむしろ単純になる



atcoder.jp
atcoder.jp
キーワード:多倍長整数、QCFium法
メモ:今回はboostの多倍長整数とQCFium法でギリ通ったが、N\leq 2\times 10^5\Theta(N)桁の多倍長整数を使用するのはやめたほうがよい



atcoder.jp
https://atcoder.jp/contests/dp/submissions/61469705atcoder.jp
キーワード:bit DP
メモ:枝刈りまたは行うDPの変更で計算量が落ちる



https://atcoder.jp/contests/dp/tasks/dp_patcoder.jp
atcoder.jp
キーワード:木DP



atcoder.jp
atcoder.jp
キーワード:最大値取得のセグ木



https://atcoder.jp/contests/dp/tasks/dp_ratcoder.jp
atcoder.jp
キーワード:ダブリング、行列累乗
メモ:ダブリングとDPの融合だと思って解いたが、確かに行列累乗として見ることができる



atcoder.jp
atcoder.jp
キーワード:XOR畳み込み、高速アダマール変換
メモ:kyopro_friendsさんのユーザ解説が簡潔だがわかりやすい
高速アダマール変換と逆変換のパートも重要だが、\sum_d [x^{dM}] ((1+x)^i (1-x)^{N-i})[x^0] ((1+x)^i (1-x)^{N-i}) \!\!\!\!\mod (1-x^M)と言い換え、x^M\equiv 1と解釈して(1+x)^i \!\!\!\!\mod (1-x^M)(1-x)^i \!\!\!\!\mod (1-x^M)をそれぞれ計算し、時間\Theta(NM)で全列挙するパートも重要
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
キーワード:区間和取得のセグ木
メモ:区間和取得のセグ木はBITでよいが、ac-libraryを使用することを考えると一点取得がしたい場合、セグ木を使いたくなる場合がある



atcoder.jp
atcoder.jp
キーワード:約数メビウス変換
メモ:問題の性質上、Nyaan's Libraryの約数メビウス変換を用いるとNの約数でない値に対してはめちゃくちゃな値が入ることに注意
ライブラリ:
ei1333.github.io
nyaannyaan.github.io



atcoder.jp
atcoder.jp
キーワード:桁DP



atcoder.jp
atcoder.jp
キーワード:挿入DP



atcoder.jp
atcoder.jp
キーワード:\Theta(3^N)で部分集合を列挙するbit DP



atcoder.jp
atcoder.jp
キーワード:全方位木DP
メモ:1回目のDFSで親を2次元配列の0番目に持っていくとちょっとわかりやすい
このようなDPテーブルでは、左の部分木と右の部分木のマージのときに掛ける前に値を足してしまわないこと



atcoder.jp
atcoder.jp
キーワード:重軽再帰DP、HL分解
メモ:ライブラリをoj-bundleで展開した結果、呼び出されていない関数が解決できずコンパイルエラーになるソースコードが生成されることがある



atcoder.jp
atcoder.jp
キーワード:最大重みマッチング、最小費用流、最小費用循環流、最大値取得および最小値とそのindexの取得、アフィン変換区間更新クエリの遅延セグ木、セグ木上の二分探索、partially retroactive priority queue、クエリ先読み
メモ:maspyさんのユーザ解説が詳しい



atcoder.jp
atcoder.jp
メモ:不等号を逆にしていないか、気をつけて確認する



atcoder.jp
atcoder.jp
メモ:二分探索が間に合いそうなら、二分探索より高速な解法がありそうでも二分探索をさっさとすべき



atcoder.jp
atcoder.jp
キーワード:最大値取得のセグ木
メモ:二分探索を考える利点として、そのまま上位互換の問題にも応用しやすいというのがある



https://atcoder.jp/contests/arc190/tasks/arc190_aatcoder.jp
atcoder.jp
メモ:ちゃんと実験して、自明な場合を除外していくと上界が見える



atcoder.jp
atcoder.jp
キーワード:最大値取得・アフィン変換区間更新クエリの遅延セグ木
メモ:区間に対してコストが決まっているDPでは、その区間の影響が確定する右端で考慮するとよいことがある



atcoder.jp
atcoder.jp
メモ:良い順序をとると貪欲ができる場合でパラメータが複数ある場合、複数のパラメータの関数によって順序が決まる場合もある



atcoder.jp
https://atcoder.jp/contests/dp/submissions/61748601atcoder.jp



atcoder.jp
https://atcoder.jp/contests/abc389/submissions/61888180atcoder.jp
メモ:いつも通りP_i,\ 3P_i,\ 5P_i,\ \ldotsに言い換えたとき、p円未満のものを全て買っているならば必ずp円のものも買えるのが本質
二分探索の境界で追加で買うべきものは(p+1)円のもののみなのも本質(ちょうど(p+1)円のものが残っているならばそちらを買って損しないし、残っていないときに(p+2)円以上のものが買えるならば(p+1)円もOKと判定されていたはず)
boost/multiprecision/cpp_int.hppは最初のほうでincludeすれば、inマクロ、outマクロ、allマクロと競合しない

atcoder.jp
メモ:テンプレート引数を弄ればchecked_int64_tを用意することもできる



atcoder.jp
atcoder.jp
メモ:答えとして求める必要がある値の個数が少なく、かつその真値の桁数が抑えられる場合、任意modでも真値を埋め込める場合がある

atcoder.jp
メモ:DPテーブルの次元が大きい場合、arrayにするのも手
arrayのゼロ埋めは「foo{};」でできる
配列の次元が大きいと、ローカルでコンパイルが終わらない可能性があることに注意



atcoder.jp
atcoder.jp
キーワード:Convex Hull Trick (Li Chao Tree) + DP
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
キーワード:区間和取得および最大値取得のセグ木、セグ木上の二分探索
メモ:答えに関して制約がある問題では、その制約を入力に関する制約に言い換えられないか考える方法がある



atcoder.jp
atcoder.jp
キーワード:有限体上の連立方程式
メモ:左ビットシフトの型に注意
ライブラリ:
nyaannyaan.github.io



atcoder.jp
atcoder.jp
キーワード:主客転倒
メモ:番兵を入れると一気に楽になる



atcoder.jp
atcoder.jp
ライブラリ:
nyaannyaan.github.io
nyaannyaan.github.io
nyaannyaan.github.io



atcoder.jp
atcoder.jp



atcoder.jp
atcoder.jp
キーワード:MEX、調和級数
メモ:誤読してしまったとき、本来の問題には適用できない考察をそのまま使わないように気をつける



atcoder.jp
atcoder.jp
キーワード:累積和による多重集合のZobrist Hash、擬似乱数



atcoder.jp
atcoder.jp



atcoder.jp
https://atcoder.jp/contests/abc391/submissions/62370683atcoder.jp
キーワード:最長共通部分列(LCS)、bit DP
メモ:階差が0または1しか取らないことを利用してbit DP



atcoder.jp
atcoder.jp
メモ:「全てのテストケースに対する~の総和は~を超えない。」系の制約に注意



atcoder.jp
atcoder.jp
メモ:たまにある、計算量解析が難しいが実は特に高速化しなくても通る問題



atcoder.jp
https://atcoder.jp/contests/abc302/submissions/62474491atcoder.jp
キーワード:整数計画問題



atcoder.jp
atcoder.jp
キーワード:Union-Find
メモ:多少実装量が増えてでも、わかりやすい(実装ミスをしにくい、意図した処理を実装できることを証明しやすい、…)実装をするべき



atcoder.jp
atcoder.jp
キーワード:平衡二分探索木、Randomized Binary Search Tree
メモ:ドキュメントとしてはこちらのほうがわかりやすい↓
ei1333.github.io
ライブラリ:
ei1333.github.io



atcoder.jp
atcoder.jp
メモ:NまたはA_iの偶奇が重要なのはわかっていたので、あとは無心で実験をするとよい



atcoder.jp
atcoder.jp
メモ:遷移が\pm aのDPをする場合は、a=0の重複に注意



atcoder.jp
atcoder.jp
メモ:青コーダーだからと言って相性の悪い緑diffがないわけではもちろんないので、本番のDやEに早々に見切りをつけて上の問題に行くのも手であると再認識

QCoder Programming Contest 002のB4~B8の勉強

はじめに

この記事は私がQPC002で解けなかった問題を理解するためのノートのようなものであり、有名な量子アルゴリズムの回路の導出などには深く立ち入りません。
より体系的に勉強したい場合は別の記事を見ることを推奨します。

B4: Quantum Fourier Transform

この問題はコンテスト中に解くことができました。

問題

www.qcoder.jp

解法

この問題は量子フーリエ変換(QFT)の回路を実装する問題です。
QFTについては、以下のサイトなどで詳しく説明されています。
dojo.qulacs.org
しかし、QCoderと異なりビッグエンディアンで実装されています。最後にスワップゲートを用いていますが、この代わりに最初にスワップゲートを用いるとリトルエンディアンでの実装になります。
公式解説のように、そもそも実装を逆順にしてしまってもよいです。
B3の題材にもなっているスワップゲートについては、以下のサイトなどで詳しく説明されています。
dojo.qulacs.org

実装

後の問題のために、QFTの回路を返す関数を実装しています。

from qiskit import QuantumCircuit
import math


def r(qc: QuantumCircuit, control: int, target: int, l: int):
    qc.cp(2*math.pi/(1<<l), control, target)

def qft(n: int) -> QuantumCircuit:
    qc = QuantumCircuit(n)
    for i in range(n//2):
        qc.cx(i, n-i-1)
        qc.cx(n-i-1, i)
        qc.cx(i, n-i-1)
    for i in range(0, n):
        qc.h(i)
        for j in range(1, n-i):
            r(qc, i+j, i, j+1)
    return qc

def solve(n: int) -> QuantumCircuit:
    qc = QuantumCircuit(n)
    # Write your code here:
    qc = qc.compose(qft(n))

    return qc

B5: Quantum Arithmetic I

この問題はコンテスト中に解くことができました。

問題

www.qcoder.jp

解法

この問題はB6以降のための前座であり、実は難しくありません。
f(x)および指数関数の性質に注目すると、位相シフトゲートを用いてj番目のQubitの位相を\exp\left(2\pi iS_j/2^m\right)ずらせばよいことがわかります。

実装

from qiskit import QuantumCircuit
import math


def solve(n: int, m: int, S: list[int]) -> QuantumCircuit:
    qc = QuantumCircuit(n)
    # Write your code here:
    for i in range(n):
        qc.p(2*math.pi*S[i]/(1<<m), i)

    return qc

B6: Quantum Arithmetic II

問題

www.qcoder.jp

解法

以下のサイトで解説されている位相推定アルゴリズムに注目します。
dojo.qulacs.org
ユニタリ行列Uと量子状態\def\ket#1{\mathinner{\left|{#1}\right\rangle}}\ket{\psi}が与えられ、\ket{\psi}Uの各固有状態\ket{\mathrm{eigen}_l}およびある定数c_l\ket{\psi}=\sum_l c_l\ket{\mathrm{eigen}_l}と表したとき、\ket{\psi}\ket{0}_m\ket{\psi}=\sum_l c_l\ket{\mathrm{eigen}_l}\ket{\lambda_l}_mと変換するのが位相推定アルゴリズムです。ここで、\ket{\lambda_l}_mUの各固有値の位相を2\piで割ってリトルエンディアンの2m桁で表した小数部分に対応する量子状態です。

さらに、B5で実装したオラクルの固有状態は各\ket{x}で、対応する固有値\exp\left(2\pi if(x)/2^m\right)であることがわかります。
そして、\ket{\lambda_l}_m\ket{f(x)\!\!\!\!\mod\! 2^m}_mと等しいこともわかります。

位相シフトゲートを2^k回かけることは1つの位相シフトゲートでできるので、位相推定アルゴリズムの回路を実装すればよいです。
ただし、上のサイトではIQFTを行う前の制御量子ビットが同じサイト内のQFTの定義と逆順に得られているのを強調していないことに注意が必要です。
トルエンディアンにおけるQFTも、先頭のQubitの位相が0桁左シフトで末尾に向かうにつれて左シフトの桁が1桁ずつ大きくなっていくことに変わりないので、k番目のQubitを使って制御位相シフトゲートを2^k回かけることには変わりはありません。よく理解せずに逆順にしてしまうとハマります。

実装

IQFTはinverse関数を使うと楽です。

from qiskit import QuantumCircuit, QuantumRegister
import math


def r(qc: QuantumCircuit, control: int, target: int, l: int):
    qc.cp(2*math.pi/(1<<l), control, target)

def qft(n: int) -> QuantumCircuit:
    qc = QuantumCircuit(n)
    for i in range(n//2):
        qc.cx(i, n-i-1)
        qc.cx(n-i-1, i)
        qc.cx(i, n-i-1)
    for i in range(0, n):
        qc.h(i)
        for j in range(1, n-i):
            r(qc, i+j, i, j+1)
    return qc

def solve(n: int, m: int, S: list[int]) -> QuantumCircuit:
    x, y = QuantumRegister(n), QuantumRegister(m)
    qc = QuantumCircuit(x, y)
    # Write your code here:
    qc.h(y)
    for k in range(m):
        for i in range(n):
            qc.cp(2*math.pi*S[i]/(1<<m)*(1<<k), y[k], x[i])
    qc = qc.compose(qft(m).inverse(), y)

    return qc

B7: Quantum Arithmetic III

問題

www.qcoder.jp

解法

B6で最初に補助量子ビットにHゲートをかけるのはそれが\ket{0}_mのQFTと等価だからなのでした。そこから、HゲートをQFTに変えればよいという予想ができます。
実際、\exp(2\pi i\times 0.j_{m-1}j_{m-2}\ldots j_0)=\exp(2\pi i\times 1.j_{m-1}j_{m-2}\ldots j_0)を考慮すると、指数法則よりHゲートをQFTに変えることで\ket{(y+f(x))\!\!\!\!\mod\! 2^m}_mのQFTが得られることがわかります。

実装

from qiskit import QuantumCircuit, QuantumRegister
import math


def r(qc: QuantumCircuit, control: int, target: int, l: int):
    qc.cp(2*math.pi/(1<<l), control, target)

def qft(n: int) -> QuantumCircuit:
    qc = QuantumCircuit(n)
    for i in range(n//2):
        qc.cx(i, n-i-1)
        qc.cx(n-i-1, i)
        qc.cx(i, n-i-1)
    for i in range(0, n):
        qc.h(i)
        for j in range(1, n-i):
            r(qc, i+j, i, j+1)
    return qc

def solve(n: int, m: int, S: list[int]) -> QuantumCircuit:
    x, y = QuantumRegister(n), QuantumRegister(m)
    qc = QuantumCircuit(x, y)
    # Write your code here:
    qft_m = qft(m)
    qc = qc.compose(qft_m, y)
    for k in range(m):
        for i in range(n):
            qc.cp(2*math.pi*S[i]/(1<<m)*(1<<k), y[k], x[i])
    qc = qc.compose(qft_m.inverse(), y)

    return qc

B8: Quantum Arithmetic IV

問題

www.qcoder.jp

解法

B6とB2が解けていればこれはウィニングランでしょう。
全体にB6を適用してから、\ket{f(x)\!\!\!\!\mod\! 2^m}_mにB2を適用し、全体にB6の逆のゲートを適用すればよいです。

実装

from qiskit import QuantumCircuit, QuantumRegister
import math


def r(qc: QuantumCircuit, control: int, target: int, l: int):
    qc.cp(2*math.pi/(1<<l), control, target)

def qft(n: int) -> QuantumCircuit:
    qc = QuantumCircuit(n)
    for i in range(n//2):
        qc.cx(i, n-i-1)
        qc.cx(n-i-1, i)
        qc.cx(i, n-i-1)
    for i in range(0, n):
        qc.h(i)
        for j in range(1, n-i):
            r(qc, i+j, i, j+1)
    return qc

def b6(n: int, m: int, S: list[int]) -> QuantumCircuit:
    x, y = QuantumRegister(n), QuantumRegister(m)
    qc = QuantumCircuit(x, y)
    # Write your code here:
    qc.h(y)
    for k in range(m):
        for i in range(n):
            qc.cp(2*math.pi*S[i]/(1<<m)*(1<<k), y[k], x[i])
    qc = qc.compose(qft(m).inverse(), y)

    return qc

def b2(n: int, L: int, theta: float) -> QuantumCircuit:
    qc = QuantumCircuit(n)
    # Write your code here:
    b=[0]*n
    for i in range(n):
        b[i]=L%2
        L//=2
    for i in range(n):
        if b[i]==0:
            qc.x(i)
    if n>1:
        qc.mcp(theta, list(range(n-1)), n-1)
    else:
        qc.p(theta, 0)
    for i in reversed(range(n)):
        if b[i]==0:
            qc.x(i)

    return qc

def solve(n: int, m: int, L: int, S: list[int], theta: float) -> QuantumCircuit:
    x, y = QuantumRegister(n), QuantumRegister(m)
    qc = QuantumCircuit(x, y)
    # Write your code here:
    qc = qc.compose(b6(n, m, S))
    qc = qc.compose(b2(m, L, theta), y)
    qc = qc.compose(b6(n, m, S).inverse())

    return qc

PuLPと制約条件の逐次的な追加で鉄則本の演習問題集のTSPを通す

AtCoderで典型的なTSPの問題あるかな…?鉄則本のTSPはN≤15, TL10sかよ…多分制約条件の逐次的な追加使わなくても通るけどまあいいや)

はじめに

巡回セールスマン問題は次のような01整数問題として定式化することができます。
\mathrm{minimize}\ \sum_{e\in E}d(e)x_e,\ \mathrm{subject\ to}\ \sum_{j\in V\setminus\lbrace i\rbrace} x_{\lbrace i, j\rbrace}=2\ (\forall i\in V),\ \sum_{{\lbrace i, j\rbrace}\subseteq S} x_{\lbrace i, j\rbrace}\leq |S|-1\ (\forall S\in 2^V\setminus\lbrace\emptyset,V\rbrace)
競プロ界隈で定期的に話題になるPuLPを用いると、鉄則本の演習問題集のB23 - Traveling Salesman Problemは以下のように解くことができます。

from math import dist
from itertools import combinations
from pulp import LpProblem, LpVariable, LpBinary, LpMinimize, lpSum, PULP_CBC_CMD

n=int(input())
x=[0.]*n
y=[0.]*n
for i in range(n):
    x[i],y[i]=(float(x) for x in input().split())

var=LpVariable.dicts("var", [(i, j) for i in range(n) for j in range(i+1, n)], 0, 1, LpBinary)
prob=LpProblem("prob", LpMinimize)

prob+=lpSum([dist((x[i], y[i]), (x[j], y[j]))*var[(i, j)] for i in range(n) for j in range(i+1, n)])

for i in range(n):
    prob+=lpSum([var[(j, i)] for j in range(i)])+lpSum([var[(i, j)] for j in range(i+1, n)])==2

for k in range(2, n-1):
    for comb in combinations(list(range(n)), k):
        lcomb=list(comb)
        prob+=lpSum([var[(lcomb[i], lcomb[j])] for i in range(k) for j in range(i+1, k)])<=k-1

prob.solve(PULP_CBC_CMD(msg=False))

print(prob.objective.value())

ただし、制約条件の個数が\Theta(2^N)になってしまうため、とても遅いです。
この問題はN≤15, TL10sなので通りますが、実行時間もメモリもギリギリでしょう。

制約条件の逐次的な追加

(正式名称がわかりません…切除平面法とは違うと思いますが…)

参考: http://dopal.cs.uec.ac.jp/okamotoy/lect/2022/ip/lect10.pdf
線形計画問題や整数計画問題について、一部の制約条件のみについての問題の最適解が全ての制約条件を満たす場合、明らかにその解は全ての制約条件についての問題の最適解でもあります。
また、巡回セールスマン問題においては得られた解が部分巡回路を持たないかどうかはDFSやUnion-Findで容易に求めることができます。

そこで、部分巡回路除去制約を持たない問題を最初に解き、得られた部分巡回路を除去する制約を追加して問題を解くことを繰り返せば、より高速になることが期待されます。

上のソースコードを書き換えると、以下のようになります。

from math import dist
from pulp import LpProblem, LpVariable, LpBinary, LpMinimize, lpSum, PULP_CBC_CMD
from atcoder.dsu import DSU

n=int(input())
x=[0.]*n
y=[0.]*n
for i in range(n):
    x[i],y[i]=(float(x) for x in input().split())

var=LpVariable.dicts("var", [(i, j) for i in range(n) for j in range(i+1, n)], 0, 1, LpBinary)
prob=LpProblem("prob", LpMinimize)

prob+=lpSum([dist((x[i], y[i]), (x[j], y[j]))*var[(i, j)] for i in range(n) for j in range(i+1, n)])

for i in range(n):
    prob+=lpSum([var[(j, i)] for j in range(i)])+lpSum([var[(i, j)] for j in range(i+1, n)])==2

unfinished=True

while unfinished:
    prob.solve(PULP_CBC_CMD(msg=False))

    unfinished=False
    cnt=0
    g=DSU(n)
    for i in range(n):
        for j in range(i+1, n):
            if var[(i, j)].value()==1:
                cnt+=1
                if cnt<n and g.same(i, j):
                    s=list(filter(lambda v: g.same(v, i), list(range(n))))
                    prob+=lpSum([var[(s[i], s[j])] for i in range(len(s)) for j in range(i+1, len(s))])<=len(s)-1
                    unfinished=True
                g.merge(i, j)

print(prob.objective.value())

特にCPythonでの提出において、実行時間もメモリもより良い結果になっています。


おわりに

このテクニックが使える類題を見つけて解こうと思ったのですが、数時間探して見つからなかったのでやけくそでこの記事を締めます笑
いかがでしたか?笑

【ネタ】動的木とカラバのアルゴリズムで最小全域木の辺の重みの総和を求める

カラバのアルゴリズム

カラバのアルゴリズム (Kalaba's Algorithm)とは、以下のように最小木問題を解くアルゴリズムです。

  • 適当な全域木をとる。
  • 最初にとった全域木に含まれない辺について、適当な順序で順に以下の操作を行う。
    • 注目する辺を全域木に追加する。
    • できた閉路(基本閉路)をなす辺で最も重みが大きい辺をとる。
    • とった辺を全域木から削除する。

組み合わせ最適化問題の文脈で、最小木問題をM凸関数の最小化と見ることで自然に導かれる貪欲アルゴリズムのようです。(私は講義で最小木問題のアルゴリズムの1つとして知っただけなので詳しくはありません。)

単純であるため一瞬これから使おうと思いましたが、冷静に考えてみるとオンラインで木の形状の変更を行いパスクエリを解かなければならないことからクラスカル法・プリム法・ブルーフカ法などと異なり容易に時間O(|E|\log|V|)を達成することが難しいです。
実用性よりも理論として重要ということでしょうか。

Link Cut Treeなどの動的木を用い、辺を表す頂点を追加した(|V|+|E|)頂点の森に対して辺の追加や削除とパスクエリを行えばならしO(|E|\log|V|)が達成できます。
動的木を自分で書かなければそこまで実装量が多いわけでもないので書きました。

ソースコード (C++)

Luzhiled's LibraryのLink Cut Treeを使用しました。

#include <algorithm>
#include <iostream>
#include <stack>
#include <utility>
#include <vector>
using namespace std;

template <typename TreeDPInfo>
struct LinkCutTree {
  using Path = typename TreeDPInfo::Path;
  using Info = typename TreeDPInfo::Info;

 private:
  struct Node {
    Node *l, *r, *p;

    Info info;

    Path sum, mus;

    bool rev;

    bool is_root() const { return not p or (p->l != this and p->r != this); }

    Node(const Info &info)
        : info(info), l(nullptr), r(nullptr), p(nullptr), rev(false) {}
  };

 public:
  using NP = Node *;

 private:
  void toggle(NP t) {
    swap(t->l, t->r);
    swap(t->sum, t->mus);
    t->rev ^= true;
  }

  void rotr(NP t) {
    NP x = t->p, y = x->p;
    push(x), push(t);
    if ((x->l = t->r)) t->r->p = x;
    t->r = x, x->p = t;
    update(x), update(t);
    if ((t->p = y)) {
      if (y->l == x) y->l = t;
      if (y->r == x) y->r = t;
    }
  }

  void rotl(NP t) {
    NP x = t->p, y = x->p;
    push(x), push(t);
    if ((x->r = t->l)) t->l->p = x;
    t->l = x, x->p = t;
    update(x), update(t);
    if ((t->p = y)) {
      if (y->l == x) y->l = t;
      if (y->r == x) y->r = t;
    }
  }

 public:
  LinkCutTree() = default;

  void push(NP t) {
    if (t->rev) {
      if (t->l) toggle(t->l);
      if (t->r) toggle(t->r);
      t->rev = false;
    }
  }

  void push_rev(NP t) {
    if (t->rev) {
      if (t->l) toggle(t->l);
      if (t->r) toggle(t->r);
      t->rev = false;
    }
  }

  void update(NP t) {
    Path key = TreeDPInfo::vertex(t->info);
    t->sum = key;
    t->mus = key;
    if (t->l) {
      t->sum = TreeDPInfo::compress(t->l->sum, t->sum);
      t->mus = TreeDPInfo::compress(t->mus, t->l->mus);
    }
    if (t->r) {
      t->sum = TreeDPInfo::compress(t->sum, t->r->sum);
      t->mus = TreeDPInfo::compress(t->r->mus, t->mus);
    }
  }

  void splay(NP t) {
    push(t);
    while (not t->is_root()) {
      NP q = t->p;
      if (q->is_root()) {
        push_rev(q), push_rev(t);
        if (q->l == t)
          rotr(t);
        else
          rotl(t);
      } else {
        NP r = q->p;
        push_rev(r), push_rev(q), push_rev(t);
        if (r->l == q) {
          if (q->l == t)
            rotr(q), rotr(t);
          else
            rotl(t), rotr(t);
        } else {
          if (q->r == t)
            rotl(q), rotl(t);
          else
            rotr(t), rotl(t);
        }
      }
    }
  }

  NP expose(NP t) {
    NP rp = nullptr;
    for (NP cur = t; cur; cur = cur->p) {
      splay(cur);
      cur->r = rp;
      update(cur);
      rp = cur;
    }
    splay(t);
    return rp;
  }

  void link(NP child, NP parent) {
    if (is_connected(child, parent)) {
      throw runtime_error(
          "child and parent must be different connected components");
    }
    if (child->l) {
      throw runtime_error("child must be root");
    }
    child->p = parent;
    parent->r = child;
    update(parent);
  }

  void cut(NP child) {
    expose(child);
    NP parent = child->l;
    if (not parent) {
      throw runtime_error("child must not be root");
    }
    child->l = nullptr;
    parent->p = nullptr;
    update(child);
  }

  void evert(NP t) {
    expose(t);
    toggle(t);
    push(t);
  }

  NP alloc(const Info &v) {
    NP t = new Node(v);
    update(t);
    return t;
  }

  bool is_connected(NP u, NP v) {
    expose(u), expose(v);
    return u == v or u->p;
  }

  vector<NP> build(vector<Info> &vs) {
    vector<NP> nodes(vs.size());
    for (int i = 0; i < (int)vs.size(); i++) {
      nodes[i] = alloc(vs[i]);
    }
    return nodes;
  }

  NP lca(NP u, NP v) {
    if (not is_connected(u, v)) return nullptr;
    expose(u);
    return expose(v);
  }

  void set_key(NP t, const Info &v) {
    expose(t);
    t->info = std::move(v);
    update(t);
  }

  const Path &query_path(NP u) {
    expose(u);
    return u->sum;
  }

  const Path &query_path(NP u, NP v) {
    evert(u);
    return query_path(v);
  }

  template <typename C>
  pair<NP, Path> find_first(NP u, const C &check) {
    expose(u);
    Path sum = TreeDPInfo::vertex(u->info);
    if (check(sum)) return {u, sum};
    u = u->l;
    while (u) {
      push(u);
      if (u->r) {
        Path nxt = TreeDPInfo::compress(u->r->sum, sum);
        if (check(nxt)) {
          u = u->r;
          continue;
        }
        sum = nxt;
      }
      Path nxt = TreeDPInfo::compress(TreeDPInfo::vertex(u->info), sum);
      if (check(nxt)) {
        splay(u);
        return {u, nxt};
      }
      sum = nxt;
      u = u->l;
    }
    return {nullptr, sum};
  }
};

struct TreeDPInfo {
    struct Path { long long max_weight; int idx; };
    struct Info { long long weight; int idx; };
    static Path vertex(const Info & u) { return {u.weight, u.idx}; };
    static Path compress(const Path& p, const Path& c) {
        if(p.max_weight>c.max_weight){
            return p;
        } else {
            return c;
        }
    };
};

struct Edge {
    int to, idx;
};

int main(void) {
    int n,m;
    cin >> n >> m;
    vector<int> a(m);
    vector<int> b(m);
    vector<long long> c(m);
    LinkCutTree<TreeDPInfo> lct;
    vector g(n, vector<Edge>());
    vector<TreeDPInfo::Info> vs(n+m);
    for(int i=0;i<n;++i){
        vs[i]={0, m};
    }
    for(int i=0;i<m;++i){
        cin >> a[i] >> b[i] >> c[i];
        --a[i];
        --b[i];
        g[a[i]].emplace_back(Edge{b[i], i});
        g[b[i]].emplace_back(Edge{a[i], i});
        vs[n+i]={c[i], i};
    }
    auto vertices=lct.build(vs);
    long long ans=0;
    stack<int> st;
    vector<bool> seen(n);
    vector<bool> used(m);
    st.emplace(0);
    seen[0]=true;
    while(!st.empty()){
        int v=st.top();
        st.pop();
        for(auto [u, i]: g[v]){
            if(!seen[u]){
                lct.evert(vertices[n+i]);
                lct.link(vertices[n+i], vertices[v]);
                lct.evert(vertices[n+i]);
                lct.link(vertices[n+i], vertices[u]);
                st.emplace(u);
                seen[u]=true;
                used[i]=true;
                ans+=c[i];
            }
        }
    }
    for(int i=0;i<m;++i){
        if(used[i]){
            continue;
        }
        int e=lct.query_path(vertices[a[i]], vertices[b[i]]).idx;
        if(c[i]<c[e]){
            lct.evert(vertices[n+e]);
            lct.cut(vertices[a[e]]);
            lct.evert(vertices[n+e]);
            lct.cut(vertices[b[e]]);
            ans-=c[e];
            lct.evert(vertices[n+i]);
            lct.link(vertices[n+i], vertices[a[i]]);
            lct.evert(vertices[n+i]);
            lct.link(vertices[n+i], vertices[b[i]]);
            ans+=c[i];
        }
    }
    cout << ans << endl;
    return 0;
}

提出 (鉄則A67)

普通に定数倍が重くて遅いです笑
atcoder.jp

感想

普通にクラスカル法かプリム法かブルーフカ法が書ければよくね?