挑战程序竞赛系列(8):2.1一往直前!贪心法(其他)

详细代码可以fork下Github上leetcode项目,不定期更新。

练习题如下:

POJ 2393: Yogurt Factory

思路:
把存储成本转换成价格,做个比较即可。

200 400 300 500
   88  89  97  91
a. 88  93  98  103 表示第一周生产全部酸奶的成本,已考虑存储成本
b.     89  94  99  表示第二周生产后续酸奶的成本,已考虑存储成本
c.         97  102 表示第三周生产后续酸奶的陈本,已考虑存储成本

所以,在这些所有可能的候选项中,每周都选取最小的成本进行生产即可。
贪心策略:
每周选择min(前周成本+S,本周成本)

代码如下:

public class SolutionDay24_P2393 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int N = in.nextInt();
        int S = in.nextInt();
        int[] C = new int[N];
        int[] Y = new int[N];
        for (int i = 0; i < N; i++){
            C[i] = in.nextInt();
            Y[i] = in.nextInt();
        }
        System.out.println(solve(C, Y, S));
        in.close();
    }

    private static long solve(int[] C, int[] Y, int S){
        int minCost = 1 << 30;
        long total = 0;
        for (int i = 0; i < C.length; i++){
            minCost = Math.min(minCost + S, C[i]);
            total += minCost * Y[i];
        }
        return total;
    }
}

注意溢出,否则WA。

POJ 1017: Packets

有 1 * 1 到 6 * 6 的产品,最少用几个 6 * 6 的箱子装它们。

这道题总共就需要考虑六种情况:
1. 产品为 6*6,直接打包,无多余空间
2. 产品为 5*5,直接打包,有多余空间,留给 1 *1产品
3. 产品为 4*4,直接打包,有多余空间,留给 1*1 ,2*2产品
4. 产品为 3*3,特殊
5. 产品为 2*2,特殊
6. 产品为 1*1,特殊

首先一定是从 size比较大的产品考虑,因为它们剩余的空间可以多装一些size较小的物品,如果把size小的物品都包裹了,那么打包size大的物品必然会剩下很多空间,显然浪费了。

代码思路:
先打包size为6,5,4,3 的产品,产品3比较特殊,但总共就出现四种情况,所以可以用space来记录打包3时余下的size为2的个数。

这样,就能计算size为2可装的总个数,接着把所有的size为2的产品放入这些空间中,如果不够放,就开辟新的空间,来存放size为2的产品。

最后,计算产品1的剩余空间,存放,多出来的开辟新空间。

代码如下:

public class SolutionDay34_P1017 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (true){
            int[] h = new int[6];
            boolean isOver = true;
            for (int i = 0; i < 6; i++){
                h[i] = in.nextInt();
                if (h[i] != 0) isOver = false;
            }
            if (isOver) break;
            System.out.println(solve(h));
        }
        in.close();
    }

    static int[] space = {0,5,3,1};
    private static int solve(int[] h){
        int n = h[5] + h[4] + h[3] + (int)Math.ceil(h[2] / 4.0);

        //计算能够放入2*2的个数
        int y = 5 * h[3] + space[h[2] % 4];

        if (h[1] > y){
            n += Math.ceil((h[1] - y) / 9.0); //多出来的2*2块,每9个能拼成 6*6块
        }

        //计算能够放入1*1的个数
        int x = 36 * n - 36 * h[5] - 25 * h[4] - 16 * h[3] - 9 * h[2] - 4 * h[1];

        if (h[0] > x) {
            n += Math.ceil((h[0] - x) / 36.0);
        }

        return n;
    }

}

POJ 3040: Allowance

这道题有些细节还没搞明白,但多少能发掘一点东西。可以简单分为三个阶段:

  • 第一阶段,筛选那些面额比周工资大的那些coin,直接发放掉。
  • 第二阶段,比较复杂。。。

该问题要转换一下,它的意思是说,尽可能多的给老牛发工资,所以我们可以这样理解:

求得所有面额的总工资,如在测试用例中,总工资为 10 * 1 + 1 * 100 + 5 * 120 = 710元,所以理想状态下,这些工资最多能够发 710 / 6 = 118 周。

但为什么是理想状态呢?因为该问题面值是不能被撕开的,所以如果(5+1)元被发完了,只能发(5+5)和(10)元的面额了,那么自然能发的周数减小了。

所以说该问题可以被看成:

尽可能的让面额组合接近C,此时能发的周数一定是最多的。那么问题来了,该怎么组合?

来证明一下:
因为我们知道,面额大于C的,与小的组合只会产生更大的面额,没必要,所以直接发放掉即可。

现在的问题就是针对那些面额均小于C的coins该怎么组合?又得回到经典的性价比问题,刚才说了 710 / 6 是最优的,但由于面额的性质可能导致 100 / 10,而非100 / 6,所以我们的策略一定要尽量避免10的大量出现。

这里,我们直接给出第二阶段的贪心策略,但我并不知道为何能够得到最优解,只能证明它的充分性,必要性实在不知道从何下手。

第二阶段策略:

  • 对硬币面额从大到小尽量凑得接近C,允许等于或不足C,但是不能超出C。
  • 接着按硬币面额从小到大凑满C(凑满的意思是允许超出一个最小面值,ps此处的最小面值指的是硬币剩余量不为0的那些硬币中的最小面值),凑满之后得出了最优解,发掉,继续进入上一步骤循环。

简单证明下:
假设我们从小到大凑足C,那么必然面额小的被用掉,这就导致面额大的在填补C时,有大量的空间无法被小额填满,为了让它超过C,只能用较大额的填,而这就进一步导致面额超过C的部分大大增加,如原本(5+1)元,凑足C=6的情况,损失0,而当1元先被用尽,则导致凑成(5+5)来分发,这就浪费了原先的剩余空间1,而损失了4,显然不划算。

这个问题,也可以类比上一道题,打包问题,我们尽可能的使用少的包裹,就让尽可能多的剩余空间被填补即可,而这策略就是从大到小慢慢塞满即可,此处只是多了一个超出部分,为了让超出部分最小,那一定是从小到大反过来选。

代码如下:

public class SolutionDay24_P3040 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int N = in.nextInt();
        int C = in.nextInt();
        int[] V = new int[N];
        int[] B = new int[N];
        for (int i = 0; i < N; i++){
            V[i] = in.nextInt();
            B[i] = in.nextInt();
        }
        System.out.println(solve(V, B, C));
        in.close();
    }

    private static class Coin{
        int v;
        int b;
        Coin(int v, int b){
            this.v = v;
            this.b = b;
        }

        @Override
        public String toString() {
            return "[" + v + "," + b + "]";
        }
    }

    //优先处理面值大的元素
    private static int solve(int[] V, int[] B, int C){
        int total = 0;
        Coin[] coins = new Coin[V.length];
        for (int i = 0; i < V.length; i++){
            coins[i] = new Coin(V[i],B[i]);
        }
        Arrays.sort(coins, new Comparator<Coin>() {
            @Override
            public int compare(Coin o1, Coin o2) {
                return o2.v - o1.v;
            }
        });

        for (int i = 0; i < V.length; i++){
            if (coins[i].v >= C) {
                total += coins[i].b;
                coins[i].b = 0;
            }
        }

        int[] need = new int[V.length];
        while (true){
            int sum = C;
            need = new int[V.length];
            //从大到小凑
            for (int i = 0; i < V.length; i++){
                if (coins[i].b > 0 && sum > 0){
                    int can_use = Math.min(coins[i].b, sum / coins[i].v);
                    if (can_use > 0){
                        sum -= can_use * coins[i].v;
                        need[i] = can_use;
                    }
                }
            }

            //从小到大凑
            for (int i = V.length - 1; i >= 0; --i){
                if (coins[i].b > 0 && sum > 0){
                    int can_use = Math.min(coins[i].b - need[i], sum / coins[i].v + 1);
                    if (can_use > 0){
                        sum -= can_use * coins[i].v;
                        need[i] += can_use;
                    }
                }
            }

            if (sum > 0){
                //剩余硬币凑不满
                break;
            }

            int min_week = 1 << 30;
            for (int i = 0; i < V.length; i++){
                if (need[i] == 0) continue;
                min_week = Math.min(min_week, coins[i].b / need[i]);
            }

            total += min_week;
            for (int i = 0; i < V.length; i++){
                if (need[i] == 0) continue;
                coins[i].b -= min_week * need[i];
            }
        }

        return total;
    }

}

POJ 1862: Stripies

这道题就很简单了,简单写下公式你就能明白,当存在三个数时,我们有:


m1+m2+m3≥22m1m2m234−−−−−−−−√−−−−−−−−−−−⎷

所以说,为了让根号的值最小,m1和m2应该为最大和次大,而m3应该是最小的,谁叫人家是平方呢?

所以用一个优先队列维持一个最大堆,poll最大和次大,计算一次,把新的值再offer进去,直到最后只剩下一个元素,比较简单。代码如下:

public class SolutionDay24_P1862 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int N = in.nextInt();
        int[] stripe = new int[N];
        for (int i = 0; i < N; i++){
            stripe[i] = in.nextInt();
        }
        System.out.println(solve(stripe));
        in.close();
    }

    private static double solve(int[] stripe){
        PriorityQueue<Double> queue = new PriorityQueue<>(new Comparator<Double>() {
            @Override
            public int compare(Double o1, Double o2) {
                return (o2-o1) > 0 ? 1 : -1;
            }
        });

        for (int i = 0; i < stripe.length; i++){
            queue.offer((double)stripe[i]);
        }

        while (queue.size() != 1){
            double m1 = queue.poll();
            double m2 = queue.poll();
            queue.offer(collide(m1, m2));
        }

        return queue.peek();
    }

    private static double collide(double m1, double m2){
        return 2 * Math.sqrt(m1 * m2);
    }
}

POJ 3262: Protecting the Flowers

一道陷阱题,起初的选牛策略是尽可能的选择d大的,当d相同,选择t长的,对于POJ的测试用例来看无比正确,但其实充满了陷阱。一个简单的选择比较损失如下:

100 9
1   5
乍一看应该先赶9,其实不然,此处应该先赶时间较短的,但不管如何,我们可以在两头牛之间计算损失,怎么算?

就是按照它的法则来:
赶走第一头损失:2*100*5
赶走第二头损失:2*1*9

显然应该赶走第二头,所以两头牛的选择如下,有牛 c1和c2

compare c1.T * c2.D > c1.D * c2.T

到这我就好奇了,为什么针对三头以上,这种选择策略也是正确的呢?

假设我们现在有m头牛,如下o1,o2,...,om,并且已知了第m头牛,和其他m-1头两两比较后,有om.T∗oi.D>oi.T∗om.D,根据上述公式,说明了在m头牛中,第m头牛是最应该被选的,现在我们假设恰巧第一次移除这第m头牛,所以有:


损失1:2∗om.T(o1.D+o2.D+...+om−1.D)

此时,我们再假设如果不移第m头牛,且把它保留到最后,所以有:


损失2:2∗om.D(o1.T+o2.T+...+om−1.T)

因为我们知道,对于任何i,有om.T∗oi.D>oi.T∗om.D,所以损失1大于损失2,嘿,把第m头牛保留到最后,损失2都比损失1小,那在某个中间时刻移除第m头牛,损失2将变得更小。所以,不管如何,都应该先移除第m头牛(损失1最大),得证。

所以代码如下:

public class SolutionDay24_P3262 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int N = in.nextInt();
        int[] T = new int[N];
        int[] D = new int[N];
        for (int i = 0; i < N; i++){
            T[i] = in.nextInt();
            D[i] = in.nextInt();
        }
        System.out.println(solve(T, D));
        in.close();
    }

    private static class Pair{
        int t;
        int d;
        Pair(int t, int d){
            this.t = t;
            this.d = d;
        }
    }

    private static long solve(int[] T, int[] D){
        Pair[] p = new Pair[T.length];
        int damage = 0;
        for (int i = 0; i < T.length; i++){
            damage += D[i];
            p[i] = new Pair(T[i],D[i]);
        }
        Arrays.sort(p, new Comparator<Pair>() {
            @Override
            public int compare(Pair o1, Pair o2) {
                return o2.d * o1.t - o1.d * o2.t;
            }
        });

        long total = 0;
        for (int i = 0; i < T.length; i++){
            damage -= p[i].d;
            total += damage * 2 * p[i].t;
        }
        return total;
    }
}