はじめに
AtCoder Beginner Contest 279の復習記事です。
記事におけるScanner
クラスは、自作の入力クラスです。
コンテスト
https://atcoder.jp/contests/abc279
問題A
文字列S
を順にみていき、文字S[i]
がv
なら1
、w
なら2
を足した総和が答えとなります。
public static void Solve()
{
var S = Scanner.Scan<string>();
int F(char c)
{
return c switch
{
'v' => 1,
'w' => 2,
_ => 0,
};
}
var answer = 0;
foreach (var c in S)
{
answer += F(c);
}
Console.WriteLine(answer);
}
問題B
文字列S
に文字列T
が含まれているかを調べます。
方法としては以下のようなものがあります。
string
のContains
メソッド(計算量O(S)
)S
の始点を固定しT
と比較する(計算量O(ST)
)- ボイヤームーア法で探索する(計算量
O(S)
)
public static void Solve()
{
var S = Scanner.Scan<string>();
var T = Scanner.Scan<string>();
var answer = S.Contains(T);
Console.WriteLine(answer ? "Yes" : "No");
}
問題C
S
の列を並べ替えてT
と等しくできるかどうかは、S
の列の集合とT
の列の集合が一致しているか確認できればいいので、``Sの列と
T`の列をそれぞれ文字列として辞書などのデータ構造などで個数を管理することで、集合が一致しているかを比較できます。
public static void Solve()
{
var (H, W) = Scanner.Scan<int, int>();
var S = new char[H][];
var T = new char[H][];
for (var k = 0; k < 2; k++)
{
for (var i = 0; i < H; i++)
{
(k == 0 ? S : T)[i] = Scanner.Scan<string>().ToCharArray();
}
}
var dictS = new Dictionary<string, int>();
var dictT = new Dictionary<string, int>();
for (var j = 0; j < W; j++)
{
var builderS = new StringBuilder();
var builderT = new StringBuilder();
for (var i = 0; i < H; i++)
{
builderS.Append(S[i][j]);
builderT.Append(T[i][j]);
}
var s = builderS.ToString();
var t = builderT.ToString();
if (!dictS.ContainsKey(s)) dictS[s] = 0;
dictS[s]++;
if (!dictT.ContainsKey(t)) dictT[t] = 0;
dictT[t]++;
}
var answer = true;
foreach (var (s, c) in dictS)
{
answer &= dictT.ContainsKey(s) && dictT[s] == c;
}
Console.WriteLine(answer ? "Yes" : "No");
}
問題D
公式解説にもあるように、F(g)=B(g-1)+A/Sqrt(g)
は下に凸な関数であるため、g
に対する三部探索を行い、最小となるg
を求めます。
このg
は浮動小数点のため、その付近の整数値のうち、F(g)
が最小となる値が答えとなります。
また、三部探索を行う際の範囲として、F(g)<F(1)=A
以下であればいいので、1<=g<=A/B
であれば十分であることがわかります。
public static void Solve()
{
var (A, B) = Scanner.Scan<long, long>();
double F(double g)
{
return B * (g - 1) + A / Math.Sqrt(g);
}
var r = (A + B - 1) / B;
var g = (long)TernarySearch(0, r, F, 1e-2);
var answer = new[] { Math.Max(g - 1, 1), g, g + 1 }.Select(x => F(x)).Min();
Console.WriteLine(answer);
}
public static double TernarySearch(double l, double r, Func<double, double> func, double eps = 1e-9)
{
while (r - l > eps)
{
var d = (r - l) / 3;
var (ml, mr) = (l + d, r - d);
if (func(ml) < func(mr)) r = mr;
else l = ml;
}
return (l + r) / 2;
}
問題E
愚直にシミュレーションしてしまうと、時間計算量がO(M^2)
となり実行時間制限に間に合いません。
求めるものはB[j]==1
となるj
の位置S
であるため、位置S
のi
の集合を辞書などのデータ構造で管理します。
x=A[k],y=A[k]+1
としたとき、B[x]
とB[y]
を入れ替えるということは、位置x
と位置y
にあるそれぞれのi
の集合を入れ替えることになります。
また、i==k
のときは操作しないということは、入れ替えの操作時にx
のi
の集合にk
が含まれていれば、i
以外をy
に移動することになります。
このとき、一つずつ移動をしてしまうと時間計算量がO(M)
になってしまいますが、x
とy
の集合の参照を入れ替えた後に、y
の集合からk
を削除し、x
の集合にk
を追加することで、時間計算量O(logN)
で操作を行うことができます。
全体の時間計算量はO(N+MlogN)
となります。
public static void Solve()
{
var (N, M) = Scanner.Scan<int, int>();
var A = Scanner.ScanEnumerable<int>().Select(x => x - 1).ToArray();
var dict = new Dictionary<int, HashSet<int>>();
for (var i = 0; i < N; i++)
{
dict[i] = new HashSet<int>();
}
for (var i = 0; i < M; i++)
{
dict[0].Add(i);
}
for (var k = 0; k < M; k++)
{
var i = k;
var x = A[k];
var y = A[k] + 1;
(dict[x], dict[y]) = (dict[y], dict[x]);
if (dict[x].Contains(i))
{
dict[x].Remove(i);
dict[y].Add(i);
}
else if (dict[y].Contains(i))
{
dict[y].Remove(i);
dict[x].Add(i);
}
}
var answer = new int[M];
foreach (var (s, set) in dict)
{
foreach (var i in set)
{
answer[i] = s + 1;
}
}
Console.WriteLine(string.Join(Environment.NewLine, answer));
}