AtCoder Beginner Contest 130
Updated:
- Review: 2020-05-18
Source codes
Solutions
A - Rounding
指示通りやるだけ。
B - Bounding
$D _ i$ たちを実際に求める。 $\sharp \{ 0 \leq i \leq N \mid D _ i \leq X \}$ を出力する。
C - Rectangle Cutting
面積については、半分 $WH/2$ が必ず達成できる。
切り方については、 $x = W/2$ かつ $y = H/2$ の場合にのみ 1
、残りは 0
を出力する。
D - Enough Array
解法
全体の個数 $N(N + 1) / 2$ から $K$ 未満の個数を引くことにする。尺取り法で実装すればよろしい。
尺取り法の実装
左端 $i$ を普通にループを回す のが良さそうである。
半開区間 $[left, right)$ を意識して実装する。
int j = 0;
ll ans = 0;
for (auto i = 0; i < N; i++)
{
while (j < N && s + a[j] < K)
{
s += a[j++];
}
ans += j - i;
s -= a[i];
}
cout << N * (N + 1) / 2 - ans << endl;
ポイント
余事象を使った方が確かに簡単ですね。
E - Common Subsequence
LCS を求める問題とかなり近い。
基本の確認: LCS の求め方
Definition
$dp[i][j] = S[0, i)$ と $T[0, j)$ の LCS の長さ
と定めると、これは配る DP で実装できる。
Initial state
初期状態: $dp[0][0] = 1$, otherwise $0$.
Answer
答え: $dp[N][M]$.
Transition
遷移は以下の通り。
\[
\begin{align}
dp[i + 1][j] &\gets \max(dp[i + 1][j], dp[i][j]), & & \tag{E.1} \\
dp[i][j + 1] &\gets \max(dp[i][j + 1], dp[i][j]), & & \tag{E.2} \\
dp[i + 1][j + 1] &\gets \max(dp[i + 1][j + 1], dp[i][j] + 1) & & \text{ if }S[i] = T[j]. \tag{E.3}
\end{align}
\]
解説放送の解法
しかしこの問題では、共通文字列の総数を求めるように言われている。上のをほぼ $\mathbin{ {+} {=} }$ にすれば良さそうであるが、それだとうまくいかない。例えば $S = (1, 2)$, $T = (2, 1)$ の場合、最初の文字が違うのに $dp[1][1] = 1 + 1 = 2$ と計算され、おかしい。これは $() = ()$ の 1 通りを $dp[0][1]$ と $dp[1][0]$ で重複して合算しているからである。
そこで、この遷移を $1$ 通りに絞るようにする。 $i$ だけ進めるのは $DP0$, $j$ だけ進めるのが $DP1$ であるようにする。以下のように定める。
Definition
$DP[i][j] = S[0, i)$ と $T[0, j)$ の共通部分列の総数
としておく。これ自体は使わず、次のものだけ実装する。
$DP0[i][j] = DP[i][j]$ を求めるために $i$ だけ進めたもの、
$DP1[i][j] = DP[i][j]$ を求めるために $j$ だけ進めたもの。
$DP0, DP1$ は $DP$ を求めるための sub_table である。
先ほどの例だと $DP0[0][0] \to DP0[1][0] \to DP1[1][0] \to DP1[1][1]$ のという $1$ 通りの遷移しか許されないので、これで重複がなくなり、答えが正しく求まる。
Initial state
初期状態: $DP0[0][0] = 1$, otherwise $0$.
Answer
答え: $DP1[N][M]$.
Transition
遷移は以下の通り。
\[
\begin{align}
DP0[i + 1][j] & \mathbin{ {+} {=} } DP0[i][j], & & \\
DP1[i][j] & \mathbin{ {+} {=} } DP0[i][j], & & \\
DP1[i][j + 1] & \mathbin{ {+} {=} } DP1[i][j], & & \\
DP0[i + 1][j + 1] & \mathbin{ {+} {=} } DP1[i][j], & & \text{ if } S[i] = T[j] \\
\end{align}
\]
ポイント
別解はあるが、 snuke さんの方法が綺麗だと思う。
F - Minimum Bounding Box
解答
まず $x, y$ 軸は独立に考えておく。それぞれの軸について、考察するべき点は最大でも $6$ 通りしかない。速度 $-1, 0, 1$ で動く初期座標が最大・最小の点の $6$ パターンである。これらの相対的な位置だけが問題なので、それぞれ速度 $0, 1, 2$ であるとして構わない。軸の「幅」は、区分的に定数かまたは $1$ 次関数である。
したがって、矩形の面積 $S(t)$ は、有限個の区分的に、それぞれ最大でも $2$ 次の連続関数である。時刻 $\infty$ では矩形の面積が $\infty$ になる、または、全部の時間で定数であるから、適当な有界区間で最小値は達成される。ここで区間ごとに次の場合分けをする。
- $S(t)$ が定数関数の区間:どの時刻でも同じ値を与える。
- $S(t)$ が $1$ 次関数の区間:正の傾きだろうが負の傾きだろうが、端点で最小値を与える。
- $S(t)$ が $2$ 次関数の区間:上に凸の場合は、端点で最小値を与える。下に凸の場合は、ただの $2$ 次関数だと最小値が $2$ 次関数の軸の部分という可能性があり得るのだが、今回の場合はない。なぜなら、 $S(t)$ が下に凸であるということは、 $x$ 軸方向にも $y$ 軸方向にも増えている、または、減っているのどちらかである。よって「区間に軸の部分に含まれる」という可能性はない。ゆえに、最小値は区間の端点でとる。
したがって最小値の候補となるのは、 $S(t)$ を区分した際の端点に限られる。すなわち、各軸の傾きが切り替わる点だけである。それはすなわち、速度 $0, 1, 2$ の点がすれ違う時に絞られる。これらを各軸で全列挙して、全部の最小値を求めればよろしい。
実装で間違いやすいところ
- 負の時刻を考慮から確実にはずす。 すれ違う時刻を求めると負になる場合があるが、これは実現されないので候補から排除する。
- 同様に、時刻 $0$ を確実に考慮にいれる。
- 初期値は $0$ の時刻のものを使う。 $10^{18}$ 程度では足りない。
- 大きい数字を扱っているので、
long double
型を使う。 実装にもよるが、double
型だと誤差が出て WA となる可能性がある。
解説放送の実装
各軸ごとに class を作るのが筋がいいだろう。以下では double get(double t);
で矩形の幅を与えている。また vector<double> events();
で最大・最小それぞれで、すれ違う時刻を求めている。
class D
{
public:
vector<double> l, r;
D() : l(3, infty), r(3, -infty) {}
void add(double x, int v)
{
++v;
mins(l[v], x);
maxs(r[v], x);
}
double get(double t)
{
double nl = infty, nr = -infty;
for (auto i = 0; i < 3; i++)
{
mins(nl, l[i] + i * t);
maxs(nr, r[i] + i * t);
}
return nr - nl;
}
vector<double> events()
{
vector<double> ts;
for (auto i = 0; i < 3; i++)
{
for (auto j = 0; j < i; j++)
{
double t = (l[j] - l[i]) / (i - j);
if (t > 0)
{
ts.push_back(t);
}
t = (r[j] - r[i]) / (i - j);
if (t > 0)
{
ts.push_back(t);
}
}
}
return ts;
}
};
後は events()
を組み合わせて、それぞれの for (auto t : ts)
に対して面積を求めればよろしい。
int main()
{
// init();
cin >> N;
D xd, yd;
for (auto i = 0; i < N; i++)
{
int x, y;
char d;
cin >> x >> y >> d;
if (d == 'L')
{
xd.add(x, -1);
yd.add(y, 0);
}
if (d == 'R')
{
xd.add(x, 1);
yd.add(y, 0);
}
if (d == 'U')
{
xd.add(x, 0);
yd.add(y, 1);
}
if (d == 'D')
{
xd.add(x, 0);
yd.add(y, -1);
}
}
vector<double> ts;
ts.push_back(0);
ts.push_back(infty);
{
auto nts = xd.events();
ts.insert(ts.end(), nts.begin(), nts.end());
}
{
auto nts = yd.events();
ts.insert(ts.end(), nts.begin(), nts.end());
}
double ans = 100000000000000000;
for (auto t : ts)
{
double now = xd.get(t) * yd.get(t);
mins(ans, now);
}
cout << fixed << setprecision(12) << ans << endl;
}
ポイント
上はほぼ snuke さんの実装です。非常に参考になる。しかしこれは多分実装ができなかっただろうね。