けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

Codeforces Round #609 (Div. 1) A. Long Beautiful Integer (R1700)

こういうのは、いかにシンプルにできるか...

問題へのリンク

問題概要

 N 桁の整数  a が与えられる (文字列として)。leading zero はない。整数  K (\lt N) が与えられる。 a 以上の整数  b であって

  •  b i 桁目と  i+K 桁目の値は等しい

という条件を満たす最小値を求めよ。

制約

  •  2 \le N \le 10^{5}

考えたこと

落ち着くんだ...ひとまず、最悪でも 999...9 (N 桁) が条件を満たすので、桁数を増やす必要はない。あと、上から  K 桁分を決めれば全部決まるので、上から  K 桁を決める問題ということになる。

  • もし、上から  K 桁のところが元のままでも、それを繰り返したときに  a 以上になるなら、それが答え
  • それが  a 未満であるならば、上から  K 桁の部分を 1 増やしたものが答え

ということになる。

#include <iostream>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <cstring>
#include <string>
#include <vector>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <numeric>
#include <utility>
#include <iomanip>
#include <algorithm>
#include <functional>
#include <unordered_map>
using namespace std;
 
int N, K;
string a;
 
string plusone(string s) {
    int id = (int)s.size() - 1;
    while (s[id] == '9') {
        s[id] = '0';
        --id;
    }
    ++s[id];
    return s;
}
 
string solve() {
    string b = a;
    for (int i = K; i < N; ++i) b[i] = a[i%K];
    if (b >= a) return b;
    string res = "";
    string prefix = plusone(a.substr(0, K));
    for (int i = 0; i < N; ++i) res += prefix[i%K];
    return res;
}
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    while (cin >> N >> K >> a) {
        string res = solve();
        cout << res.size() << endl;
        cout << res << endl;
    }
}

yukicoder No.974 最後の日までに

めちゃくちゃ面白い!!!

問題へのリンク

問題概要 (意訳)

思いを寄せている先輩に  W 週後に告白したいので、その日までに先輩の好感度を最大化しておきたいです。最初、「所持金」と「好感度」はともに 0 です。毎週、以下のいずれかの行動を起こすことができます。

  • 学校に行く: ただし、2 週連続で学校に行くことはできない。学校に行った次の日は、デートをしなければならない ( W 週目は例外)
  • アルバイト: 所持金が  a_{i} ( i 週目の場合) 増える
  • デートする: 学校に行った次の日の場合は、好感度が  b_{i} だけ増えて、所持金が  c_{i} 減る。そうでない場合は何も変化しない。

 W 週後には、所持金が 0 以上である必要がある。 W 週後の好感度の最大値を求めよ。

制約

  •  1 \le W \le 52

考えたこと

要するに、毎日以下の 2 通りのパターンがありうる

  • アルバイトする
  • その日学校行って、次の日にデートする

残り日数が  N 週である場合、前者は残り  N-1 週となって、後者は残り  N-2 週となることに注意する。よって、単純な全探索を行った場合の計算量を  T(N) とすると、

  •  T(N) = T(N-1) + T(N-2) + O(1)

となって、これは実は  \gamma = \frac{1 + \sqrt{5}}{2} として、 T(N) = O(\gamma^{N}) となる。 O(2^{N}) ではないのだ。 \gamma = 1.618... くらい。

半分全列挙

あとは、部分和問題に対して半分全列挙をして  O(N 2^{\frac{N}{2}}) とするように、この問題でも半分全列挙すれば  O(N \gamma^{\frac{N}{2}}) となる。  N \le 52 より、十分間に合うのだ!!!!!面白い!!!!!

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;

template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; }
template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; }

using pll = pair<long long, long long>;
const long long INF = 1LL<<60;
int N;
vector<long long> a, b, c;

// ma[所持金 down 分] = 好感度 up 分
void rec(int n, int finish, map<long long, long long> &ma, long long syoji = 0, long long love = 0) {
    if (n == finish) {
        chmax(ma[-syoji], love);
        return;
    }

    // アルバイト
    rec(n + 1, finish, ma, syoji + a[n], love);

    // 学校からのデート
    if (n + 2 <= finish) rec(n + 2, finish, ma, syoji - c[n+1], love + b[n+1]);
}

long long solve(int mid) {
    // 前半と後半
    map<long long, long long> former, latter;
    rec(0, mid, former);
    rec(mid, N, latter);

    // 後半の累積 max
    long long cur = -INF;
    map<long long, long long> maxlatter;
    maxlatter[-INF] = -INF;
    for (auto it : latter) {
        maxlatter[it.first] = max(cur, it.second);
        chmax(cur, it.second);
    }

    // まとめ
    long long res = 0;
    for (auto it : former) {
        long long need = -(it.first);
        auto it2 = maxlatter.upper_bound(need);
        --it2;
        chmax(res, it.second + it2->second);
    }
    return res;
}

int main() {
    cin >> N;
    a.resize(N), b.resize(N), c.resize(N);
    for (int i = 0; i < N; ++i) cin >> a[i] >> b[i] >> c[i];
    cout << max(solve(N/2), solve(N/2+1)) << endl;
}

キーエンス プログラミング コンテスト 2020 E - Bichromization (黄色, 900 点)

実装に精彩を欠いてしまった...コンテスト中に WA を取りきれず...

WA の原因は「すでに色を決めたはずの頂点について再度色を上書きしていることがある (現在見ている D 値について、それより小さい値の頂点とはつながっておらず、等しい D 値同士で結ばれた辺の両端をともに着色する場合)」というものだった。

問題へのリンク

問題概要

 N 頂点、 M 辺からなる連結な無向単純グラフが与えられる。各頂点  v には整数値  D_{v} がついている。今、各頂点を白・黒に塗り分けて、各辺に 1 以上 109 以下の整数値の重みを割り振って、以下の条件を満たすようにしたい。

  • 白く塗られた頂点が存在
  • 黒く塗られた頂点が存在
  • どの頂点  v についても、その頂点と色が異なるような頂点のうち、もっとも距離が近い頂点への最短路長が  D_{v} である

このようなことが可能かどうかを判定し、可能ならば具体的な解を一つ構成せよ。

制約

  •  2 \le N \le 10^{5}
  •  1 \le M \le 2 \times 10^{5}

考えたこと

最初は摑みどころがないので、どういう場合ができないのかを考えてみることにした。たとえば

  • N = 2
  • M = 1
  • E = {(1, 2)}
  • D = {1, 3}

とかはできない。頂点 1 と頂点 2 を別々の色に塗ることになるが、それらを結ぶ辺の値を 3 未満にはできない。なぜならそうすると、頂点 2 から頂点 1 への最短路が 3 未満であることになって、D[2] = 3 に矛盾するため。よって辺の値は 3 でなければならないが、今度は D[1] = 1 に矛盾してしまう。

というわけで、N = 2、M = 1 の場合は、D[0] と D[1] が等しくなければならない。一般に、辺 (u, v) の両端を別々の色で塗るとき、辺の値は max(u, v) 以上でなければならず、u と v のうちの D の値が小さい側については、他の頂点との最短路がその値になるようにする必要がある。

...ということを考えると、究極の「D の値が最小の頂点」に着目したくなる。

D の値が最小の頂点に着目する

まず、D の値が最小の頂点 v が、単独で存在したとする。このとき、絶対に無理。なぜなら、v と隣接するどの頂点 w についても、辺 (u, v) の値は、max(Du, Dv) (> Dv) 以上でなければならない。しかしそうすると、v についての条件を満たせない。

逆に、D の値の最小値を m としたとき、D_v = m となる v に隣接する頂点で存在して D の値が m である場合は、上手く行く。その辺に m を割り当てればよい。

整理すると、D の値が m (最小値) であるような頂点集合が以下の条件を満たす必要がある。

  • 隣接関係がある部分についてグループ分けしたとき、孤立しているグループを含まない (グループがいくつかに分かれるのは OK)

次に、D が 2 番目に大きい頂点 v の集合について考察すると、今度は以下のことが必要であるといえる

  • 頂点 v が、D の値が最小の頂点と隣接している
  • 頂点 v が、D の値が同じ頂点と隣接している

いずれも、その隣接している頂点をどれか一つ選んで、その辺に値 D[v] を振れば OK。一般に、同様にして以下のことがいえる


任意の頂点  v について、 v に隣接する頂点のうち、ある頂点  w が存在して、 D_{w} \le D_{v} となることが必要である


そしてこれが十分でもあるのだ!!!証明は実はそんなに難しくない。

  • 各頂点  v に対して、 D_{w} \le D_{v} が成立するよな頂点  w を見つけ、辺  (v, w) のみを取り出す

ということをしてできる部分グラフを考えると、その各連結成分は

  • サイクル

のいずれかになる。サイクルの場合は、それに含まれるすべての頂点の D 値が等しいことになる。よって、辺を上手く入れ替えれば木にできる。よって結局、各連結成分は木になる場合のみ考えればよい。木の場合は、その木に含まれる各辺 (u, v) について、その値を max(D[u], D[v]) とすれば OK。さらに木は二部グラフなので、どの辺の両端も色が異なるようにできる。そして、サイクルにも木にも属さない辺は悪さをしないようにすべて INF にしておく。

このように構成した解は条件を満たしている。

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
using pint = pair<int,int>;
using Graph = vector<vector<pint>>;
const int INF = 1000000000;

int N, M;
Graph G;
vector<int> D;

void solve() {
    map<int, vector<int>> ma;
    for (int v = 0; v < N; ++v) ma[D[v]].push_back(v);

    vector<int> color(N, -1);
    vector<int> res(M, INF);
    for (auto it : ma) {
        int val = it.first;
        auto vs = it.second;
        for (auto v : vs) {
            if (color[v] != -1) continue; // 既に処理済み
            bool exist = false;
            for (auto e : G[v]) {
                int nv = e.first;
                if (D[v] < D[nv]) continue;
                exist = true;
                res[e.second] = val;
                if (color[nv] == -1) {
                    color[v] = 0;
                    color[nv] = 1;
                }
                else color[v] = 1 - color[nv];
                break;
            }
            if (!exist) {
                cout << -1 << endl;
                return;
            }
        }
    }
    for (int i = 0; i < N; ++i) {
        if (color[i] == 0) cout << "W";
        else cout << "B";
    }
    cout << endl;
    for (int i = 0; i < M; ++i) cout << res[i] << endl;
}

int main() {
    cin >> N >> M;
    D.resize(N);
    for (int i = 0; i < N; ++i) cin >> D[i];
    G.assign(N, vector<pint>());
    for (int i = 0; i < M; ++i) {
        int u, v; cin >> u >> v; --u, --v;
        G[u].emplace_back(v, i);
        G[v].emplace_back(u, i);
    }
    solve();
}

キーエンス プログラミング コンテスト 2020 D - Swap and Flip (青色, 700 点)

隣接 swap 操作を行いながら最小回数でソートする系の問題は、過去に何度も出てる!!!

drken1215.hatenablog.com

drken1215.hatenablog.com

これらはいずれも自然に DP で解くことができる。今回も。なお、「どれを表にするかを決め打って転倒数を求める」という方法もあるっぽい。

問題へのリンク

問題概要

表に  A_{i}、裏に  B_{i} と書かれた  N 枚のカードが一列に並んでいる。以下の操作を繰り返すことで、表に出ているカードが広義単調増加となるようにしたい。最小回数を求めよ。

  • 隣り合う 2 枚のカードを選んで swap し、ともに裏返す

制約

  •  1 \le N \le 18
  •  1 \le A_{i}, B_{i} \le 50

考えたこと

操作がわかりづらいけど、もともと i 番目にあったカードが最終的に j 番目に移動するとき

  • | i - j | が偶数ならば、表の数値は A[ i ]
  • | i - j | が奇数ならば、表の数値は B[ i ]

になるというイメージ。これを踏まえて、最終的な位置がどうなるべきなのかを求めていくことになる。

そして、この手の隣接 swap 系の問題は、似た雰囲気の DP でできることが多い。今回は

  • dp[ bit ][ s ] :=  N 枚のカードのうち、bit で表されるものについては左詰めしてソートされている状態で、最後の値が  s である状態にするまでに必要な操作回数

注意点として、bit の中身が  c 枚であるとき、bit で表されないカードについては、 c+1 番目以降に配置され直されることになる。なので、dp[ bit ][ s ] の状態で、bit に含まれないカードがそれぞれ何番目に対応するのかを対応付ける必要がある。

dp[ bit ][ s ] に新たに i 番目のカードを加えるとき、そのカードの現在地が p 番目であるとして、

  • chmin( dp[ bit | (1 << i) ][ t ], dp[ bit ][ s ] + abs(c - p) )

という感じになる。t の部分は、

  • | i - c | が偶数のときは A[ i ]
  • | i - c | が奇数のときは B[ i ]

ということになる。t の値が s よりも小さい場合は遷移しない。ここでは  s は絶対値を用いているけど、実際は  2N 種類の値に用いることができる。その場合、計算量は  O(N^{2}2^{N}) になる。

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; }
template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; }

using pint = pair<int,int>;
const int INF = 1<<29;

int N;
vector<int> A, B;

int solve() {
    vector<vector<int>> dp((1<<N)+1, vector<int>(55, INF));
    dp[0][0] = 0;
    for (int bit = 0; bit < (1<<N); ++bit) {
        int con = 0;
        for (int i = 0; i < N; ++i) if (bit & (1<<i)) ++con;
        
        // bit に含まれない残りの要素が何番目に対応するか
        vector<pint> ords;
        int iter = con;
        for (int i = 0; i < N; ++i)
            if (!(bit & (1<<i)))
                ords.emplace_back(i, iter++);

        for (int s = 0; s <= 50; ++s) {
            if (dp[bit][s] >= INF) continue;
            for (auto p : ords) {
                // p.first: もともとの a の index
                // p.second: dp[bit][s] の状態での index
                int ns = -1;
                if (abs(p.first - con) % 2 == 0) ns = A[p.first];
                else ns = B[p.first];

                if (ns >= s) {
                    chmin(dp[bit|(1<<p.first)][ns], dp[bit][s] + abs(p.second - con));
                }
            }
        }
    }
    int res = INF;
    for (int s = 0; s <= 50; ++s) chmin(res, dp[(1<<N)-1][s]);
    if (res < INF) return res;
    else return -1;
}

int main() {
    cin >> N;
    A.resize(N), B.resize(N);
    for (int i = 0; i < N; ++i) cin >> A[i];
    for (int i = 0; i < N; ++i) cin >> B[i];
    cout << solve() << endl;
}

キーエンス プログラミング コンテスト 2020 C - Subarray Sum (茶色, 400 点)

結構ギャグ要素強めな問題だった

問題へのリンク

問題概要

3 つの整数  N, K, S が与えられる。
長さ  N の整数列  A_{0}, \dots, A_{N-1} であって、以下の条件を満たすものを構築せよ:

  •  1 \le A_{i} \le 10^{9}
  •  A_{l} + \dots, A_{r} = S を満たすような  (l, r) の組がちょうど  K 個ある

制約

  •  1 \le N \le 10^{5}
  •  0 \le K \le N
  •  1 \le S \le 10^{9}

考えたこと

とりあえず一つ思うこととして、 A_{i} は全部 1 以上なので、 A の区間の左端を  l で固定したとき、

  •  A_{l} + \dots, A_{r} = S

を満たすような  r は、もし存在するならば、ただ一通りだけ、ということがわかる。複数の  r に対して  A_{l} + \dots, A_{r} が同じ値になることはありえない。

ということで、そういう  r が存在するような  l がちょうど  K 個になるようにすればいいんだ、ということになる。その最も簡単な場合というのは

  • A[0] = S (r = 0)
  • A[1] = S (r = 1)
  • ...
  • A[K-1] = S (r = K-1)

というような場合。こうしたとき、 l \ge K に対しては区間和が  S にならないようにする必要があるので、残りは  S+1 とかで埋めておくと安心できる。

ただし、 S = 1000000000 のときは  S+1 10^{9} を超えてしまうので、その場合だけ 1 にすれば OK。

まとめると、以下の感じで OK。


  •  S \lt 1000000000 のとき、 S, S, \dots, S, S+1, \dots, S+1 ( S K 個)

  •  S = 1000000000 のとき、 S, S, \dots, S, 1, \dots, 1 ( S K 個)


#include <iostream>
using namespace std;

int main() {
    int N, K, S;
    cin >> N >> K >> S;
    int rem = S+1;
    if (rem > 1000000000) rem = 1;
    for (int i = 0; i < K; ++i) cout << S << " ";
    for (int i = K; i < N; ++i) cout << rem << " ";
    cout << endl;
}