ひらめの日常

日常のメモをつらつらと

AtCoder: ABC128-E Roadwork (500)

Roadwork

問題はこちら

atcoder.jp

考え方

まずは、 d_j に出発した人がどのような条件を満たした時に  x_i の地点で通行止に引っかかるかを考えてみる。すると、1秒ごとに1進むので、以下の条件を満たしているときに通行止に引っかかるということが分かる。

  •  s_i - x_i \leq d_j \lt t_i - x_i

これを愚直にやると、 N 箇所の通行止について、 Q 人の出発時間を調べるので  N \times Q のオーダーになり制限時間に間に合わない。

そこで、 各々の通行止区間について、そこで止まるものを取り除く という風に考えてみる。具体的には、一番手前で止まった場合はもうそれ以降の区間について考える必要がないので、調べる集合から取り除くことができるという考え。

なぜこれを行うと間に合うのか?

  • setでスタート時間  d_1, ..., d_j, ..., d_Q を管理する。
  • まず通行止区間を全て調べるので、ここに  O(N)
  • 各通行止区間について、 s_i - x_i \leq d_j となるような  d_jlower_bound で見つける。ここに  O(log(Q))
  •  d_j \lt t_i - x_i となっている限り、set からスタート時間を削除し、 d_j にスタートした人の答えは  x_i となる。ここは最大でも Q 回までしか回ることはない。要素の削除には  O(log(Q))

以上より、合計の計算量は  Nlog(Q) + Qlog(Q) = (N+Q)log(Q) になる。

具体的な実装

  • structのvectorとして、 s, t, x を管理。 x の昇順にsortする。
  • 各スタート時間とindexのpairを、setにいれて管理。
  • それぞれの通行止区間について、 s_i - x_i \leq d_j \lt t_i - x_i を満たすような  d_j があれば、setからeraseする。
    • この時、別に答え出力用のanswer配列を用意しておき、answer[index] x_j を代入する。
  • answerを出力する。

解答

抜粋部分

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

#define rep(i, n) for(ll i = 0; i < (ll)(n); i++)

struct stx {
    ll s, t, x;

    stx(ll s, ll t, ll x) : s(s), t(t), x(x) {}
};

int main() {
    ll n, q;
    cin >> n >> q;
    vector<stx> v;
    rep(i, n) {
        ll s, t, x;
        cin >> s >> t >> x;
        stx temp(s, t, x);
        v.emplace_back(temp);
    }
    set<pair<ll, ll>> set1;
    rep(i, q) {
        ll d;
        cin >> d;
        set1.insert(pair<ll, ll>(d, i));
    }
    auto comp = [](stx &a, stx &b) {
        if (a.x == b.x) {
            if (a.s == b.s) return a.t < b.t;
            return a.s < b.s;
        }
        return a.x < b.x;
    };
    sort(v.begin(), v.end(), comp);

    vector<ll> ans(q, -1);
    rep(i, n) {
        ll s = v[i].s, x = v[i].x, t = v[i].t;
        auto it = set1.lower_bound(pair<ll, ll>(s - x, -1));
        while (it != set1.end()) {
            if (t - x <= it->first) break;
            ans[it->second] = x;
            set1.erase(it++);
        }
    }
    rep(i, q) cout << ans[i] << endl;
    return 0;
}

Submission #6087534 - AtCoder Beginner Contest 128

AtCoder: ABC130-E Common Subsequence (500)

Common Subsequence

問題はこちら

atcoder.jp

この問題はLCS(Longest common subsequence)と関わりが深いのでまずはそれから書いてみる。

LCS

最長の共通部分列の長さを求めてくださいというのが典型的な問題。次のような例を考えてみる。

s: abcd
t: acfed

 s_i = t_j (= x) の時、これまでの共通部分列の末尾にそれぞれ x を加えることで長さが+1された共通部分列を新しく作ることができる。 例でみると、 s_i = d, t_j = d の時、今までのLCSは "ac" なので、"acd" を作ることでより長いものを作ることができる。

よって、常にLCSを持ちながら遷移するdpを実装することで答えを求めることができる。遷移は次の通り。

  •  dp[i + 1][j + 1] :  s_i までと  t_j までの中のLCS
  •  s_i = t_j の時:  dp[i + 1][j + 1] = dp[i][j] + 1
  • それ以外:  dp[i + 1][j + 1]  = max(dp[i][j + 1], dp[i + 1][j])

今回の問題

それぞれちょうど s_i t_jで終わるものの共通部分列の数を数えてみる。

 s_i = t_j (= x) の時、これまでに存在していた全ての共通部分列に対して、末尾にそれぞれ  x を加えることで、新しい共通部分列を作ることができる。つまり、これまでに存在していた全ての共通部分列(=  s _ {i-1}までと t _ {j - 1}までに存在する全ての共通部分列)の数だけ増える。さらに、 x 一文字のみのものも増えるので、これに+1されることになる。

遷移テーブルのイメージは次のようになる。水色の部分を例とすると、"abc" までと "abcfe" までの共通部分列の数は水色で囲われた部分の和になっている。なので、 "abcd" までと "abcfed" までの共通部分列の数は水色で囲われた部分 + d自身のみの場合である。

f:id:thescript1210:20190618181932j:plain:w300
遷移テーブル

  •  sum[i + 1][j + 1]:  s_i までと  t_j までの共通部分列の個数の二次元累積和。
  •  dp[i + 1][j + 1] :  s_i までで ちょうど s_iで終わるもの と、 t_j までで ちょうど t_jで終わるもの の共通部分列の数。
  •  s_i = t_j の時:  dp[i + 1][j + 1] = sum[i][j] + 1
  • それ以外:  dp[i + 1][j + 1]  = 0
  • 累積和の更新:  sum[i + 1][j + 1] = sum[i][j + 1] + sum[i + 1][j] - sum[i][j] + dp[i + 1][j + 1]

解答

抜粋部分

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
#define rep(i, n) for(ll i = 0; i < (ll)(n); i++)
const ll mod = 1e9 + 7;

int main() {
    ll n, m;
    cin >> n >> m;
    vector<ll> s(n), t(m);
    rep(i, n) cin >> s[i];
    rep(i, m) cin >> t[i];

    vector<vector<ll>> dp(n + 1, vector<ll>(m + 1, 0));
    vector<vector<ll>> sum(n + 1, vector<ll>(m + 1, 0));
    rep(i, n) {
        rep(j, m) {
            if (s[i] == t[j]) {
                dp[i + 1][j + 1] = sum[i][j] + 1;
            } else {
                dp[i + 1][j + 1] = 0;
            }
            dp[i + 1][j + 1] %= mod;
            sum[i + 1][j + 1] = sum[i][j + 1] + sum[i + 1][j] + dp[i + 1][j + 1];
            sum[i + 1][j + 1] %= mod;
            sum[i + 1][j + 1] = (sum[i + 1][j + 1] + mod - sum[i][j]) % mod;
        }
    }

    ll ans = 1;
    rep(i, n + 1) rep(j, m + 1) ans += dp[i][j], ans %= mod;
    cout << ans << endl;
    return 0;
}

Submission #6008512 - AtCoder Beginner Contest 130

参考

betrue12.hateblo.jp

qiita.com

Educational Codeforces Round 63 D. Beautiful Array

Beautiful Array

問題はこちら

codeforces.com

自分が気づいたところ

まず最初に,xが負の時は最小区間和を選んできてそれをx倍する.xが正の時は最大区間和を選んできてそれをx倍する.とすれば最大値が求まると考えた.ここでセグ木とかをググり始める... だが,区間和を取ってきて,その区間和のindexも取ってくるのはどのようにして実装すればいいかわからなかった.

次に,累積和を尺取りのように持てば区間和の最大・最小を検出できるのではないかと考えたが,これもACにはならず.

解説を読んで

TwitterでDPだという文面を見たので,実装は見ないで自分で書いてみた.状態の遷移は以下の通りにできると考えた.

ここでは3の状態がポイントで,一回*xをする→そのまま足すという遷移をした場合は,もう一度*xをして足すことはできない(連続した区間にのみ*xできる)ためにそれを新たに状態として持つことにした.

dp[i+1][0] := i番目をそのまま足す
dp[i+1][1] := i番目を*xして足す
dp[i+1][2] := 使わない(0にリセット)
dp[i+1][3] := i番目までに*xをしたことがあり,i番目のものはそのまま足す

状態遷移はそれぞれを場合分けしてイテレーションを回していく.詳しくはコードを見ると早いと思う.

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
#define rep(i, n) for(ll i = 0; i < (ll)(n); i++)

const ll half_inf = ll(1e5);
ll dp[3 * half_inf + 10][4];

int main() {
    ll n, x;
    cin >> n >> x;
    vector<ll> a(n);
    rep(i, n) cin >> a[i];

    const int plus = 0, multi = 1, no = 2, al_multi = 3;
    for (int i = 0; i < n; ++i) {
        dp[i + 1][al_multi] = max({dp[i][multi] + a[i], dp[i][al_multi] + a[i]});
        dp[i + 1][plus] = max({dp[i][plus] + a[i], dp[i][no] + a[i]});
        dp[i + 1][multi] = max({dp[i][multi] + x * a[i],
                                dp[i][no] + x * a[i],
                                dp[i][plus] + x * a[i]
                               });
        dp[i + 1][no] = 0;
    }
    ll ans = 0;
    for (int i = 0; i <= n; ++i) {
        for (int j = 0; j < 4; ++j) {
            ans = max(dp[i][j], ans);
        }
    }
    cout << ans << endl;
    return 0;
}

Submission #53197896 - Codeforces

こちらの解説を見るとどうやら状態数は3つでいいらしい??(後ほど実装したい)

cinnamo-coder.hatenablog.com

こちらの解法が似ているらしいので後ほど解きたい.

atcoder.jp

自分の詰まったところ

  • そもそもDPという発想がコンテスト中に浮かんでこなかった.
  • 遷移の歴史によって,次に行える遷移が違うがそれをどう実装するかについて.

学んだこと

  • 遷移規則が複数あるが,列挙できる → DPという発想.
    • 連続区間を 使う/使わない などはかなり典型例になるのでは??
  • 状態を増やすことによって遷移規則を表現できることがある.

AtCoder: Tenka1 2019-C Stones (300)

Stones

問題はこちら

atcoder.jp

自分が気づいたところ

  1. 全ての石が黒 or 白になると勘違いをし, WA
  2. 左端から ... と白が続いている時は無視し,そのさきは全て黒 or 白になると勘違いし,WA
  3. 右端から ### と黒が続いている時も無視できることに気づくが,黒→白に変更することを実装せず,WA * 2
  4. なぜか1, 2, 3を合体させた実装を提出し,WA
  5. ここでようやく ...##というならびになるような境界をずらし,累積和を使って計算してくことに気づくが,境界条件をミスってWA

ということで,結果としては6ペナ1完でひどいことになりました. f:id:thescript1210:20190421071658p:plain

Submission #5069132 - Tenka1 Programmer Contest 2019

自分の学んだこと

  • 早解きのモードになっていたので,簡単な実装で何回もWAを連発してはいけない.
  • 境界条件をずらして行くときに,全てが黒 or 白の場合もきちんと考慮すること.
  • 自分のヒューリスティックな解法が証明されたものでなければ,きちんと状態を列挙して解を求めに行くこと.

Codeforces Round #552 (Div. 3) E. Two Teams

Two Teams

問題はこちら

codeforces.com

自分が気づいたところ

愚直解は,毎回使っていない最大値を検索→幅  kを探索し,使っていないものをk個チームに属させるというもの. 計算量としては,  n=O(10^{5}) なのでそのまま実装すると  O(n^{2})になるので間に合わない.

そこで,一回使ったものは取り除いて考えることができるようなデータ構造を考えれば良いと気づいた.

ここで,すでに使用したindexを扱う問題としてこれを解いたなーと思いながらも,帰着させることはできず.

B - Minimum Sum

AGC005-B 「Minimum Sum」 (400) - Mister雑記

解説を読んで

解説はこちらを参考にさせていただいた(いつもありがとうございます...!). cinnamo-coder.hatenablog.com

ある要素から右方向にあって、まだチームが決まっていない選手の最小のindexを持つことでこれは達成できる。左方向にも同じものを作っておく。

そこで,以下のように実装し,随時これをupdateする実装をした.

set<int> s : 使用してないスキルの集合
l[i] : i番目の人から,左方向でまだチームが決まっていない選手の最小のindex
r[i] : i番目の人から,右方向でまだチームが決まっていない選手の最小のindex
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
#define rep(i, n) for(ll i = 0; i < (ll)(n); i++)

int main() {
    ll n, k;
    cin >> n >> k;
    vector<ll> a(n), idx(n);
    rep(i, n) {
        cin >> a[i];
        idx[--a[i]] = i;
    }
    set<ll> s; // 使ってないもの
    rep(i, n) s.insert(i);
    vector<ll> l(n), r(n);
    rep(i, n) {
        if (i == 0) l[i] = -1;
        else l[i] = i - 1;
    }
    rep(i, n) {
        if (i == n - 1) r[i] = -1;
        else r[i] = i + 1;
    }

    vector<ll> ans(n);
    ll turn = 1;
    while (!s.empty()) {
        ll maxval = *s.rbegin();
        s.erase(maxval);
        ll i = idx[maxval];
        ans[i] = turn;
        ll li = i, ri = i;
        for (int j = 0; j < k; ++j) {
            if (l[li] == -1) {
                break;
            }
            s.erase(a[l[li]]);
            li = l[li];
            ans[li] = turn;
        }
        for (int j = 0; j < k; ++j) {
            if (r[ri] == -1) {
                break;
            }
            s.erase(a[r[ri]]);
            ri = r[ri];
            ans[ri] = turn;
        }
        ll nli = l[li], nri = r[ri];
        if (nri != -1) l[nri] = nli;
        if (nli != -1) r[nli] = nri;
        turn = 3 - turn;
    }
    rep(i, n) cout << ans[i];
    return 0;
}

https://codeforces.com/contest/1154/submission/52917361

自分の詰まったところ

l[i], r[i]が端に到達した時の扱い方として,-1を立てるという方法にしたが,ここでバグを発生させていた.一回きちんと落ち着いて手で書いてからやった方が良い.

純粋にindex周りを扱うデータ構造の実装が遅い.

学んだこと

  • 最短の距離にあるものをそれぞれupdateしていくとうこと.
  • 値がdistinctな時には,array[それぞれの値] = index という構造を使うことでindexの復元が容易であるということ.

Codeforces Round #551 (Div.2) C Serval and Parenthesis Sequence

Serval and Parenthesis Sequence

問題はこちら 問題文難しくないですか...

codeforces.com

自分が気づいたところ

括弧を扱う問題では,stackを用いると綺麗に書けるなと思い実装.しかしコンテスト時間内に問題文を解読できなかった...

解説を読んで

このブログとかが簡潔かつ分かりやすかった. perogram.hateblo.jp

使える(の数をカウントしておく(=cとする).

?が現れたとき,c>0の場合貪欲に(を使う.なぜなら,なるべく(を左に持っていった方が途中で正しい数式になってしまう可能性が下がるから.c==0の場合は,(を使っても文字列全体として正しい数式になることはないので,)を使う.

そして,括弧の管理はstackで行う.s[i]=='('の時はstackにpush.s[i] == ')'の時は,stackが空でなければpop.といったようにする.stackが空になるということは,その時点で()の数が等しい.→正しい数式になっている.

これを踏まえると求められている条件は,「出力文字の途中でstackが空にならない.かつ,最後にはstackが空になる」であるので,それに従って貪欲に解を構成できるか試す.

https://codeforces.com/contest/1153/submission/52734359

自分の詰まったところ

  • 問題文の解読.
  • 変に左右の括弧の数を数えて累積和とかやってた.
  • 不正解となり得るパターンをどのようにして判定するか.

学んだこと

  • 片方を使える分だけ貪欲に使っていくと,もう片方とのマッチング可能性は下がっていく.
  • stackを使わなくても,( → +1, ( → -1という風に変換してあげればもっと簡単に計算できる.

JOI2018/2019-D 日本沈没(難易度6)

日本沈没

問題はこちら

atcoder.jp

自分が気づいたところ

小課題1に関しては,愚直にシミュレーションしていけば求まるが,満点をもらうことはできない.

  A _ {i-1} \gt A _ {i} \lt A _ {i+1} のように,谷になっているものに注目した.すると,問題は「谷となっているところのうち,一番低いところ  b と,左右の崖のうち低い方 l の間の区間  b \leq s \lt l が,一番重なっているところはどこか」という風に言い換えることができる.

...しかし区間の重なりと捉えたところで計算量が落ちるわけではないのでよく分からない...

解説を読んで

状態ごとに捉えてみる.具体的には,「注目している陸地が一つ沈んだ時に島の数はどうなるか?」について注目する.すると,注目している陸地の左右に注目するだけで変化を捉えることができる.考えていることは近いが,自分は谷に注目していたので処理が煩雑になっていた...

JOI 2018/2019 予選 問題4 解説

そこで,陸地をsortして順番に海面を上げていき,陸地の数の増加を数えれば良い.計算量はsortがボトルネックとなり  Nlog(N)

Submission #4936590 - JOI2018/2019 予選ページ

自分の詰まったところ

  • 陸地が同じ高さで連続している時に,余分に引き算をしていたので,入力を読み込む時に連続しているものは弾くことが必要.
  • 全ての陸地が0の時にバグが発生すので,陸地の入力の最初と最後に高さ0を入れることで解決.

学んだこと

  • 注目するものを転換するということは大事だと思った.一つのものに注目していて解法が出ない時,その対照的なものに注目すると答えにつながる可能性がある.
  • 状態ごとに捉える(i-1回目からi回目への遷移でどのように答えが変化するか)ことが苦手なので,トレーニングが必要.
  • 重複は入力の時点で弾く.
  • コーナーケースに対しては,入力の両端に自分で何か加えるとうまくいくことも多い(lower_bound()の時などが思い返される...)