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

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

AOJ 3172 ?UPC (HUPC 2020 day3-A)

これはそんなにコーナーケースとかはなさそう

問題へのリンク

問題概要

 N 個の文字列  S_{1}, \dots, S_{N} が与えられます。各文字列に対して、

  • 英大文字の部分だけを取り出して、この順に連結した文字列

を作り出し、それを  N 個連結します。そうして得られた文字列の末尾に "UPC" を連結してできる文字列を出力してください。

制約

  •  1 \le N \le 100
  •  1 \le |S_{i}| \le 100

考えたこと

これはさすがにやるだけっぽい

C++

#include <iostream>
#include <string>
using namespace std;

int main() {
    int N;
    cin >> N;
    string res = "";
    for (int i = 0; i < N; ++i) {
        string S;
        cin >> S;
        for (auto c : S) {
            if (c >= 'A' && c <= 'Z') res += c;
        }
    }
    res += "UPC";
    cout << res << endl;
}

Python3

def conv(S):
    return ''.join([c if 'A' <= c <= 'Z' else '' for c in S])
N = int(input())
print(''.join([conv(input()) for _ in range(N)]) + 'UPC')

AOJ 3174 No Palindromes (HUPC 2020 day3-C)

これ楽しい!

問題へのリンク

問題概要

長さ  N の英小文字のみからなる文字列  S が与えられます。 S の文字を入れ替えることによって、次の条件を満たす文字列  T を作ることができるかどうかを判定してください (可能ならば具体例を 1 つ挙げてください)。

  •  T のどの長さ 2 以上の連続する部分文字列も回文ではない。

制約

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

必要条件を列挙しながら、条件の言い換え

まず、「長さ 2 以上の連続する部分文字列も回文でない」という条件をわかりやすく言い換えていくことにしよう!!!

そのためには「必要条件を列挙して十分性が成り立つのを待つ」という方針が有効になることが多いイメージ。とりあえず直ちに思い浮かぶのは

  • 同じ文字が連続してはダメ (その 2 文字が回文となるため)
  • 同じ文字が 1 文字分間隔を空けて配置されるのもダメ (その 3 文字が回文となるため)

よって、「どの連続する 3 文字も互いに相異なる」が必要条件として挙げられる。そして逆にこれを満たせば、どの連続する 3 文字も回文ではなくなるので OK。

Greedy へ

以上を踏まえて判定方法を考える。

  • 使用文字が 1 種類のみの場合は、 N = 1 に限り OK
  • 使用文字が 2 種類のみの場合は、 N = 2 に限り OK

使用文字が 3 種類以上あるときは、直感的には Greedy でよさそう。つまり、

  • 1 個前の文字と 2 個前の文字以外の文字のうち
  • 残り最多の文字を採用していく (そのようなものが存在しなくなったら "-1")

念のための証明としては、次のような感じでできる。

  •  N が 3 の倍数のとき、
    • 頻度が  N/3 より大きいものがあったらダメ
    • それ以外は上記の Greedy で実際に作れる
  •  N が 3 で割って 1 あまるとき
    • 頻度が  N/3 + 1 より大きいものがあったらダメ
    • 頻度が  N/3 + 1 であるものが 2 個以上あったらダメ
    • それ以外は上記の Greedy で実際に作れる
  •  N が 3 で割って 2 あまるとき
    • 頻度が  N/3 + 1 より大きいものがあったらダメ
    • それ以外は上記の Greedy で実際に作れる
#include <bits/stdc++.h>
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; }

string solve(int N, const string &S) {
    map<char,int> ma;
    for (auto c : S) ma[c]++;
    if (ma.size() == 1) {
        if (N == 1) return S;
        else return "-1";
    }
    else if (ma.size() == 2) {
        if (N == 2) return S;
        else return "-1";
    }
    else {
        string res = "";
        while (!ma.empty()) {
            int vmax = -1;
            char pmax;
            for (auto it : ma) {
                if (res.size() > 0 && res.back() == it.first) continue;
                if (res.size() > 1 && res[res.size() - 2] == it.first) continue;
                if (chmax(vmax, it.second)) pmax = it.first;
            }
            if (vmax == -1) return "-1";
            res += pmax;
            ma[pmax]--;
            if (ma[pmax] == 0) ma.erase(pmax);
        }
        return res;
    }
}

int main() {
    int N;
    string S;
    cin >> N >> S;
    cout << solve(N, S) << endl;
}

AOJ 3180 GCDMST (HUPC 2020 day3-I)

中盤枠という感じで作られた

問題へのリンク

問題概要

 1, \dots, N の番号を振られた  N 個の頂点があります。 最初、これらを繋ぐ辺はありません。 あなたはいくつかの辺を追加してこのグラフを連結にしたいと思いました。

頂点  i j を繋ぐ辺を追加するには  A_{{\rm gcd}(i, j)} のコストがかかります。 このグラフを連結にするように辺を追加するとき、かかるコストの和の最小値を求めてください。

制約

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

考えたこと

普通に kruskal 法でできそう!

  • A が小さい順に処理していく
  • w = A[v] として、辺 (v, 2v), (v, 3v), ... それぞれについて連結でないならば重さ w で繋ぐ

という風にすれば OK。このとき、考える辺の本数は「調和級数の和」の形になるので、 O(N \log N) となる。よって全体の計算量は  O(N \alpha(N) \log N)

コード

#include <bits/stdc++.h>
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; }

struct UnionFind {
    vector<int> par;
    
    UnionFind(int n) : par(n, -1) { }
    void init(int n) { par.assign(n, -1); }
    
    int root(int x) {
        if (par[x] < 0) return x;
        else return par[x] = root(par[x]);
    }
    
    bool issame(int x, int y) {
        return root(x) == root(y);
    }
    
    bool merge(int x, int y) {
        x = root(x); y = root(y);
        if (x == y) return false;
        if (par[x] > par[y]) swap(x, y); // merge technique
        par[x] += par[y];
        par[y] = x;
        return true;
    }
    
    int size(int x) {
        return -par[root(x)];
    }
};

using pll = pair<long long, int>;
int main() {
    int N;
    cin >> N;
    vector<pll> A(N);
    for (int i = 0; i < N; ++i) cin >> A[i].first, A[i].second = i + 1;
    sort(A.begin(), A.end());
    UnionFind uf(N);
    long long res = 0;
    for (int i = 0; i < N; ++i) {
        long long w = A[i].first;
        int u = A[i].second;
        for (int v = u * 2; v <= N; v += u) {
            if (uf.issame(u-1, v-1)) continue;
            res += w;
            uf.merge(u-1, v-1);
        }
    }
    cout << res << endl;
}

AOJ 3183 Flipping a Path (HUPC 2020 day3-L)

こういう系すごく楽しいよね

問題へのリンク

問題概要

頂点数  N、辺数  M の重み付き有向単純グラフ  G が与えられる。

このグラフ上で次の条件を満たすパス (同じ頂点を二度通らないものに限る) が存在するかどうかを判定し、そのようなパスの重みの最小値を求めよ。ただし、長さ 0 のパスも重み 0 のパスとみなすことにする。

  • パス上の辺の向きをすべて flip したときに、グラフ全体が強連結となる

制約

  •  2 \le N \le 10^{5}
  •  1 \le M \le \min(10^{5}, N(N-1))

考えたこと

強連結性を考える問題なので、まずは強連結成分分解してみる。そして例によって、まずは必要条件を挙げて行ってみる (ダメなケースを列挙していく)。ここで、強連結成分分解したときの各強連結成分を縮約して得られるグラフを  G' とする。

  •  G' の source が 2 個以上あるときはダメ
  •  G' の sink が 2 個以上あるときはダメ

ということがまずは考えられる。この先がかなり間違いやすくて怖いところ (僕も間違えた)。一応の正解は、以下の判定方法。

  • 元のグラフ  G 上で、source にある頂点群から、sink にある頂点群への最大フローの大きさが 2 以上であることが必要
  • 逆にこのとき、source にある頂点群から sink にある頂点群への最短パスを P とするとき、P を反転すれば強連結となる

なお、この判定方法の代わりに「元のグラフ  G 上で source にある頂点群から sink にある頂点群への最短パスを求めて、それを flip して実際に強連結となれば可能」という風に判定しても OK。

フローが 2 以上であることが必要であることの証明

source の頂点群から sink の頂点群へのフローが 1 のときは、カット S, T を考えたときに、S から T へと入る辺は 1 本しかないので、そこを flip してしまうと明らかに強連結とはならない。よってフローが 2 以上であることが必要と言える。

P を反転すれば強連結となることの証明

フローが 2 以上であるならば、P に対する増加パス Q が存在する。ここで P を反転したとする (P' とする)。なおこのとき P の最短性から、P の flip 操作によって、source や sink が強連結成分であることが崩れないことに注意する。

この P を反転したグラフ上で、任意の頂点 u から任意の頂点 v へと到達できることを示そう。まず、u から source にも sink にも到達できることを示す。元のグラフでは u から sink へ到達できるパス R が存在するはずである。そのパス R 上に P' の辺がなければ sink へ到達することができて、P' をたどって source にも到達できる。パス R 上に P' 上の頂点があるならば、そこから P' に沿って source に到達できるし、そこからパス Q に沿って sink に到達できる。次に、source や sink から任意の頂点 v へ到達できることを示す。元のグラフで source から頂点 v へ到達するパス S を考える。パス S 上にパス P' 上の頂点があるとき、そのうち最も v に近い頂点には sink から P' に沿って到達できて、そこから S に沿って v へと到達できる。以上から、source 群から sink 群へのフローが 2 以上であるとき、source にある頂点群から sink にある頂点群への最短パス P' を flip したときに強連結になることが示された。

嘘判定:G' の source から sink へのフローが 2 以上

下図のような反例がある。縮約後の G' ではフローが 2 以上だが、条件を満たすパスは存在しない。

まとめ

  • 強連結成分分解する
  • 強連結成分がただ 1 つならば、答えは 0
  • そうでないとき、source 成分が複数個、sink 成分が複数個のときはダメ
  • 元のグラフ上で source の頂点群から sink の頂点群への最大フロー値が 2 以上でなければダメ

以上の条件を満たすとき、source の頂点群から sink の頂点群への最短パスを求めれば、それが答えとなる。計算量は O(M + N) (フローアルゴリズムで 2 本とれた時点で打ち切って OK)。

コード

#include <bits/stdc++.h>
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; }

struct SCC {
    using Edge = int;
    using SGraph = vector<vector<Edge>>;

    // input
    SGraph G, rG;

    // result
    vector<vector<int>> scc;
    vector<int> cmp;
    SGraph dag;

    // constructor
    SCC(int N) : G(N), rG(N) {}

    // add edge
    void addedge(int u, int v) {
        G[u].push_back(v);
        rG[v].push_back(u);
    }

    // decomp
    vector<bool> seen;
    vector<int> vs, rvs;
    void dfs(int v) {
        seen[v] = true;
        for (auto e : G[v]) if (!seen[e]) dfs(e);
        vs.push_back(v);
    }
    void rdfs(int v, int k) {
        seen[v] = true;
        cmp[v] = k;
        for (auto e : rG[v]) if (!seen[e]) rdfs(e, k);
        rvs.push_back(v);
    }

    // reconstruct
    set<pair<int,int>> newEdges;
    void reconstruct() {
        int N = (int)G.size();
        int dV = (int)scc.size();
        dag.assign(dV, vector<Edge>());
        newEdges.clear();
        for (int i = 0; i < N; ++i) {
            int u = cmp[i];
            for (auto e : G[i]) {
                int v = cmp[e];
                if (u == v) continue;
                if (!newEdges.count({u, v})) {
                    dag[u].push_back(v);
                    newEdges.insert({u, v});
                }
            }
        }
    }

    // main
    void solve() {
        // first dfs
        int N = (int)G.size();
        seen.assign(N, false);
        vs.clear();
        for (int v = 0; v < N; ++v) if (!seen[v]) dfs(v);

        // back dfs
        int k = 0;
        scc.clear();
        cmp.assign(N, -1);
        seen.assign(N, false);
        for (int i = N - 1; i >= 0; --i) {
            if (!seen[vs[i]]) {
                rvs.clear();
                rdfs(vs[i], k++);
                scc.push_back(rvs);
            }
        }

        // reconstruct
        reconstruct();
    }
};

// edge class (for network-flow)
template<class FLOWTYPE> struct Edge {
    int rev, from, to;
    FLOWTYPE cap, icap;
    Edge(int r, int f, int t, FLOWTYPE c) : rev(r), from(f), to(t), cap(c), icap(c) {}
    friend ostream& operator << (ostream& s, const Edge& E) {
        if (E.cap > 0) return s << E.from << "->" << E.to << '(' << E.cap << ')';
        else return s;
    }
};

// graph class (for network-flow)
template<class FLOWTYPE> struct Graph {
    vector<vector<Edge<FLOWTYPE> > > list;
    
    Graph(int n = 0) : list(n) { }
    void init(int n = 0) { list.clear(); list.resize(n); }
    void reset() { for (int i = 0; i < (int)list.size(); ++i) for (int j = 0; j < list[i].size(); ++j) list[i][j].cap = list[i][j].icap; }
    inline vector<Edge<FLOWTYPE> >& operator [] (int i) { return list[i]; }
    inline const size_t size() const { return list.size(); }
    
    inline Edge<FLOWTYPE> &redge(Edge<FLOWTYPE> e) {
        if (e.from != e.to) return list[e.to][e.rev];
        else return list[e.to][e.rev + 1];
    }
    
    void addedge(int from, int to, FLOWTYPE cap) {
        list[from].push_back(Edge<FLOWTYPE>((int)list[to].size(), from, to, cap));
        list[to].push_back(Edge<FLOWTYPE>((int)list[from].size() - 1, to, from, 0));
    }
    
    void add_undirected_edge(int from, int to, FLOWTYPE cap) {
        list[from].push_back(Edge<FLOWTYPE>((int)list[to].size(), from, to, cap));
        list[to].push_back(Edge<FLOWTYPE>((int)list[from].size() - 1, to, from, cap));
    }

    /* 
    // debug
    friend ostream& operator << (ostream& s, const Graph& G) {
        s << endl; for (int i = 0; i < G.size(); ++i) { s << i << " : " << G.list[i] << endl; }return s;
    }
    */
};

template<class FLOWTYPE> struct Dinic {
    const FLOWTYPE INF = 1<<30; // to be set
    vector<int> level, iter;

    Dinic() { }
    void dibfs(Graph<FLOWTYPE> &G, int s) {
        level.assign((int)G.size(), -1);
        level[s] = 0;
        queue<int> que;
        que.push(s);
        while (!que.empty()) {
            int v = que.front();
            que.pop();
            for (int i = 0; i < G[v].size(); ++i) {
                Edge<FLOWTYPE> &e = G[v][i];
                if (level[e.to] < 0 && e.cap > 0) {
                    level[e.to] = level[v] + 1;
                    que.push(e.to);
                }
            }
        }
    }
    
    FLOWTYPE didfs(Graph<FLOWTYPE> &G, int v, int t, FLOWTYPE f) {
        if (v == t) return f;
        for (int &i = iter[v]; i < G[v].size(); ++i) {
            Edge<FLOWTYPE> &e = G[v][i], &re = G.redge(e);
            if (level[v] < level[e.to] && e.cap > 0) {
                FLOWTYPE d = didfs(G, e.to, t, min(f, e.cap));
                if (d > 0) {
                    e.cap -= d;
                    re.cap += d;
                    return d;
                }
            }
        }
        return 0;
    }
    
    FLOWTYPE solve(Graph<FLOWTYPE> &G, int s, int t) {
        level.assign((int)G.size(), -1); iter.assign((int)G.size(), 0);
        FLOWTYPE res = 0;
        while (true) {
            dibfs(G, s);
            if (level[t] < 0) return res;
            for (int i = 0; i < (int)iter.size(); ++i) iter[i] = 0;
            FLOWTYPE flow = 0;
            while ((flow = didfs(G, s, t, INF)) > 0) {
                res += flow;
            }
        }
    }
};

const int INF = 1<<29;
int solve() {
    int N, M;
    cin >> N >> M;
    SCC scc(N);
    Graph<int> fG(N+2);
    int s = N, t = N+1;
    for (int i = 0; i < M; ++i) {
        int u, v;
        cin >> u >> v;
        --u, --v;
        scc.addedge(u, v);
        fG.addedge(u, v, 1);
    }
    scc.solve();
    int siz = scc.scc.size();
    if (siz == 1) return 0;
    set<int> source, sink;
    for (int k = 0; k < siz; ++k) source.insert(k), sink.insert(k);
    for (int v = 0; v < siz; ++v) {
        for (auto e : scc.dag[v]) {
            source.erase(e);
            sink.erase(v);
        }
    }
    if (source.size() > 1) return -1;
    if (sink.size() > 1) return -1;
    vector<int> S = scc.scc[*source.begin()], T = scc.scc[*sink.begin()];
    for (auto v : S) fG.addedge(s, v, INF);
    for (auto v : T) fG.addedge(v, t, INF);
    Dinic<int> din;
    int maxflow = din.solve(fG, s, t);
    if (maxflow < 2) return -1;

    auto G = scc.G;
    vector<int> dist(N, -1);
    queue<int> que;
    for (auto v : S) dist[v] = 0, que.push(v);
    while (!que.empty()) {
        int v = que.front();
        que.pop();
        for (auto e : G[v]) {
            if (dist[e] == -1) {
                dist[e] = dist[v] + 1;
                que.push(e);
            }
        }
    }
    int res = INF;
    for (auto v : T) chmin(res, dist[v]);
    return res;
}

int main() {
    cout << solve() << endl;
}

AOJ 3178 Katsusando (HUPC 2020 day3-G)

いわゆる区間分割する系の DP。こういう系は高速化を要求するタイプの問題が多いけど、今回は素直な二乗 DP で OK!

問題へのリンク

問題概要

直線上にカツが  N 個ある。 i 個目のカツは位置  X_i にあり、その重さは  W_i である。これらのカツを、次のようにしてすべて回収する。

  • 直線上に 2 点 A, B を配置する (コスト 1)。配置した点上にカツがあるならば、カツを回収する
  • 2 点を動かしていく。距離 1 動かすのに要するコストは、1 + (回収したカツの重さの総和) となる
  • 2 点が一致したら、コスト  K を支払って 2 点を回収する

すべてのカツを回収するのに要するコストの最小値を求めよ。

制約

  •  1 \le N \le 2000

考えたこと

一回の操作で回収するコストは連続する区間になる。よって、区間ごとに分割するタイプの DP で解けそう。つまり、

  •  {\rm dp} \lbrack i \rbrack := 区間  \lbrack 0 : i) のカツをすべて回収する最小コスト

として、

  •  {\rm dp} \lbrack i \rbrack =  \min_{0 \le j \lt i} ({\rm dp} \lbrack j \rbrack + f(j, i-1))

という感じで行ける。ここで  f(l, r) は、A をカツ  l に、B をカツ  r に配置したときに、カツ  l, l+1, \dots, r をすべて回収するための最小コストとする。

f(l, r) を求める

よって、区間  \lbrack l, r) のカツをすべて回収するのに必要なコストを計算する問題へと帰着された。

2 点 A, B をどこで合流させるかを考えることになる。基本的には、カツがあるところに合流させればよい。よって、 k = l, l+1, \dots, r-1 のいすれかのカツのところで 2 点 A, B を合流させる。つまり、

  • カツ  x から右へと移動してカツ  y まで移動するのにかかるコストを  g(x, y)
  • カツ  y から左へと移動してカツ  x まで移動するのにかかるコストを  h(x, y)

と表すと、 f(x, y) = \min_{k = x, x+1, \dots, y}(g(x, k) + h(k, y)) となる。そして少し考えると、

  •  x を固定したとき、 f(x, y) を最小にするような  k y に対して単調増加

ということがいえる。よって、しゃくとり法を用いることで、 f(l, r) テーブル全体を求めるのに要する計算量は  O(N^{2}) になる。

#include <bits/stdc++.h>
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; }

int N, K;
vector<long long> X, W;
long long solve() {
    vector<vector<long long>> cost(N+1, vector<long long>(N+1, 0));
    vector<long long> sumW(N+1, 0), sum(N+1, 0);
    for (int i = 0; i < N; ++i) {
        sumW[i+1] = sumW[i] + W[i];
        sum[i+1] = sum[i] + sumW[i+1] * (X[i+1] - X[i]);
    }

    auto g = [&](int l, int r) {
        return sum[r] - sum[l] - sumW[l] * (X[r] - X[l]);
    };
    auto h = [&](int l, int r) {
        return (sumW[r+1] - sumW[l]) * (X[r] - X[l]) - g(l, r);
    };
    auto f = [&](int l, int r, int k) {
        return g(l, k) + h(k, r);
    };

    for (int l = 0; l < N; ++l) {
        int k = l;
        for (int r = l; r < N; ++r) {
            while (k < r && f(l, r, k) > f(l, r, k + 1)) ++k;
            cost[l][r] = f(l, r, k) + (X[r] - X[l]) + K + 1;
       }
    }

    vector<long long> dp(N+1, 1LL<<60);
    dp[0] = 0;
    for (int i = 1; i <= N; ++i) {
        for (int j = 0; j < i; ++j) {
            chmin(dp[i], dp[j] + cost[j][i-1]);
        }
    }
    return dp[N];
}

int main() {
    cin >> N >> K;
    X.assign(N+1, 0), W.assign(N+1, 0);
    for (int i = 0; i < N; ++i) cin >> X[i] >> W[i];
    cout << solve() << endl;
}