前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >挑战程序竞赛系列(2):2.3优化递推关系式

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

作者头像
用户1147447
发布2019-05-26 09:43:33
3080
发布2019-05-26 09:43:33
举报

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1434730

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,更新可视图如下:

上述情况不算if条件的过滤,时间复杂度为O(n∑i(m∗ki))O(n\sum_i (m * k_i)),看下图:

如当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;
    }

}

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

呵呵,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个元素。(空集)

递推式:

dpi=∑k=0aidpi−1

dpi = \sum_{k = 0}^{ai} dpi-1

按书中的定义得,从前ii个家族中取出jj个,可以从前i−1i-1个家族中取出j−kj-k个,再从第ii个家族中取出kk个添加进来。

两点:

  • dpi−1dpi-1的值代表了取j−kj-k个元素的组合数,每个组合都是唯一代表一个集合。
  • 当有kk个相同的元素加入到新的集合中,就有了dpi=1×dpi−1dpi = 1 \times dpi-1,因为第ii个家庭的kk个元素组合始终为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;
    }
}

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

dpi=∑k=0αidpi−1=dpi−1+∑k=1αidpi−1=dpi−1+∑k=0αi−1dpi−1=dpi−1+∑k=0αidpi−1−dpi−1j−1−αi]=dpi−1+dpi+dpi−1j−1−αi]

\begin{align*} dpi &= \sum_{k = 0}^{\alphai} dpi-1\ &= dpi-1 + \sum_{k=1}^{\alphai} dpi-1\ &= dpi-1 + \sum_{k=0}^{\alphai-1}dpi-1\ &= dpi-1 + \sum_{k=0}^{\alphai}dpi-1 - dpi-1j-1-\alphai]\ &= dpi-1 + dpi + dpi-1j-1-\alphai] \end{align*}

代码如下:

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];
    }
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017年05月03日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2.3 优化递推关系式
    • POJ 1742: Coins
      • POJ 3046: Ant Counting
        • POJ 3181: Dollar Dayz
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档