算法思想之动态规划(三)——找零钱问题

前言

今天我们继续讨论经典的动态规划问题之找零钱问题

找零钱问题

问题描述

假设你是一名超市收银员,现有n种不同面值的货币,每种面值的货币可以使用任意张。顾客结账时,你需要找给顾客aim元零钱,你可以给出多少种方法。例如,有1、2、3元三种面值的货币,你需要找零3元,那么共有3种方法:1张1元+1张2元、3张1元、1张3元。

问题分析

假设长度为n的一维数组penny,其中每个元素对应每种货币的面值。找零钱问题可以抽象为使用penny中的元素可以有多少种方法组成数值aim
简单的,我们可以遍历数组,对下标index的元素使用i(0 \leq i \leq aim / penny[index])次, 计算剩余的数组元素和剩余数值满足要求的方法数。把每一次的方法数相加求和即为该问题的解。不难发现,每一次要求解的都是和父问题具有同样性质的子问题,即使用penny[index:n-1]中的元素有多少种方法组成数值aim - i * penny[index]
由此,很容易写出该问题的暴力搜索(即递归)方法和记忆搜索方法。但是如果要直接写出动态规划的状态转移方程可能需要费点功夫。不过,我们可以按照算法思想之动态规划(一)讨论的动态规划的一般步骤进行思考。
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

对于上面4条,我们一一来看。
(1) 划分阶段
构造有序或可排序的阶段,要求我们计算的时候肯定当前计算结果依赖于前面阶段的结果。如果问题中的aim=3penny = [1, 2, 3],如何划分阶段呢?对于penny来说,可按照下标进行划分,即1,2,3这3个阶段;对于aim来说,由于每一阶段下都要求aim是非负整数,那么我们可以划分为0-aim,即0,1,2,3这4个阶段。
(2) 确定状态和状态变量
由第(1)步,我们可以得到一个的n \times (aim+1)的矩阵dp,每个矩阵元素dp[i][j]代表的含义是使用数组pennyi个元素组成数值j的方法总数。
(3) 确定决策并写出状态转移方程
由第(2)步总结可得出规律:dp[i][j] = dp[i-1][j] + dp[i-1][j-penny[i]] + ... + dp[i-1][j-k*penny[i]].例如:仍然以(1)中的问题为例,dp[1][2] = dp[0][2] + dp[0][2-1*1]),代表使用penny前2个元素(即1,2)组成2的方法数 = 不使用2组成2的方法数 + 使用1个2组成2的方法数。
其实,对于该状态转移方程还可以继续优化:令z = j-penny[i]由于dp[i][z] = dp[i-1][z] + dp[i-1][z - penney[i]] +... + dp[i-1][z-k' *penny[i]],可以看出dp[i][z]dp[i][j]展开式中第2项之后的值是一样的,因此状态方程可化简为:dp[i][j] = dp[i-1][j] + dp[i][j-penny[i]]
(4) 寻找边界条件
由第(3)步,可得到化简前的边界条件为:j-k*penny[i] \geq 0,化简后的边界条件为:j-penny[i] \geq 0
总结来看,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)

代码实现

public class Exchange {
    // 用于记忆搜索的计算过的方案
    public static HashMap<String, Integer> map = new HashMap<>();

    public static int countWays(int[] penny, int n, int aim) {
        if (0 == n || null == penny || aim <= 0) {
            return 0;
        }
        // return core1(penny, 0, aim);
        // return core2(penny, 0, aim);
        // return core3(penny, n, aim);
        return core4(penny, n, aim);
    }

    /**
     * 暴力搜索
     * @param penny
     * @param index
     * @param aim
     * @return
     */
    public static int core1(int[] penny, int index, int aim) {
        int result = 0;
        if (index == penny.length) {
            result = aim == 0 ? 1 : 0;
        } else {
            for (int i = 0; i * penny[index] <= aim; i++) {
                result += core1(penny, index + 1, aim - i * penny[index]);
            }
        }
        return result;
    }

    /**
     * 记忆搜索
     * @param penny
     * @param index
     * @param aim
     * @return
     */
    public static int core2(int[] penny, int index, int aim) {
        String key = String.valueOf(index) + "_" + String.valueOf(aim);
        if (map.containsKey(key)) {
            return map.get(key);
        }
        int result = 0;
        if (index == penny.length) {
            result = aim == 0 ? 1 : 0;
        } else {
            for (int i = 0; i * penny[index] <= aim; i++) {
                result += core2(penny, index + 1, aim - i * penny[index]);
            }
        }
        map.put(key, result);
        return result;
    }

    /**
     * 动态规划
     * @param penny
     * @param n
     * @param aim
     * @return
     */
    public static int core3(int[] penny, int n, int aim) {
        int[][] dp = new int[n][aim + 1];
        for (int i = 0; i < aim + 1; i++) {
            dp[0][i] = i % penny[0] == 0 ? 1 : 0;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < aim + 1; j++) {
                for (int m = 0; m * penny[i] <= j; m++) {
                    dp[i][j] += dp[i - 1][j - m * penny[i]];
                }
            }
        }
        return dp[n-1][aim];
    }

    /**
     * 动态规划优化
     * @param penny
     * @param n
     * @param aim
     * @return
     */
    public static int core4(int[] penny, int n, int aim) {
        int[][] dp = new int[n][aim + 1];
        for (int i = 0; i < aim + 1; i++) {
            dp[0][i] = i % penny[0] == 0 ? 1 : 0;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < aim + 1; j++) {
                if (j < penny[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - penny[i]];
                }
            }
        }
        return dp[n - 1][aim];
    }

    public static void main(String[] args) {
        int[] penny = new int[]{3, 4, 7};
        int n = penny.length;
        int aim = 33;
        System.out.println(countWays(penny, n, aim));
    }
}

其他经典问题

未来几篇博文,我将继续对经典的动态规划问题进行整理,敬请关注~
由于本人水平有限,文章难免有欠妥之处,欢迎大家多多批评指正!

写在最后

欢迎大家关注我的个人博客复旦猿

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容