打怪獸問題

2022-10-11 06:01:28

打怪獸問題

作者:Grey

原文地址:

部落格園:打怪獸問題

CSDN: 打怪獸問題

題目描述

題目連結: 牛客-打怪獸

開始時你的能力是0,你的目標是從0號怪獸開始,通過所有的怪獸。
如果你當前的能力,小於i號怪獸的能力,你必須付出money[i]的錢,賄賂這個怪獸,然後怪獸就會加入你,他的能力直接累加到你的能力上;
如果你當前的能力,大於等於 i 號怪獸的能力,你可以選擇直接通過,你的能力並不會下降,你也可以選擇賄賂這個怪獸,然後怪獸就會加入你,他的能力直接累加到你的能力上。
返回通過所有的怪獸,需要花的最小錢數。

主要思路

本題要根據資料量不同使用不同的動態規劃解法

情況1:如果怪獸數量不大和血量都不大的情況,定義如下遞迴函數

long p(int[] hp, int[] money, int i, int j)

遞迴含義是:當前血量是 j,從第 i 號怪獸開始,到最後通過所有怪獸的最少錢數是多少?

程式碼和註釋說明如下

    public static long p(int[] hp, int[] money, int i, int j) {
        // i 已經到最後了,不需要繼續花錢打怪獸
        if (i == hp.length) {
            return 0;
        }
        // i號怪獸選擇賄賂
        long p = money[i] + p(hp, money, i + 1, j + hp[i]);
        // 不選賄賂i號怪獸,這時候是有條件的,就是當前血量 j 需要大於當前怪獸值
        if (j >= hp[i]) {
            return Math.min(p, p(hp, money, i + 1, j));
        }
        return p;
    }

通過上述遞迴函數,可以轉換成動態規劃,利用一個二維陣列即可,程式碼如下

    public static long func1Dp(int[] hp, int[] money) {
        int sum = 0;
        for (int a : hp) {
            sum += a;
        }
        long[][] dp = new long[hp.length + 1][sum + 1];
        for (int i = hp.length - 1; i >= 0; i--) {
            for (int j = sum - hp[i]; j >= 0; j--) {
                dp[i][j] = money[i] + dp[i + 1][j + hp[i]];
                if (j >= hp[i]) {
                    dp[i][j] = Math.min(dp[i][j], dp[i + 1][j]);
                }
            }
        }
        return dp[0][0];
    }

這個二維陣列的規模就是上述遞迴函數 i 和 j 的變化範圍相乘,其中 i 的變化範圍是[0……怪獸數量之和],j 的變化範圍是[0……血量和]。所以適用於怪獸數量和血量都不大的場景。

情況2:如果怪獸能力比較大,但是錢數不大情況,我們需要調整遞迴函數,定義如下遞迴函數

long p2(int[] hp, int[] money, int index, int m)

遞迴含義是:從 0……index 號怪獸,花的錢,必須嚴格等於 m 的情況下,如果通過不了,返回-1;如果可以通過,返回能通過情況下的最大能力值。

遞迴方法實現如下

    public static long p2(int[] hp, int[] money, int index, int m) {
        // 從右往左填,base case 
        if (index == -1) {
            return m == 0 ? 0L : -1L;
        }
        // 賄賂當前怪獸
        long p1 = p2(hp, money, index - 1, m);
        long ans = -1;
        if (p1 != -1 && p1 >= hp[index]) {
            // 賄賂是有效的,或者賄賂結果可以支援下一次的決策
            ans = p1;
        }
        // 不賄賂當前怪獸
        long p2 = p2(hp, money, index - 1, m - money[index]);
        if (p2 != -1) {
            // 可以走到最後,與賄賂得到的能力值pk下。
            ans = Math.max(ans, p2 + hp[index]);
        }
        return ans;
    }

同理,這個遞迴也可以改成動態規劃,也是利用一個二維陣列,

 public static long func4(int[] d, int[] p) {
        int sum = 0;
        for (int num : p) {
            sum += num;
        }
        // dp[i][j]含義:
        // 能經過0~i的怪獸,且花錢為j(花錢的嚴格等於j)時的武力值最大是多少?
        // 如果dp[i][j]==-1,表示經過0~i的怪獸,花錢為j是無法通過的,或者之前的錢怎麼組合也得不到正好為j的錢數
        int[][] dp = new int[d.length][sum + 1];
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j <= sum; j++) {
                dp[i][j] = -1;
            }
        }
        // 經過0~i的怪獸,花錢數一定為p[0],達到武力值d[0]的地步。其他第0行的狀態一律是無效的
        dp[0][p[0]] = d[0];
        for (int i = 1; i < d.length; i++) {
            for (int j = 0; j <= sum; j++) {
                // 可能性一,為當前怪獸花錢
                // 存在條件:
                // j - p[i]要不越界,並且在錢數為j - p[i]時,要能通過0~i-1的怪獸,並且錢陣列合是有效的。
                if (j >= p[i] && dp[i - 1][j - p[i]] != -1) {
                    dp[i][j] = dp[i - 1][j - p[i]] + d[i];
                }
                // 可能性二,不為當前怪獸花錢
                // 存在條件:
                // 0~i-1怪獸在花錢為j的情況下,能保證通過當前i位置的怪獸
                if (dp[i - 1][j] >= d[i]) {
                    // 兩種可能性中,選武力值最大的
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j]);
                }
            }
        }
        int ans = 0;
        // dp表最後一行上,dp[N-1][j]代表:
        // 能經過0~N-1的怪獸,且花錢為j(花錢的嚴格等於j)時的武力值最大是多少?
        // 那麼最後一行上,最左側的不為-1的列數(j),就是答案
        for (int j = 0; j <= sum; j++) {
            if (dp[d.length - 1][j] != -1) {
                ans = j;
                break;
            }
        }
        return ans;
    }

可以看到二維陣列的 dp 規模是錢數 * 達到的最大能力。適用於怪獸能力比較大,但是錢數不大的情況。

完整程式碼如下(含對數器)


public class NowCoder_BeatMonster {
    // i到最後通過所有怪獸的最少錢數是多少?
    // 適合怪獸能力不大的情況
    public static long func1(int[] hp, int[] money) {
        return p(hp, money, 0, 0);
    }

    public static long p(int[] hp, int[] money, int i, int j) {
        if (i == hp.length) {
            return 0;
        }
        // 選擇賄賂
        long p = money[i] + p(hp, money, i + 1, j + hp[i]);
        // 不選賄賂,有條件
        if (j >= hp[i]) {
            return Math.min(p, p(hp, money, i + 1, j));
        }
        return p;
    }

    public static long func1Dp(int[] hp, int[] money) {
        int sum = 0;
        for (int a : hp) {
            sum += a;
        }
        long[][] dp = new long[hp.length + 1][sum + 1];
        for (int i = hp.length - 1; i >= 0; i--) {
            for (int j = sum - hp[i]; j >= 0; j--) {
                dp[i][j] = money[i] + dp[i + 1][j + hp[i]];
                if (j >= hp[i]) {
                    dp[i][j] = Math.min(dp[i][j], dp[i + 1][j]);
                }
            }
        }
        return dp[0][0];
    }

    public static long func2(int[] hp, int[] money) {
        int sum = 0;
        for (int a : money) {
            sum += a;
        }
        int N = hp.length;
        for (int i = 0; i < sum; i++) {
            if (p2(hp, money, N - 1, i) != -1) {
                return i;
            }
        }
        return sum;
    }

    // 從0....index 怪獸,花的錢,必須嚴格==m
    // 如果通過不了,返回-1
    // 如果可以通過,返回能通過情況下的最大能力值
    public static long p2(int[] hp, int[] money, int index, int m) {
        if (index == -1) {
            return m == 0 ? 0L : -1L;
        }
        // 賄賂當前怪獸
        long p1 = p2(hp, money, index - 1, m);
        long ans = -1;
        if (p1 != -1 && p1 >= hp[index]) {
            ans = p1;
        }
        long p2 = p2(hp, money, index - 1, m - money[index]);
        if (p2 != -1) {
            ans = Math.max(ans, p2 + hp[index]);
        }
        return ans;
    }


    public static long func4(int[] d, int[] p) {
        int sum = 0;
        for (int num : p) {
            sum += num;
        }
        // dp[i][j]含義:
        // 能經過0~i的怪獸,且花錢為j(花錢的嚴格等於j)時的武力值最大是多少?
        // 如果dp[i][j]==-1,表示經過0~i的怪獸,花錢為j是無法通過的,或者之前的錢怎麼組合也得不到正好為j的錢數
        int[][] dp = new int[d.length][sum + 1];
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j <= sum; j++) {
                dp[i][j] = -1;
            }
        }
        // 經過0~i的怪獸,花錢數一定為p[0],達到武力值d[0]的地步。其他第0行的狀態一律是無效的
        dp[0][p[0]] = d[0];
        for (int i = 1; i < d.length; i++) {
            for (int j = 0; j <= sum; j++) {
                // 可能性一,為當前怪獸花錢
                // 存在條件:
                // j - p[i]要不越界,並且在錢數為j - p[i]時,要能通過0~i-1的怪獸,並且錢陣列合是有效的。
                if (j >= p[i] && dp[i - 1][j - p[i]] != -1) {
                    dp[i][j] = dp[i - 1][j - p[i]] + d[i];
                }
                // 可能性二,不為當前怪獸花錢
                // 存在條件:
                // 0~i-1怪獸在花錢為j的情況下,能保證通過當前i位置的怪獸
                if (dp[i - 1][j] >= d[i]) {
                    // 兩種可能性中,選武力值最大的
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j]);
                }
            }
        }
        int ans = 0;
        // dp表最後一行上,dp[N-1][j]代表:
        // 能經過0~N-1的怪獸,且花錢為j(花錢的嚴格等於j)時的武力值最大是多少?
        // 那麼最後一行上,最左側的不為-1的列數(j),就是答案
        for (int j = 0; j <= sum; j++) {
            if (dp[d.length - 1][j] != -1) {
                ans = j;
                break;
            }
        }
        return ans;
    }

    public static int[][] generateTwoRandomArray(int len, int value) {
        int size = (int) (Math.random() * len) + 1;
        int[][] arrs = new int[2][size];
        for (int i = 0; i < size; i++) {
            arrs[0][i] = (int) (Math.random() * value) + 1;
            arrs[1][i] = (int) (Math.random() * value) + 1;
        }
        return arrs;
    }

    public static void main(String[] args) {
        int len = 12;
        int value = 20;
        int testTimes = 10000;
        for (int i = 0; i < testTimes; i++) {
            int[][] arrs = generateTwoRandomArray(len, value);
            int[] d = arrs[0];
            int[] p = arrs[1];
            long ans1 = func1(d, p);
            long ans4 = func1Dp(d, p);
            long ans2 = func2(d, p);
            long ans3 = func4(d, p);
            if (ans1 != ans2 || ans2 != ans3 || ans1 != ans4) {
                System.out.println("oops!");
            }
        }

    }

}

更多

演演算法和資料結構筆記

參考資料

演演算法和資料結構體系班-左程雲