読者です 読者をやめる 読者になる 読者になる

二分探索の練習

二分探索の練習。
問題は「プログラミングコンテスト」「二分探索」あたりで適当にググったもの。


- UpperBoundとLowerBound
CのSTD関数をC#で実装。

public static int LowerBound(int[] ar, int val)
{
    var lb = -1;
    var ub = ar.Length;

    while (ub - lb > 1)
    {
        var mid = (lb + ub) / 2;
        if (ar[mid] >= val)
        {
            ub = mid;
        }
        else
        {
            lb = mid;
        }
    }

    return ub;
}

public static int UpperBound(int[] ar, int val)
{
    var lb = -1;
    var ub = ar.Length;

    while (ub - lb > 1)
    {
        var mid = (lb + ub) / 2;
        if (ar[mid] <= val)
        {
            lb = mid;
        }
        else
        {
            ub = mid;
        }
    }

    return lb + 1;
}


- Topcoder SRM676 Div1Easy(WaterTank)
https://community.topcoder.com/stat?c=problem_statement&pm=14019

Cリットル入る水がめと、配列tとxが与えられる。水がめには、t[i]の時間、x[i]リットルの水が注ぎこまれる。水がめが溢れないようになるべく小さい穴をあけたい。
この穴の単位時間あたりの水の排出量を求める。

配列の大きさは1~50、tとxは1~1,000,000、Cは1~10^9

LowerBoundと同じように二分探索する。
最初、1sごとにループするコードを書いてタイムアウトになってしまった。水の増え方が線形なので、t[i]単位でまとめて計算して問題ない。

public class WaterTank {
    double EPS = 1e-8;

    int[] t;
    int[] x;
    double C;

    bool NoOverflow(double rate)
    {
        var tank = 0.0;
        for (int n = 0; n < t.Length; n++)
        {
            //これだと間に合わない!
            //線形に増えるので1sごとにループする必要はない
            //var water = (double)x[n];
            //for (int sec = 0; sec < t[n]; sec++)
            //{
            //    tank += water;
            //    tank -= rate;
            //    if (tank > C) return false;
            //    if (tank < 0) tank = 0.0;
            //}

            tank += (double)x[n] * t[n];    //キャストを忘れない!(忘れてもサンプルテストは通る)
            tank -= rate * t[n];
            if (tank > C) return false;
            if (tank < 0) tank = 0.0;
        }

        return true;
    }

    public double minOutputRate(int[] pt, int[] px, int pC)
    {
        t = pt;
        x = px;
        C = (double)pC;

        double lb = 0.0, ub = 1000001.0;

        while (ub - lb > EPS)
        {
            var mid = (lb + ub) / 2;
            if (NoOverflow(mid))
                ub = mid;
            else
                lb = mid;
        }

        return ub;
    }
}


- Topcoder SRM456 Div2Hard(CutSticks)
https://community.topcoder.com/stat?c=problem_statement&pm=10563&rd=13909

いろいろな長さの棒sticks[]がある。そして最大でC回、棒を取り出して2つに割ることができる(割ってできた棒をさらに割ることもできる)。
最終的にできた棒を、増加しない順にならべて、K番目の棒を取り出す。このとき、その取り出した棒の最大長を求める。
なお、最低でもK本の棒ができるように、割っていく必要がある。

sticks[]の長さ:1~50
sticks[]の各要素:1~1000000000
C:1~1000000000
K:1~sticks[]の長さ+C

「ある長さにできるかどうか」を判定しながら二分探索する。
終了条件を上限と下限の差にすると無限ループになってしまうので、十分大きな固定回数のループにする。

public class CutSticks {
    double EPS = 1e-10;

    bool CanMake(double len, double[] sticks, int C, int K)
    {
        var cnt = 0;
        foreach (var stick in sticks)
        {
            if (stick - len > EPS)
            {
                var maxDevideNum = (int)(stick / len) - 1;
                var devideNum = Math.Min(C, maxDevideNum);

                cnt += (devideNum + 1);
                C -= devideNum;

                if (cnt >= K) return true;
            }
        }

        return false;
    }

    public double maxKth(int[] sticks, int C, int K) {
        double lb = 0;
        double ub = 1000000001;

        var dsticks = sticks.Select(a => (double)a).OrderByDescending(b => b).ToArray();

        //while(ub - lb > EPS)  //これだと (new int[] { 1000000000 }, 1000000000, 1) のケースで終了しない
        for (int i = 0; i < 500; i++)
        {
            var mid = (lb + ub) / 2;

            if (CanMake(mid, dsticks, C, K))
                lb = mid;
            else
                ub = mid;
        }

        return lb;
    }
}


- Topcoder SRM636 Div2Hard(ChocolateDividingHard)
https://community.topcoder.com/stat?c=problem_statement&pm=13388&rd=16079

チョコレートの板String[] chocolateがある。
chocolateの各char要素は、そのセルのクオリティを表している('0'~'9')。
縦に4つ、横に4つに割ったとき、1つの塊のクオリティの合計の最小値を最大にしたい。この値を答えよ。

chocolate[]の縦横の要素数は4~75

最大のクオリティを求める二分探索を行う。
その中で縦(または横)全パターンのループを回し、パターンごとにクオリティを満足する最小の横Indexを二分探索で求めていく。
横に4分割できればOK。

二分探索の中で二分探索をしていて、はまるとデバッグが大変になる問題だと思う。

public class ChocolateDividingHard {
    int[,] sums;
    int rowNum;
    int colNum;
    int sum;

    //row1 < row2, col1 < col2
    //(row1 + 1, col1 + 1) to (row2, col2)
    int GetArea(int row1, int row2, int col1, int col2)
    {
        var ret = sums[row2, col2] -
            (col1 >= 0 ? sums[row2, col1] : 0) -
            (row1 >= 0 ? sums[row1, col2] : 0) +
            (col1 >= 0 && row1 >= 0 ? sums[row1, col1] : 0);

        return ret;
    }

    bool CanDevide(int col1, int col2, int size, int[] rows)
    {
        for (int rowIdx = 0; rowIdx < rows.Length; rowIdx++)
        {
            var row1 = rowIdx > 0 ? rows[rowIdx - 1] : -1;
            var row2 = rows[rowIdx];
            if (GetArea(row1, row2, col1, col2) < size) return false;
        }
        return true;
    }

    int GetMinColIdx(int startIdx, int size, int[] rows)
    {
        //binary search (l, u]
        int l = startIdx - 1;   //ここを l = startIdx にすると、startIdxが答えの時に間違った解を返す!
        int u = colNum;

        while (l < u - 1)
        {
            var mid = (l + u) / 2;
            if (CanDevide(startIdx - 1, mid, size, rows))
                u = mid;
            else
                l = mid;
        }

        return (u == colNum) ? -1 : u;
    }

    bool CanDevide(int size, int[] rows)
    {
        var col = -1;
        for (int i = 0; i < 4; i++)
        {
            col = GetMinColIdx(col + 1, size, rows);
            if (col == -1) return false;
        }
        return true;
    }

    bool CanDevide(int size)
    {
        for (int row1 = 0; row1 < rowNum; row1++)
            for (int row2 = row1 + 1; row2 < rowNum; row2++)
                for (int row3 = row2 + 1; row3 < rowNum; row3++)
                    if (CanDevide(size, new int[] { row1, row2, row3, rowNum - 1 }))
                        return true;
        return false;
    }

    int findBest()
    {
        //binary search [l, u)
        int l = 0;
        int u = sum + 1;

        while (u > l + 1)
        {
            var mid = (l + u) / 2;

            if (CanDevide(mid))
                l = mid;
            else
                u = mid;
        }

        return l;
    }

    public int findBest(string[] chocolate) {
        rowNum = chocolate.Length;
        colNum = chocolate[0].Length;

        sums = new int[rowNum, colNum];
        for (int row = 0; row < rowNum; row++)
        {
            for (int col = 0; col < colNum; col++)
            {
                sums[row, col] = 
                    (row > 0 ? sums[row - 1, col] : 0) +
                    (col > 0 ? sums[row, col - 1] : 0) +
                    ((int)(chocolate[row][col] - '0')) -
                    (row > 0 && col > 0 ? sums[row - 1, col - 1] : 0);
                sum += (int)(chocolate[row][col] - '0');
            }
        }

        return findBest();
    }
}