ひらめの日常

日常のメモをつらつらと

AtCoder: ABC127-E Cell Distance (500)

Cell Distance

問題はこちら atcoder.jp

数式があるので、それを文章にする。

NM 列のマス目のうち、K マスに駒をおく。このコストは全ての駒のペアのx座標の差 + y座標の差の和で計算される。これを全ての配置について和を取りなさい。」

考え方

愚直にやると、以下のようになり到底間に合わない。

  1. 全てのペアについて距離の合計を求める:O((NM)^{2})
  2. 全ての配置について試す: O(_ {NM} \mathrm{C} _ {K})(指数時間)

よって、それぞれについて高速化が必要である。

step1 - 和の計算の分解

 \sum _ {i=1}^{K-1} \sum _ {j=i+1}^{K}\left(\left|x _ {i}-x _ {j}\right|+\left|y _ {i}-y _ {j}\right|\right) について、xyは分解して考えることができる。

 \sum _ {i=1}^{K-1} \sum _ {j=i+1}^{K}\left|x _ {i}-x _ {j}\right| + \sum _ {i=1}^{K-1} \sum _ {j=i+1}^{K}\left|y _ {i}-y _ {j}\right|

よって、 x座標についてのコストの総和を計算し、同様にして y座標のコスト総和を計算し、それぞれを足すことで答えを得ることができる。これからはx座標のコストのみを考えていくことにする。

step2 - 計算式の整理

2点間の距離 dを固定して考えてみる。 すると、「考え方」で述べた二つの過程は次のように言い換えることができる。

  1. 全てのペアについて距離の合計を求める -> 距離 d となるようなペアの個数
  2. 全ての配置について試す -> そのペアが使われるような配置の場合の数
  3. 上記を全ての距離について試す。

step3 - 距離dとなるようなペアの個数

距離 dとなるようなペアの個数は、以下のようにして求めることができる。

まず、距離 dとなるような列の取り方が、 (M - d) 通り。そして、各列においてどの行の座標を使うかの組み合わせが N^{2} 通り。以上より、 (M - d) N^{2} 通り。

step4 - そのペアが使われるような配置の場合の数

 NM 個の座標のうち、そのペア以外の  (NM - 2) 個の候補から、座標を  (K - 2) 個えらぶような場合の数なので、 _ {NM - 2} \mathrm{C} _ {K - 2} 通り。

step 5 - 全ての距離について計算する。

疑似言語で書くとこんな感じになる。

int sumx = 0
for d in 0 to M-1:
  sumx += d * (M - d) * N * N

sumx *= combination(N * M - 2,  K  - 2)

...
同様にしてN, Mを入れ替えてsumyも計算する
...
print(sumx + sumy)

解答

modを取ったり、combinationでmodの逆元をとることなどを忘れないようにする。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const ll mod = 1000000007;

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

/* ------------- ANSWER ------------- */
/* ---------------------------------- */


const int MAX = 510000;
long long fac[MAX], finv[MAX], inv[MAX];

// テーブルを作る前処理
void COMinit() {
    fac[0] = fac[1] = 1;
    finv[0] = finv[1] = 1;
    inv[1] = 1;
    for (int i = 2; i < MAX; i++) {
        fac[i] = fac[i - 1] * i % mod;
        inv[i] = mod - inv[mod % i] * (mod / i) % mod;
        finv[i] = finv[i - 1] * inv[i] % mod;
    }
}

// 二項係数計算
ll COM(int n, int k) {
    if (n < k) return 0;
    if (n < 0 || k < 0) return 0;
    return fac[n] * (finv[k] * finv[n - k] % mod) % mod;
}

int main() {

    ll n, m, k;
    cin >> n >> m >> k;
    COMinit();

    // sum for x
    ll sum_x = 0;
    rep(i, m) sum_x += i * (m - i) * n * n;
    sum_x %= mod;
    sum_x *= COM(n * m - 2, k - 2);
    sum_x %= mod;

    // sum for y
    ll sum_y = 0;
    rep(i, n) sum_y += i * (n - i) * m * m;
    sum_y %= mod;
    sum_y *= COM(n * m - 2, k - 2);
    sum_y %= mod;

    cout << (sum_y + sum_x) % mod << endl;
    return 0;
}

Submission #6260827 - AtCoder Beginner Contest 127

AtCoder: ABC131-F Must Be Rectangular! (600)

Must Be Rectangular!

問題はこちら

atcoder.jp

考え方

解説放送が一番わかりやすいと思うので、そちらを見ると良い。

https://www.youtube.com/watch?v=XI8exXVxZ-Qwww.youtube.com

座標を二部グラフで捉えるという考え方は典型らしい。

下記の座標を、x座標とy座標をそれぞれ頂点としてもつ二部グラフで考えてみると、長さ3のpathが存在する場合は、その始点と終点を結ぶことで新たな頂点が追加されることになる。

f:id:thescript1210:20190628082017j:plain:w300
座標
f:id:thescript1210:20190628082030j:plain:w300
二部グラフ

また、連結成分に関しては、その部分が完全二部グラフになるまで辺を追加することができる。これは長さが3より大きいところに関しては長さ3になるようにpathを調整することで新たに辺を追加することが可能になるから。

完全二部グラフを作るための辺の数は、それぞれのグループに属する頂点数を掛け算すれば良い。答えは新たに張ることのできる辺の数なので、答えは以下のようになる。

連結成分のx座標の個数 * 連結成分のy座標の個数 - もともと存在していた辺の数n

解答

解説放送を聞いて実装上のテクニック

  • 二部グラフの表現の方法。x座標: xy座標: y + MAXV とすることでいつものグラフ構造を用いながら、二部グラフを表現することができる。
  • 連結成分の数の保持の方法。大きさ2のvectorを用意し、cnt[e / MAXV]++ とすることで、今見ている頂点がx座標に属するかかy座標に属するかを場合分けしている。

抜粋部分

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
#define rep(i, n) for(ll i = 0; i < (ll)(n); i++)
#define each(i, mp) for(auto& i:mp)

const ll MAXV = 1e5 + 5;
vector<ll> xy[MAXV * 2];
vector<ll> cnt;
bool visited[MAXV * 2];

void dfs(ll now) {
    visited[now] = true;
    each(e, xy[now]) {
        if (visited[e]) continue;
        cnt[e / MAXV]++;
        dfs(e);
    }
}

int main() {
    ll n;
    cin >> n;
    rep(i, n) {
        ll x, y;
        cin >> x >> y;
        xy[x].emplace_back(y + MAXV);
        xy[y + MAXV].emplace_back(x);
    }
    ll ans = 0;
    rep(i, MAXV * 2) {
        if (visited[i]) continue;
        cnt = vector<ll>(2);
        cnt[i / MAXV]++;
        dfs(i);
        ans += cnt[0] * cnt[1];
    }
    cout << ans - n << endl;
    return 0;
}

Submission #6142912 - AtCoder Beginner Contest 131

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の復元が容易であるということ.