挑战程序竞赛系列(2):2.3优化递推关系式

2.3 优化递推关系式

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

练习题如下:
1. POJ 1742: Coins
2. POJ 3046: Ant Counting
3. POJ 3181: Dollar Dayz

POJ 1742: Coins

有n种面额的硬币,面额个数分别为A_i、C_i,求最多能搭配出几种不超过m的金额?

测试用例:

3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0

该dp很有意思,用boolean[][] dp,具有传递效果。递推式:

dp[i+1][j] |= dp[i][j-k*A[i]];

最后统计dp[n][1]~dp[n][m]中有多少个true即可。

代码如下:

public class SolutionDay03_P1742 {
//  static int MAX_N = 100;
//  static int MAX_M = 100000;
//  static boolean[][] dp = new boolean[MAX_N+1][MAX_M+1];
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);

        while (true){
            int n = in.nextInt();
            int m = in.nextInt();

            if (n == 0 && m == 0) break;

            int[] A = new int[n];
            int[] C = new int[n];

            for (int i = 0; i < n;  i++){
                A[i] = in.nextInt();
            }
            for (int i = 0; i < n;  i++){
                C[i] = in.nextInt();
            }

//          for (int i = 0; i < dp.length;i++){
//              Arrays.fill(dp[i], false);
//          }

            boolean[][] dp = new boolean[n+1][m+1];

            dp[0][0] = true;
            for (int i = 0; i < n; i++){
                for (int j = 0; j <= m; j++){
                    for (int k = 0; k <= C[i]; k++){
                        if (k * A[i] <= j){
                            dp[i+1][j] |= dp[i][j-k*A[i]];
                        }
                    }
                }
            }

            int count = 0;
            for (int i = 1; i <= m; i++){
                if (dp[n][i]) count ++;
            }
            System.out.println(count);
        }

        in.close();
    }

}

很遗憾,TLE,更新可视图如下:
alt text

上述情况不算if条件的过滤,时间复杂度为 O(ni(mki)) ,看下图:
alt text

如当i = 0, j = 3时,它也会有三次伪更新,但这三次更新是否有必要?显然完全不需要,因为我们知道dp[0][3] = 0。所以,程序很明显会TLE。

这里的主要原因在于,我们在计算下一阶段状态时,丢失了一些非常关键的信息。试想一下,我们能否在下一阶段带上前一阶段的硬币数?

观察上述更新过程,你会发现,它的更新很有特点,如从阶段0 -> 阶段1,有三次有效更新,而三次正是硬币可选择的个数0,1,2。同理,阶段1 -> 阶段2,两次有效更新,而硬币选择的个数为0,1。所以,给了我们一个启发,把硬币个数带入下一阶段,然后让它平行更新。

所以有了新的递推式:

dp[i][j] = c[i] // 表示当前能够更新的最大可能数。
dp[i][j] = -1 // 表示当前无法再更新

更新规则:
// 上一阶段跳入下一阶段
dp[i+1][j] = C[i], if dp[i][j] >= 0 // 说明当前面值可以在下一阶段递增
// 越界(当前面值超过最大数m)或者当前阶段没有硬币可用
dp[i+1][j] = -1, if j < A[i] || dp[i+1][j-A[i]] <= 0
// 用掉一个硬币,递推式减1,直到-1
dp[i+1][j] = dp[i+1][j-A[i]] - 1;

代码如下:

public class SolutionDay03_P1742 {
//  static int MAX_N = 100;
//  static int MAX_M = 100000;
//  static boolean[][] dp = new boolean[MAX_N+1][MAX_M+1];
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);

        while (true){
            int n = in.nextInt();
            int m = in.nextInt();

            if (n == 0 && m == 0) break;

            int[] A = new int[n];
            int[] C = new int[n];

            for (int i = 0; i < n;  i++){
                A[i] = in.nextInt();
            }
            for (int i = 0; i < n;  i++){
                C[i] = in.nextInt();
            }
            System.out.println(solve(n, m, A, C));
        }

        in.close();
    }

    public static int solve(int n, int m, int[] A, int[] C){
        int[][] dp = new int[n+1][m+1];
        for (int i = 0; i < dp.length; i++){
            Arrays.fill(dp[i], -1);
        }
        dp[0][0] = 0;
        for (int i = 0; i < n; i++){
            for (int j = 0; j <= m; j++){
                if (dp[i][j] >= 0){
                    dp[i+1][j] = C[i];
                }
                else if (j < A[i] || dp[i+1][j - A[i]] <= 0){
                    dp[i+1][j] = -1;
                }
                else{
                    dp[i+1][j] = dp[i+1][j-A[i]] - 1;
                }
            }
        }

        int answer = 0;
        for (int i = 1; i <= m; i++){
            if (dp[n][i] >= 0) answer++;
        }

        return answer;
    }

}

我们再来看看它的更新图,如下:
alt text

呵呵,MLE了,真是题目虐我千百遍,我待她如初恋啊,书上也说了,有些题就得重复利用数组。为啥可以?看上图,平行更新,且由if判断的顺序决定。代码如下:

    public static int solve(int n, int m, int[] A, int[] C){
        int[] dp = new int[m+1];
        Arrays.fill(dp, -1);
        dp[0] = 0;

        for (int i = 0; i < n; i++){
            for (int j = 0; j <= m; j++){
                if (dp[j] >= 0){
                    dp[j] = C[i];
                }
                else if (j < A[i] || dp[j-A[i]] <= 0){
                    dp[j] = -1;
                }
                else{
                    dp[j] = dp[j-A[i]] - 1;
                }
            }
        }

        int answer = 0;
        for (int i = 1; i <= m; i++){
            if (dp[i] >= 0) answer++;
        }
        return answer;
    }

POJ 3046: Ant Counting

蚂蚁牙黑,蚂蚁牙红:有A只蚂蚁,来自T个家族。同一个家族的蚂蚁长得一样,但是不同家族的蚂蚁牙齿颜色不同。任取n只蚂蚁(S<=n<=B),求能组成几种集合?

一道排列组合题,问题可以归简为:从元素为n个的集合中,取出m个元素,总共有多少种取法?

dp[i][j] 表示前i个家族中,取出j个元素的组合总数

初始化:
dp[0][0] = 1, 表示无蚂蚁,取出0个元素。(空集)

递推式:

dp[i][j]=k=0a[i]dp[i1][jk]

按书中的定义得,从前 i 个家族中取出j个,可以从前 i1 个家族中取出 jk 个,再从第 i 个家族中取出k个添加进来。

两点:

  • dp[i1][jk] 的值代表了取 jk 个元素的组合数,每个组合都是唯一代表一个集合。
  • 当有 k 个相同的元素加入到新的集合中,就有了dp[i][j]=1×dp[i1][jk],因为第 i 个家庭的k个元素组合始终为1,该家族的每个元素无差别。

代码如下:

public class SolutionDay03_P3046 {

    static final int MOD = 1000000;
    static int[] family = new int[1000+16];

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int T = in.nextInt();
        int A = in.nextInt();
        int S = in.nextInt();
        int B = in.nextInt();

        for (int i = 0; i < A; i++){
            int index = in.nextInt();
            ++family[index];
        }
        System.out.println(solve(T,A,S,B,family));
        in.close();
    }

    public static int solve(int T, int A, int S, int B, int[] family){
        int[][] dp = new int[T+1][10000 + 16];
        dp[0][0] = 1;
        int total = family[0];
        for (int i = 1; i <= T; ++i){
            total += family[i];
            for (int k = 0; k <= family[i]; ++k){
                for (int j = total; j >= k; --j){
                    dp[i][j] = (dp[i][j] + dp[i-1][j-k]) % MOD;
                }
            }
        }

        int ans = 0;
        for (int i = S; i <= B; ++i){
            ans = (ans + dp[T][i]) % MOD;
        }
        return ans;
    }
}

上述时间和空间都不尽人如意,递推式优化:

dp[i][j]=k=0α[i]dp[i1][jk]=dp[i1][j]+k=1α[i]dp[i1][jk]=dp[i1][j]+k=0α[i]1dp[i1][jk1]=dp[i1][j]+k=0α[i]dp[i1][j1k]dp[i1][j1α[i]]=dp[i1][j]+dp[i][j1]+dp[i1][j1α[i]]

代码如下:

public static int solve(int T, int A, int S, int B, int[] family){
        int[][] dp = new int[2][A+1];

        dp[0][0] = dp[1][0] = 1;

        int total = A;
        for (int i = 1; i <= T; ++i){
            for (int j = 1; j <= total; ++j){
                if (j - 1 - family[i] >= 0){
                    dp[i%2][j] = (dp[i%2][j-1] - dp[(i-1)%2][j-1-family[i]] + dp[(i-1)%2][j] + MOD) % MOD;
                }else{
                    dp[i%2][j] = (dp[i%2][j-1] + dp[(i-1)%2][j] ) % MOD;
                }
            }
        }

        int ans = 0;
        for (int i = S; i <= B; ++i){
            ans = (ans + dp[T%2][i]) % MOD;
        }
        return ans;
    }

POJ 3181: Dollar Dayz

农夫约翰有N元钱,市场上有价值1……K的商品无限个,求所有的花钱方案?

dp[i][j] :用i种价格配出金额j的方案数

初始化:
dp[i][0] = 1; 用i中价格配出金额0的方案数为1

递推式:
dp[i][j] = dp[i-1][j] + dp[i-1][j-i] + dp[i-1][j-2*i] + ... + dp[i-1][0]
如:
1 * x + 2 * y = 5;
y = 0, 1 * x = 5 
y = 1, 1 * x = 3
y = 2, 1 * x = 1
dp[2][5] = dp[1][5] + dp[1][3] + dp[1][1]

代码如下:

public class SolutionDay04_P3181 {

    public static void main(String[] args) {

        Scanner in = new Scanner(System.in);
        int N = in.nextInt();
        int K = in.nextInt();
        System.out.println(solve(N, K));

        in.close();
    }

    public static BigInteger solve(int N, int K){
        BigInteger[][] dp = new BigInteger[K + 1][N + 1];
        for (int i = 0; i < dp.length; i++){
            for (int j = 0; j < dp[0].length; j++){
                dp[i][j] = BigInteger.ZERO;
            }
        }
        dp[0][0] = BigInteger.ONE;
        for (int i = 1; i <= K; ++i) {
            for (int k = 0; k <= N; k += i) {
                for (int j = N; j >= k; --j) {
                    dp[i][j] = dp[i][j].add(dp[i - 1][j - k]);
                }
            }
        }
        return dp[K][N];
    }
}

递推式优化:

dp[i][j] = dp[i-1][j] + dp[i-1][j-i] + dp[i-1][j-2*i] + ... + dp[i-1][0];

dp[i][j-i] = dp[i-1][j-i] + dp[i-1][j-2*i] + dp[i-1][j-3*i] + ... + dp[i-1][0];

所以有:
dp[i][j] = dp[i-1][j] + dp[i][j-i];

代码如下:

    public static BigInteger solve(int N, int K){
        BigInteger[][] dp = new BigInteger[K + 1][N + 1];
        for (int i = 0; i < dp.length; i++){
            for (int j = 0; j < dp[0].length; j++){
                dp[i][j] = BigInteger.ZERO;
            }
        }
        dp[0][0] = BigInteger.ONE;
        for (int i = 1; i <= K; ++i) {
            //必须得初始化,dp[i][0]不能由dp[0][0]推得
            dp[i][0] = BigInteger.ONE;
            for (int j = 1; j <= N; ++j){
                if (j < i){
                    dp[i][j] = dp[i-1][j];
                }
                else{
                    dp[i][j] = dp[i-1][j].add(dp[i][j-i]);
                }
            }
        }
        return dp[K][N];
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值