TopCoder SRM 642 Div1

今回もSRM 642の解説行きます。今回からソースコードのヘッダ部分は切ります(一応マクロは残します)。

今回の問題は

  • Easy: メモ化 / 確率
  • Medium: 最小費用流

となっております。


Easy (250点)

問題

ある町にはバスがN本あり、それぞれのバスは駅を出発してtime[i]分で駅に戻ってくる。一度に運行できるバスは1本だけであるため、ひとたびバスが出発するとtime[i]分はバスがでない。出発するバスはそれぞれprob[i] %の確率で選ばれる。今あなたが時刻sに駅に到着したとすると待ち時間の期待値はいくつか?

制約条件

1 <= N <= 100
0 <= s <= 100000

解法

ある時刻tにバスが出発する確率をメモ化で求める。すなわち、ある時刻にバスが出る確率をmemo[t]とするとき、
memo[t+time[i]] = sum {i: memo[t] * (prob[i] / 100.0)}
が成り立つ。

今求めたいのは時刻sから初めてバスが出る時刻なので、上のメモ化を時刻がs以上になったら打ち切る。
するとs以上に現れる確率の合計が1になるようになる。

あとは、これをsと時刻の差を取りながら期待値計算していけばいい。

double memo[200011];

class WaitingForBus {
public:
    double whenWillBusArrive(vector<int> time, vector<int> prob, int s) {
        memset(memo, 0, sizeof(memo));
        memo[0] = 1.0;
        int upper = s;
        for (int i = 0; i < s; i++) {
            for (int j = 0; j < time.size(); j++) {
                memo[i + time[j]] += memo[i] * (prob[j] / 100.0);
                upper = max(upper, i + time[j]);
            }
        }
        double ret = 0.0;
        for (int t = s; t <= upper; t++) {
            ret += memo[t] * (t - s);
        }
        return ret;
    }
};

Meidum (500点)

問題

あなたの庭には木がN本ある。その木はそれぞれheight[i]の高さを持っており、1日にadd[i]だけ伸びる。また、あなたはチェーンソーをM本もっていて、そのチェーンソーで木をdevice[j]の高さにすることができる。ただし、1日に各チェーンソーは1回しか使えず、木も1日に1回しか切れない。適切に木の手入れをしていった時のtime日目の木の高さの合計の最小値を求めよ。

解法

まず、着眼点としてはそれぞれの木はtime日の間でただ1回だけきればよく、それ以上切ってもチェーンソーの無駄遣いにしかなりません。

また、もしt日目に木を切ったとすれば最終日の木の高さはdevice[t] + add[i] * (time – t)です(tは1からtime)。

仮に木を切ることができないのであれば、その高さはheight[i] + add[i] * timeとなるが、木を切ることができないというのを言い換えると、height[i] + add[i] * timeがいずれのdevice[t] + add[i] * (time – t)よりも小さくなるということになります。

ということは、各木について何日目にどのチェーンソーを割り当てるのが最小になるかを求める問題になり、組み合わせ+最小化ということで最小費用流が使えそうです。

問題はどうグラフを持つかですが、まず各木は1回しか切れないという条件から、sourceから各木のノードへcap=1, cost=0のエッジを引きます。

もし、各木を何日目かに切ったとするとそのあとに伸びる高さはadd[i] * jと表せるので、各木ノードと各日ノードの間にadd[i] * jのエッジを引きます。このとき、各木ノードにはそもそも1しかフローが流れないので、木が2日以上の日に切られることはありません。

次に、その日にどのチェーンソーを使うかという部分ですが、各日には各チェーンソーが1回しか使えないので、これは各日ノードから各チェーンソーノードにcap=1, cost=0のエッジを張ればいいです。

最後にチェーンソーは全体で合計time回ずつ使えて、使った時には残りの高さがdevice[i]になるので、各チェーンソーノードからsinkに向かってcap=time, cost=devide[i]のエッジを張ります。

以上で、大体OKなのですが、木を切れない場合があるので、それを別にノードとして用意します。このノードをUとすると各木ノード
からUに向かってcap=1, cost=height[i] + add[i] * timeのエッジを張り、さらにUからTへcap=N, cost=0のエッジをはればOKです。

後は木を切る本数N本をフローとして流して、最小費用流を求めればいいです。ちなみにBellman-Fordを使った実装で問題なく通りました。


struct MincostGraph {
    static const long long infty = 1ll << 60;
    struct Edge { 
        int to, cap;
        Ty cost;
        int rev;
    };
    vector<vector<Edge> > graph;
    vector<Ty> dist;
    vector<int> prevv;
    vector<int> preve;
    MincostGraph(int n)
        : graph(n)
        , dist(n)
        , prevv(n)
        , preve(n)
    {
    }
    void addEdge(int from, int to, int cap, Ty cost) {
        Edge e1 = { to, cap, cost, (int)graph[to].size() };
        Edge e2 = { from, 0, -cost, (int)graph[from].size() };
        graph[from].push_back(e1);
        graph[to].push_back(e2);
    }
    Ty mincost(int s, int t, int f) {
        Ty ret = 0;
        while (f > 0) {
            bellmanFord(s);
            if (dist[t] == infty) return infty;

            int cap = f;
            for (int v = t; v != s; v = prevv[v]) {
                Edge& e = graph[prevv[v]][preve[v]];
                cap = min(cap, e.cap);
            }

            f -= cap;
            ret += cap * dist[t];
            for (int v = t; v != s; v = prevv[v]) {
                Edge& e = graph[prevv[v]][preve[v]];
                e.cap -= cap;
                graph[v][e.rev].cap += cap;
            }
        }
        return ret;
    }
    void bellmanFord(int s) {
        fill(dist.begin(), dist.end(), (Ty)infty);
        dist[s] = 0;
        bool update = true;
        while (update) {
            update = false;
            for (int i = 0; i < graph.size(); i++) {
                if (dist[i] == infty) continue;
                for (int j = 0; j < graph[i].size(); j++) {
                    Edge& e = graph[i][j];
                    if (e.cap > 0 && dist[e.to] > dist[i] + e.cost) {
                        dist[e.to] = dist[i] + e.cost;
                        prevv[e.to] = i;
                        preve[e.to] = j;
                        update = true;
                    }
                }
            }
        }
    }
};

class TaroCutting {
public:
    int getNumber(vector<int> height, vector<int> add, vector<int> device, int time) {
        int n = height.size();
        int m = device.size();
        int U = n + m + time;
        int S = U + 1;
        int T = S + 1;
        MincostGraph<lint> graph(T+1);
        for (int i = 0; i < n; i++) {
            graph.addEdge(S, i, 1, 0);
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < time; j++) {
                graph.addEdge(i, n + j, 1, add[i] * j);
            }
        }
        for (int i = 0; i < time; i++) {
            for (int j = 0; j < m; j++) {
                graph.addEdge(n + i, n + time + j, 1, 0);
            }
        }
        for (int i = 0; i < m; i++) {
            graph.addEdge(n + time + i, T, time, device[i]);
        }
        for (int i = 0; i < n; i++) {
            graph.addEdge(i, U, 1, height[i] + add[i] * time);
        }
        graph.addEdge(U, T, n, 0);
        return (int)graph.mincost(S, T, n);
    }
};

まとめ

今回はEasyは結構簡単だった気がします。途中確率がパーセントで与えられているのに気付かなくて、少し時間かかりましたがw

Mediumはフローっぽいなぁとは思いましたが、グラフの作り方が思いつかず時間切れという感じです。

というわけで、今回の解説を終わります。最後までお読みいただきありがとうございました。