tatyam’s blog

(ノ) - ω - (ヾ)モチモチ

ABC145 解説

27分遅刻の42分全完でした f:id:tatyam_prime:20191117003620p:plain

A - Circle

半径 r の円の面積は、半径 1 の円の面積の何倍ですか?

円の面積は \pi r^2 であることを思い出すと r^2 倍である。

#include <bits/stdc++.h>
using namespace std;

int main(){
    int r;
    cin >> r;
    cout << r * r << endl;
}

B - Echo

文字列 S は、ある文字列を2回繰り返したものですか?

n が2で割れないなら No、2で割れるなら文字列比較をする。

#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    string s;
    cin >> n >> s;
    if(n & 1){
        puts("No");
        return 0;
    }
    n /= 2;
    puts(s.substr(0, n) == s.substr(n, n) ? "Yes" : "No");
}

C - Average Length

ハミルトン路の長さの平均はいくつですか?

ハミルトン路の長さの合計をとって N! で割る。

8!*8=322560 なので、 O(N!N) が間に合う。
ある辺を使う回数は N!(N-1) / _NC_2 = 2(N-1)! 回なので O(N^2) でも解ける。

O(N!N) 解

#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;

double dist(pii a, pii b){
    return sqrt((a.first - b.first) * (a.first - b.first) + (a.second - b.second) * (a.second - b.second));
}
int main(){
    int n;
    cin >> n;
    vector<pii> a(n);
    for(auto& i : a) cin >> i.first >> i.second;
    vector<int> perm(n);
    iota(perm.begin(), perm.end(), 0);
    double ans = 0;
    do{
        for(int i = 0; i < n - 1; i++) ans += dist(a[perm[i]], a[perm[i + 1]]);
    }while(next_permutation(perm.begin(), perm.end())); // 順列全探索はこれが便利
    for(int i = 1; i <= n; i++) ans /= i;
    cout << fixed << setprecision(20) << ans << endl; // 誤差に注意
}

O(N2) 解

#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;

double dist(pii a, pii b){
    return sqrt((a.first - b.first) * (a.first - b.first) + (a.second - b.second) * (a.second - b.second));
}
int main(){
    int n;
    cin >> n;
    vector<pii> a(n);
    for(auto& i : a) cin >> i.first >> i.second;
    double ans = 0;
    for(int i = 0; i < n; i++) for(int j = i + 1; j < n; j++) ans += dist(a[i], a[j]);
    ans *= 2;
    ans /= n;
    cout << fixed << setprecision(20) << ans << endl;
}

D - Knight

(0,0) にある駒を (i+1,j+2) または (i+2,j+1) に動かして (X,Y) に移動させる時の通り数はいくつですか?

どちらの操作も x+y 方向に 3 動いているから、 (X + Y) % 3 != 0 なら 0
操作回数を N とすると、 N=(X+Y)/3
どちらの操作も x, y 方向ともに 1 以上動いているから、次のように読み換える。

(0,0) にある駒を (i,j+1) または (i+1,j) に動かして (X - N,Y - N) に移動させる時の通り数はいくつですか? これは有名問題で、 (i+1,j) に動くのが X - N 回、 (i,j+1) に動くのが Y - N 回の並び替えになるから、 _{X+Y-2N}C_{X-N} 通り。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1000000007;
ll modpow(ll a, ll b){
    ll ans = 1;
    while(b){
        if(b & 1){
            ans *= a;
            ans %= MOD;
        }
        a *= a;
        a %= MOD;
        b /= 2;
    }
    return ans;
}

int main(){
    ll x, y;
    cin >> x >> y;
    if((x + y) % 3 || x > y * 2 || y > x * 2){
        puts("0");
        return 0;
    }
    ll n = (x + y) / 3;
    x -= n;
    y -= n;
    vector<ll> fac(x + y + 1), inv(x + y + 1); // fac[i] = i! mod 1000000007, inv[i] = 1 / i! mod 1000000007
    fac[0] = 1;
    for(ll i = 1; i <= x + y; i++) fac[i] = fac[i - 1] * i % MOD;
    inv.back() = modpow(fac.back(), MOD - 2);
    for(ll i = x + y; i > 0; i--) inv[i - 1] = inv[i] * i % MOD;
    cout << fac[x + y] * inv[x] % MOD * inv[y] % MOD << endl;
}

E - All-you-can-eat

見た感じナップサック問題なのでDPを疑う。
最後時間ギリギリに注文できるのが厄介。 でも O(N^2) くらいじゃないと間に合わない。
→ 最後に注文する料理をDPの状態に持てばいい。
あとはナップサック問題と同じ

DPの持ち方

dp[i][j][0]= 料理 i まで見たときに j 分までに食べ終わるときの美味しさの合計の最大値 dp[i][j][1]= 料理 i まで見たときに j 分までに食べ終わり、その後料理を1つ注文するときの美味しさの合計の最大値
このままだとメモリをかなり喰うが、遷移の順番を気をつければ、dp[T][2] に圧縮することができる。

DPの遷移

dp[i+1][j][0]=max(dp[i][j][0], dp[i][j - A][0]+B)
dp[i+1][j][1]=max(dp[i][j][1], dp[i][j - A][1]+B, dp[i][j][0] + B)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
template<class T> bool chmax(T& a, const T& b){ if(a < b){ a = b; return 1; } return 0; }

int main(){
    ll n, t;
    cin >> n >> t;
    ll dp[n + 1][t][2];
    memset(dp, 0x00, sizeof(dp));
    for(ll i = 0; i < n; i++){
        ll a, b;
        cin >> a >> b;
        for(ll j = 0; j < t; j++){
            dp[i + 1][j][0] = dp[i][j][0];
            dp[i + 1][j][1] = max(dp[i][j][1], dp[i][j][0] + b);
            if(j >= a) chmax(dp[i + 1][j][0], dp[i][j - a][0] + b);
            if(j >= a) chmax(dp[i + 1][j][1], dp[i][j - a][1] + b);
        }
    }
    cout << max(dp[n][t - 1][0], dp[n][t - 1][1]) << endl;
}

圧縮版

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
template<class T> bool chmax(T& a, const T& b){ if(a < b){ a = b; return 1; } return 0; }

int main(){
    ll n, t;
    cin >> n >> t;
    vector<vector<ll>> dp(t, vector<ll>(2));
    for(ll i = 0; i < n; i++){
        ll a, b;
        cin >> a >> b;
        for(ll i = t; i --> a; ) chmax(dp[i][1], dp[i - a][1] + b);
        for(ll i = 0; i < t; i++) chmax(dp[i][1], dp[i][0] + b);
        for(ll i = t; i --> a; ) chmax(dp[i][0], dp[i - a][0] + b);
    }
    cout << max(dp[t - 1][0], dp[t - 1][1]) << endl;
}

F - Laminate

N 項の数列 H のうち K 項以下を変えた時の \sum_{i=0}^{N-1}{max(0, H[i] - H[i-1])} の最小値を求めよ。(H[-1]=0)

谷を埋めたり、山を削ったりすると小さくなる。
K 項以下がやっかいだけど、部分問題に落とせそうなのでDPを疑う。

DPの持ち方

dp[i][j][k]= 数列を i まで見たときに j 項を変えて、最後の項が k のときの最小値

DPの遷移

dp[i][j][k]=\min_l(dp[i-1][j-1][l]+max(0, k - l))
{\rm chmin}(dp[i][j][H[i]], \min_l(dp[i-1][j][k]+max(0, H[i] - l)))

しかし、このままでは O(N^4) でTLEになってしまう。
O(N^4) かかっている上の遷移の無駄について考えると、ある項を変えるときは、前の項と同じにするのがよいとわかる。(どんな項が続いても山も谷もできないため)

dp[i][j][k]=dp[i-1][j-1][k]
{\rm chmin}(dp[i][j][H[i]], \min_k(dp[i-1][j][k]+max(0, H[i] - k)))

k の値が不連続であるため、unordered_map を使いたくなるが、 TLEする可能性が高いので座標圧縮する。

サンプルコードは疲れたのでこれ