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

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

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;
}

AOJ 3177 Painting (HUPC 2020 day3-F)

こういうの楽しいよね!

問題へのリンク

問題概要

 N 個のマスが横一列に並んでいます。各マスは赤、青、黄、緑のいずれか 1 色で塗られている ("RBYG" からなる長さ  N の文字列  S で与えられる)。

以下の操作を好きな順序で好きな回数だけ行えます。文字列  T を作ることが可能かどうかを判定せよ。

  • 異なる色で塗られた隣接 2 マスを選ぶ
  • その 2 マスをどちらにも用いられていない 2 色で 1 つずつ塗る

制約

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

考えたこと

この手の問題は不変量を導きたくなるところ。とりあえず各色を 0, 1, 2, 3 に対応させて考えることにしよう。一瞬、mod とかを考えたくなるけど、それだとあまりうまくいかない。たとえば mod 4 で不変なのかと思いたくなるけど、

  • (0, 1) と (2, 3)
  • (0, 2) と (1, 3)
  • (0, 3) と (1, 2)

のうち、真ん中のやつは mod 4 だと上手くいかない。

XOR

そこで、

  • 0 ^ 1 ^ 2 ^ 3 = 0

であることに着目しよう。そうすると、

  • 0 ^ 1 = 2 ^ 3
  • 0 ^ 2 = 1 ^ 3
  • 0 ^ 3 = 1 ^ 2

であることがいえる!!!!!つまり、全体の XOR 和は不変なのだ。S と T とで XOR 和が等しいことが必要条件となる。しかしこれだけでは不十分で、

  • S != T
  • S がすべて同色
  • T がすべて同色

という場合はどうしようもない ( N = 1 はケースにない)。逆にそうでなければできる。一応そのことを証明してみる。

数学的帰納法

この手の証明は数学的帰納法というイメージがある。まず、 N = 2 のとき、XOR が等しい全パターン作れることはいえる。とくに、

(0, 1) → (2, 3) → (1, 0)

というように、媒介することで swap ができることに注意しておく。 N = 3 の場合も大丈夫そう。

それでは、 N \ge 4 S, T がともに全部同色ではないとき、互いに移れることを数学的帰納法によって示そう。まず前提として今回の操作は逆操作が可能なので、あらかじめ  S T に都合よく操作してしまってかまわない。全部同色でない文字列は、どの隣接する 2 文字も異なるようにできることに注意します。よって次のようにして  S, T を一致させることができます。

  • S や T をあらかじめ、どの隣接する 2 文字も異なるような状態にしておきます
  • S の先頭の 2 文字に対して操作を行うことで、S[0] == T[0] となるようにします
  • このとき、S, T の後半 N-1 文字中は「全部同色」という状態ではないので、帰納法の仮定から、互いに一致させることができます

まとめ

  • S == T なら Yes
  • S または T が「全部同色」なら No
  • S の XOR 和 != T の XOR 和なら No
  • それ以外は Yes

計算量は  O(N)

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

int conv(char c) {
    if (c == 'R') return 0;
    else if (c == 'B') return 1;
    else if (c == 'Y') return 2;
    else return 3;
}

bool solve(string S, string T) {
    if (S == T) return true;
    int sx = 0, sy = 0;
    set<int> ss, st;
    for (auto c : S) ss.insert(c), sx ^= conv(c);
    for (auto c : T) st.insert(c), sy ^= conv(c);
    if (ss.size() == 1 || st.size() == 1) return false;
    return sx == sy;
}

int main() {
    int N;
    string S, T;
    cin >> N >> S >> T;
    if (solve(S, T)) cout << "Yes" << endl;
    else cout << "No" << endl;
}

AOJ 3173 Hokkaido_University2 (HUPC 2020 day3-B)

きちんと細部まで詰めるのが大変かもしれない

問題へのリンク

問題概要

時刻  t に座標  (x, y) にいるとき、以下のいずれかの行動をすることができる。

  •  x が偶数または  t が偶数のとき、時刻  t+1 (x+1, y) に移動する
  •  x が奇数または  t が偶数のとき、時刻  t+1 (x-1, y) に移動する
  •  y が偶数または  t が奇数のとき、時刻  t+1 (x, y+1) に移動する
  •  y が奇数または  t が奇数のとき、時刻  t+1 (x, y-1) に移動する

時刻 0 に座標  (s_{x}, s_{y}) にいて、座標  (g_{x}, g_{y}) を目指したい。最短でたどりつく時刻はいつになるか?

制約

  •  0 \le s_{x}, s_{y}, g_{x}, g_{y} \le 10^{18}

考えたこと

 X = |s_{x} - g_{x}| Y = |s_{y} - g_{y}| とする。色々手を動かしていると、割と多くの場合で  X + Y で到達できることがわかる。どういうときにそれができるのかを色々考えてみる。

まず一つ言えるのは、斜め 45 度方向への移動であれば、任意の時刻・座標からでも、適切にジグザグに進むことができる。

もう一つ言えるのは、

  •  x が偶数かつ  t が奇数」という状態を作れば、x 軸正方向に無限に進める
  •  x が奇数かつ  t が奇数」という状態を作れば、x 軸負方向に無限に進める
  •  y が偶数かつ  t が偶数」という状態を作れば、y 軸正方向に無限に進める
  •  y が奇数かつ  t が偶数」という状態を作れば、y 軸負方向に無限に進める

以上を踏まえると、まず  s_{x} \neq g_{x} かつ  s_{y} \neq g_{y} である場合は、適切な位置合わせを行うことで、進みたい方向に無限に進める状態になる。そしてそれによって「斜め 45 度移動で到達できる状態」を作ってあげれば、到達できる。つまり、 s_{x} \neq g_{x} かつ  s_{y} \neq g_{y} である場合には、 X + Y で到達できる。

残りは丁寧に場合分けする。

sx = gx のとき

  •  s_{y} が偶数のとき、初期状態から y 軸正方向には無限に進めるが、y 軸負方向に進むなら 1 秒待つ必要がある。
  •  s_{y} が奇数のとき、初期状態から y 軸負方向には無限に進めるが、y 軸正方向に進むなら 1 秒待つ必要がある。

sy = gy のとき

  •  s_{x} が偶数のとき、初期状態から x 軸負方向には無限に進める。 s_{x} + 1 にも 1 秒で行ける。それ以外はどこかで 1 秒待つ必要がある。
  •  s_{x} が奇数のとき、初期状態から x 軸正方向には無限に進める。 s_{x} - 1 にも 1 秒で行ける。それ以外はどこかで 1 秒待つ必要がある。
#include <iostream>
using namespace std;

long long solve(long long sx, long long sy, long long gx, long long gy) {
    long long base = abs(sx - gx) + abs(sy - gy);
    if (sx == gx && sy == gy) return 0;
    else if (sx != gx && sy != gy) return base;
    else if (sx == gx) {
        if (sy % 2 == 0) {
            if (sy < gy) return base;
            else return base + 1;
        }
        else {
            if (sy > gy) return base;
            else return base + 1;
        }
    }
    else {
        if (sx % 2 == 0) {
            if (sx > gx || sx == gx - 1) return base;
            else return base + 1;
        }
        else {
            if (sx < gx || sx == gx + 1) return base;
            else return base + 1;
        }
    }
}

int main() {
    long long sx, sy, gx, gy;
    cin >> sx >> sy >> gx >> gy;
    cout << solve(sx, sy, gx, gy) << endl;
}