謝罪
常体と敬体がごっちゃです。ごめんなさい。
エンタメとしてお楽しみください。
解法を書くのに、公式解説、公式解説放送を主に参考にしています。それだけで理解できなかったものは、他の方のブログを参考にしています。その際は、参考にしたブログのリンクを貼っています。
ABC212-G Power Pair
問題概要
素数Pが与えられ、xn≡y(modP)を満たすnが存在する(x,y)の組(0≤x,y≤P−1)の個数を求める問題。
素数Pに対する原始根rが必ず存在するため、x=ra,y=rbとか表現すると、
xn≡y(modP)⇔ran≡rb(modP)⇔an≡b(modP−1)
となるので(最後の変換はフェルマーの小定理より)、an≡b(modP−1)を満たす(a,b)の組(1≤a,b≤P−1)を数えれば良い。
さて、ここからどう数えるかだが、
- P−1とaの最大公約数が1であるようなaを見つけてきたとき、n=1,2,⋯P−1のときbはP−1通りの値を取る。
- P−1とaの最大公約数が2であるようなaを見つけてきたとき、n=1,2,⋯P−1のときbは(P−1)/2通りの値を取る。
というように、P−1とaの最大公約数がgであるようなaを見つけてきたとき、n=1,2,⋯P−1のときbは(P−1)/g通りの値を取ることがわかる。
a=1,2,⋯P−1のそれぞれに対してP−1との最大公約数を取るのはO(PlogP)かかってしまうので、GCD(P−1,a)の値がgになるようなaの個数を数えることにする。これはgが大きい順に数えるとうまくいく(説明がめんどいのでコードを貼ってごまかす)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| mint ans = 1;
map<long,long> mp;
for(auto&i:f){ //fはP-1の約数(降順)
// GCD(P-1, x) = iとなるxの個数を求める
long n = P/i;
for(auto&j:f){
if(j<=i)continue;
if(j%i==0){
n -= mp[j];
}
}
mint a = P/i;
mint b = n;
ans += a*b;
mp[i] = n;
}
|
提出コード
感想
原始根って便利なんですね
ABC212-H Nim Counting
問題概要
長さKの数列(A1,A2,⋯,AK)が与えられ(1≤Ai≤216)、ここから1個以上N個以下の数を選び(重複可能)、それらの選んだ数のXORをとったとき、0にならないような選び方の個数を求める問題。
この問題ではxorの畳み込みという技術を使います。xorの演算子を⊕、xor畳み込みの演算子を∗とすると、xor畳み込みというのは、
A=(a1,a2,⋯,an)B=(b1,b2,⋯,bn)ci=x⊕y=i∑axby
となるようなベクトルC=A∗B=(c1,c2,⋯,cn)を求めることで、O(nlogn)で行えます。すなわち、これでxor畳み込み後に添え字が0$以外の要素の総和を求めることでこの問題は解けそうです。
Hadamard変換
天下り的ですが、
H0Hk=1=(Ek−1Ek−1Ek−1−Ek−1)(Hk 00Hk)
と行列Hiを定義します(Ekはk次の単位行列)。このとき、Hkは2k次の行列です。
HkHk=2kE2k
が成り立ちますので、Hk−1=2−kHkとなります。また、
長さ2kのベクトルAとBに対して次が成り立ちます。
(HkA)(HkB)=Hk(A∗B)
左辺はベクトルの各要素ごとの積を表しています。これを変形すると、
A∗B=2−kHk((HkA)(HkB))
が成り立ちます。しかし、これは長さ2kのベクトルと2k次の正方行列の積の計算をする必要がありますので、n=2kとして、結局O(n2)かかってしまいます。
しかし、この行列Hkとベクトルの積はうまいことに高速化ができます。
高速Walsh-Hadamard変換
Hkを変形します。
Hk=(Ek−1Ek−1Ek−1−Ek−1)(Hk 00Hk)=(Ek−1Ek−1Ek−1−Ek−1)⎝⎛Ek−2Ek−200Ek−2−Ek−20000Ek−2Ek−200Ek−2−Ek−2⎠⎞⎝⎛Hk−20000Hk−20000Hk−20000Hk−2⎠⎞=⋯
この行列の積への分解は高々O(logn)回で終わります(上記の変形をし続けると最後にH0が対角上に並んだ行列、すなわち単位行列が現れます)。
そして、各分解された行列をよく見ると、各行で0ではない要素は必ず2つです。すなわち、この分解された行列単体とベクトルの積は、O(n)で行うことができ、行列はO(logn)個しかありませんので、結局O(nlogn)で行うことができます。
ピンと来ないという私のために、n=8のときのH3を分解してみます。
H3=(E2E2E2−E2)⎝⎛E1E100E1−E10000E1E100E1−E1⎠⎞⎝⎛E0E0000000E0−E000000000E0E0000000E0−E000000000E0E0000000E0−E000000000E0E0000000E0−E0⎠⎞⎝⎛H000000000H000000000H000000000H000000000H000000000H000000000H000000000H0⎠⎞=⎝⎛100010000100010000100010000100011000−100001000−100001000−100001000−1⎠⎞⎝⎛101000000101000010−100000010−100000000101000000101000010−100000010−1⎠⎞⎝⎛110000001−100000000110000001−100000000110000001−100000000110000001−1⎠⎞
これを実装するのにも結構頭を使います。
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
| template<class T> vector<T> Hadamard(const vector<T>&A,const int k){
// 2^k次行列
if((int)A.size()!=(1<<k)){
cout<<"A size should be 2^k"<<endl;
return vector<T>();
}
vector<T> res = A;
int h = 1;
for(int i=0;i<k;i++){
vector<T> tmp(1<<k,0);
for(int j=0;j<(1<<k);j+=h*2){
for(int l=j;l<j+h;l++){
tmp[l] = res[l] + res[l+h];
tmp[l+h] = res[l] - res[l+h];
}
}
h <<= 1;
res = tmp;
}
return res;
}
template<class T> vector<T> xor_convolution(const vector<T>&A,const vector<T>&B,const int k){
// xor convolution
// A,Bのサイズは2^k
vector<T> a = Hadamard(A,k);
vector<T> b = Hadamard(B,k);
vector<T> res(1<<k);
for(int i=0;i<(1<<k);i++){
res[i] = a[i] * b[i];
}
res = Hadamard(res,k);
return res; //これを2^kで割る必要がある
}
|
このコードは最後に得た値の各要素を2kで割り算してやる必要があるので注意です。
問題に戻る
問題で求めたいのは、C=(c1,c2,⋯,c216)(ciはi個の石がある山の個数)があって、
H(H(C))+H((H(C))2)+H((H(C))3)+⋯+H((H(C))N)
です(Ciは各要素のi乗を取ることを意味します)。Hadamard変換は線形変換なので、次のように変形できます。D=H(C)とすると、
H(D+D2+D3+⋯+DN)
かなりスッキリしました。あとは、等比数列の和の公式を使って、括弧の中身を計算することができます。
等比数列の和の公式は
di−1di(diN−1)
です。di=0,1の場合はこのまま計算すると値がおかしくなるのでそこだけ別処理することに注意します。
提出コード
参考サイト
感想
これのAND版やOR版もあるらしい。考えた人天才。
ABC213-G Connectivity 2
問題概要
N頂点M辺の単純無向グラフGで、0本以上の辺を取り除いて新しいグラフHを作る。各k=2,3,⋯,Nに対して、頂点1とkがHで連結になるような辺の取り除き方の個数を求める問題。
言い換え
問題で求められている「頂点1とkが連結(ry」の個数をC(k)とします。また、次のようにf(S),g(S)を定義します。
- f(S):= Sを頂点集合とするGの連結部分グラフの個数
- g(S):= Sを頂点集合とするHの部分グラフの個数
すると、次のようにC(k)を表現できます。ちなみにVはGの頂点集合です。
C(k)={1,k}⊂S⊂V∑f(S)g(V∖S)
全てのf(S),g(S)が既知ならば、これはO(N2N)で計算できます。
g(S)の計算
これは簡単です。
辺の両端が頂点集合Sに含まれるような辺の個数をE(S)とすれば、2E(S)がg(S)になります。
各頂点集合についてM個の辺がそれぞれ含まれているかどうかを判定するのでO(M2N)で計算できます。
f(S)の計算
問題はこちらです。f(S)=g(S)−(頂点集合がSであるような非連結なグラフの個数)です。g(S)は既知なので、後者を計算する必要があります。
連結でないグラフというのは、連結成分が2個以上あるグラフのことです。「この2個以上の連結成分」というのを、「1つの連結成分があって、残りの使ってない頂点たちを好きなようにしてもらう」という数え方をします。
Sに含まれる頂点を1つ取ってきて、これをvとするとf(S)は次のようになります(このvがないとカウントが重複してしまいます)。
f(S)=g(S)−v∈T⊊S∑f(T)g(S∖T)
これをSについていい感じの順番で計算していくことで、O(3N)で計算できます。
部分集合の部分集合の列挙
部分集合のiの部分集合jの列挙はO(3N)でできます。
1
2
3
4
5
6
| for (int i = 0; i < (1 << N); i++) {
for (int j = i; j >= 0; j--) {
j &= i;
// (i, j) は条件を満たす
}
}
|
提出コード
感想
f(S),g(S)に分けてg(S)の計算方法考えるのむずくね??
ABC213-H Stroll
問題概要
問題
DPで解けそうな問題設定だが、時間が間に合わない。
DP解
ds,tをtキロメートル歩いて地点sにいる通り数とすると次のように計算できる。
ds,t=(s′,i,x)∑ds′,t−x×pi,t−x
ただし、シグマ記号は「s′からsに向かうi番目の長さxの道」を全てのペアについて足し合わせることを意味する。
これをtの小さい方から計算すればO(MT2)で計算できるがこれだと時間がかかりすぎる。
シンプルにN=2のときを考えると、
d1,t=u∑d2,u×pt−u
という式になって、畳み込みっぽい見た目をしている。実際にはd2,uが定数ならば畳み込みで計算できる。
分割統治FFT
定数ならば畳み込みができるということで、半分を事前に計算して定数にし、いい感じに再帰的に計算するとなんとO(Tlog2T)で計算できてしまうのだ。言葉での説明が難しいので図を用意した。
図は単純に1次元のDPを分割統治FFTで解いたものである。

分割統治FFT
ただ、計算に使用する数値は全て確定していなければならないので、以下のような順番で計算する必要がある。

分割統治FFT(計算順序)
今回は、頂点ごとにDPの値を持つが、全ての辺について上記の図のような遷移があるので、各遷移を全ての辺ごとに行う。計算量はO(MTlog2T)となる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| void DCFFT(int l,int r,vector<edge>&E,vector<vector<mint>>&p, vector<vector<mint>>&dp){
int m = (l+r)/2;
if(l+1==r)return ;
DCFFT(l,m,E,p,dp);
for(int i=0;i<(int)E.size();i++){
auto [u,v] = E[i];
// [m,r)を更新
/*
本来だったら
for(int i=m;i<r;i++){
dp[v][i] += sum_x (dp[u][i-x]*p[v][x]);
}
をやるが、FFTを使う
*/
auto dp2u = vector<mint>( dp[u].begin()+l, dp[u].begin()+m );
auto p2 = vector<mint>( p[i].begin(), p[i].begin()+r-l );
auto dp3u = convolution(dp2u,p2);
for(int j=m;j<r;j++){
dp[v][j] += dp3u[j-l];
}
}
DCFFT(m,r,E,p,dp);
}
|
提出コード
感想
添字をミスりまくってめっちゃ時間かかった。
地味に赤diff初AC。
ABC214-G Three Permutations
包除原理
条件が扱いづらいので、包除原理を使います。
- f(x)を、x個の相異なる添字を選んだときに、その添字でpi=riまたはqi=riとなるようなriの割り当て方の個数とする。
すると、包除原理により、求める答えは
N!+x=1∑N(−1)xf(x)(N−x)!
となります。(N−x)!は残りのN−x個の添字に対する割り当て方の個数です。
この数え方にすぐに納得しない私は具体例を考えてみました。
- N=3,p=(1,2,3),q=(2,3,1)のとき
- r=(3,1,2)の1通りだけであるが、これを上記の計算式で求める。
- f(1)について
- i=1を被らせるとき、r1=1,2のどちらかで2通り
- (1,3,2),(1,2,3),(2,1,3),(2,3,1)の4通りがある
- i=2を被らせるとき、r2=2,3のどちらかで2通り
- (2,1,3),(2,3,1),(3,1,2),(3,2,1)の4通りがある
- i=3を被らせるとき、r3=3,1のどちらかで2通り
- (3,1,2),(3,2,1),(1,2,3),(1,3,2)の4通りがある
- f(1)=2+2+2
- ここで注目したいのが、例えば(1,2,3)という順列はi=1とi=2のパターンの両方に含まれているということである。ここで2回取り除かれている順列は、f(2)以降で調整されていくのが包除原理である。
- f(2)について
- i=1,2を被らせるとき(1,2,3),(1,3,2),(2,3,1)の3通りがある
- i=2,3を被らせるとき(1,2,3),(3,2,1),(2,3,1)の3通りがある
- i=3,1を被らせるとき(1,2,3),(2,1,3),(2,3,1)の3通りがある
- f(2)=9
- ここで、(1,2,3),(2,3,1)のパターンは逆に足しすぎていので、f(3)で調整する。
- f(3)について
- i=1,2,3を被らせるとき(1,2,3),(2,3,1)の2通りがある
- f(3)=2
- よって、3!−6×2!+9×1!−2×0!=6−12+9−2=1となり、正しく計算できていることがわかる。
グラフに変換
グラフを使います。頂点をN個用意して、頂点piとqiを繋ぐ辺を張ります。

辺を張る
ここで注意するべきは、連結成分は必ずサイクルになります。なぜなら、p,qはどちらも順列であるので、同じ数がちょうど2回出現する⇔頂点の次数が2であるからです。「各辺は、その両端の頂点番号を使ってはいけない」ことを示します。
次の画像は被らせる添字とグラフの対応を表したものです(画像には「サイクル」とありますが、「連結成分」と書くべきでした)。

被らせる添字を選ぶ
元のグラフの連結成分ごとに、k個の辺を選ぶ(=k個の数を被らせる)方法の場合の数をそれぞれ計算すれば、DPによってf(x)を計算できます。
次の節ではそれぞれの連結成分で、k個の辺を選ぶ方法の場合の数を計算する方法を説明します。
DPで計算
サイクル状の頂点と辺を、v1,e1,v2,e2,⋯,vy,ey,v1と割り振ります。
- DP[i][j] := i番目までの辺を使って、j個の辺を選んだときの場合の数
しかし、これだと情報が不十分です。辺e1を選ぶとv1,v2のどちらかを選べます。e1がv2を選んだ場合、e2はv3を選ぶしかありません。逆に、e1がv1を選んだか、e1を選んでいない場合は、e2はv2,v3の2つの選択肢から選ぶことができます。よって、この情報を追加します。
さらに、円環なので、一番初めの頂点v1が使われたという情報によって、eyがv1が選べるかどうかが決まります。よって、この情報も追加します。
- DP[i][j][k][l] := i番目までの辺を使って、j個の辺を選んだとき、i番目の辺がvi+1を選んだ/選んでいない(k=0/1)、v1を使った/使っていない(l=0/1)の場合の数
こちらのDPですが、遷移が多いので注意深く実装します。
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| mint dp[3001][3001][2][2];
vector<mint> f(int n){
if(n==1){
return vector<mint>{mint(1), mint(1)};
}
rep(i,3001)rep(j,3001)rep(k,2)rep(l,2)dp[i][j][k][l] = mint(0);
// nはサイクルの長さ
// dp[i][j][k][l] :=
// i番目までの辺を見て、j個使ったとき、
// k=0: 直前を使っていない k=1: 直前を使った
// l=0: 0を使っていない l=1: 0を使った
dp[0][0][0][0] = 1;
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++){
if(i==1){
// 使わない
dp[i][j][0][0] += dp[i-1][j][0][0];
if(j>0){
// 前を使う
dp[i][j][0][1] += dp[i-1][j-1][0][0];
// 後ろを使う
dp[i][j][1][0] += dp[i-1][j-1][0][0];
}
}else if(i==n){
// 使わない
dp[i][j][0][0] += dp[i-1][j][0][0];
dp[i][j][0][0] += dp[i-1][j][1][0];
dp[i][j][0][1] += dp[i-1][j][0][1];
dp[i][j][0][1] += dp[i-1][j][1][1];
if(j>0){
// 前を使う
dp[i][j][0][0] += dp[i-1][j-1][0][0];
dp[i][j][0][1] += dp[i-1][j-1][0][1];
// 後ろを使う
dp[i][j][1][0] += dp[i-1][j-1][0][0];
dp[i][j][1][0] += dp[i-1][j-1][1][0];
}
}else{
rep(l,2){
// 使わない
dp[i][j][0][l] += dp[i-1][j][0][l];
dp[i][j][0][l] += dp[i-1][j][1][l];
if(j>0){
// 前を使う
dp[i][j][0][l] += dp[i-1][j-1][0][l];
// 後ろを使う
dp[i][j][1][l] += dp[i-1][j-1][0][l];
dp[i][j][1][l] += dp[i-1][j-1][1][l];
}
}
}
}
}
vector<mint> res(n+1, mint(0));
rep(j,n+1){
rep(k,2){
rep(l,2){
res[j] += dp[n][j][k][l];
}
}
}
return res;
}
|
提出コード
ABC214-H Collecting
問題概要
問題リンク
K人で分担して有向グラフ上の落とし物をできるだけ多く拾う問題。
最小費用流問題
強連結成分にある落とし物はすべて取るのが最適なので、強連結成分一つをまとめて1つの頂点として考えた新しいグラフを作ります。そのグラフはDAGになります。(このとき、頂点1から辿ることができない頂点とその頂点から出てる辺は無視します)。
サンプル1の場合、次のようなグラフを作りSからTへK=2だけ流せば最小コストの符号を取ったものが答えとなる。

最小費用流
しかし、負辺があるので、(私が知っている)アルゴリズムが適用できない。
負辺を取り除いたものがこちらです。

負辺を取り除く
SからTまでグラフにそって適当なルートを通ったときに、通った辺のコストが「全体の落とし物のうち拾えなかった個数」を示しています。
このようなグラフは、強連結成分分解後にM頂点にしてトポロジカル順に頂点番号をつけたグラフについて、次のような操作によって機械的に構築できます。
- 各頂点uごとにiuとouを用意して、iuからouに(1,0)と(∞,Xu)の辺を張る。Xu
- 辺(u,v)に対して、ouからivに(∞,Xu+1+Xu+2+⋯+Xv−1)の辺を張る。
- Sからi1に(K,0)の辺を張る。
- 各頂点uごとに、ouからTに(∞,Xu+1+Xu+2+⋯+XM)の辺を張る。
各頂点について容量が1の辺があるのは、一人だけ「Xuを拾えなかった」ことを回避できるからです。
よって、求まったコストをK∑uXuから引けば答えになります。
提出コード
参考サイト
感想
フローのこういうテクニックは覚えていきたい。
ABC215-G Colorful Candies 2
N個のキャンディがあり、i番目のキャンディの色はCiである。
K=1,2,⋯,Nについて、N個のうちK個のキャンディを選ぶ(同様に確からしい)とき、キャンディの種類数の期待値を求める問題。
式を作る
とりあえずKを固定して考える。確率変数Xiを種類iのキャンディを1以上選ぶとき1、選ばないとき0とすると、答えは次の式で求められる。
E[i∑Xi]=i∑E[Xi]
線形性で右辺に変形できる。その種類のキャンディがm個あった場合に1つ以上選ぶ組み合わせはNCK−N−mCK通りと二項係数で計算できる。
この計算方法だと、各Kについて最大N種類の計算をすることになるので、O(N2)かかりそうだが、ちょうどx個ある種類のキャンディをまとめて数え上げればO(N(N))になる(詳しくはコード、計算量の節を参照)。
メインの実装自体はかなり簡潔。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| int main(void){
std::cin.tie(0)->sync_with_stdio(0);
int N;cin>>N;
Combination<mint> comb(N+1);
map<int,int>mp;
rep(i,N){
int c;cin>>c;
mp[c]++;
}
map<int,int> mp2;
for(auto&[k,v]:mp){
mp2[v]++; //v個あるキャンディの種類数
}
for(int k=1;k<=N;k++){
mint ans = 0;
for(auto&[m,a]:mp2){
mint A = a;
ans += (comb.com(N,k) - comb.com(N-m,k)) / comb.com(N,k) * A;
}
cout << ans << endl;
}
}
|
map
を使っているため定数倍が重め。
提出コード
計算量について
aiを種類iの個数だとすると、種類数(キャンディが1個以上ある)がM個、キャンディの総数がN個のとき、次の条件たちを満たす。
- a1+a2+⋯+aM=N
- 1≤ai≤N
計算量は、aiの種類数をDとすると、O(DN)である。Dは高々O(N)にしかならないというのがミソである。
なぜなら、Dをできるだけ大きくしようとすると、aiはできるだけ小さくするべきなので、
a1=1,a2=2,⋯と設定していくべきであり、1+2+3+⋯+D=O(D2)=O(N)となる。
感想
今回の記事の中ではかなり簡単に見えるが、Xiの定義を思いつくところは経験値が必要だと思った。