 鲜花( 0)  鸡蛋( 0)
|
动态规划的基本思想是将待求解问题分解成若干个子问题,先求解子问题,并将这些子问题的解保存起来,如果以后在求解较大子问题的时候需要用到这些子问题的解,就可以直接取出这些已经计算过的解而免去重复运算。保存子问题的解可以使用填表方式,例如保存在数组中。$ X3 A9 |! T, G: W. m
用一个实际例子来体现动态规划的算法思想——硬币找零问题。- ^/ A; B( ~! G6 y3 `$ J8 N
硬币找零问题描述:现存在一堆面值为 V1、V2、V3 … 个单位的硬币,问最少需要多少个硬币才能找出总值为 T 个单位的零钱?假设这一堆面值分别为 1、2、5、21、25 元,需要找出总值 T 为 63 元的零钱。9 }/ H, A e) u* P
很明显,只要拿出 3 个 21 元的硬币就凑够了 63 元了。4 d. x" Z. P7 q
基于上述动态规划的思想,我们可以从 1 元开始计算出最少需要几个硬币,然后再求 2 元、3元…每一次求得的结果都保存在一个数组中,以后需要用到时则直接取出即可。那么我们什么时候需要这些子问题的解呢?如何体现出由子问题的解得到较大问题的解呢?# ^) ?7 z3 e U9 s; B3 }% F+ q
其实,在我们从 1 元开始依次找零时,可以尝试一下当前要找零的面值(这里指 1 元)是否能够被分解成另一个已求解的面值的找零需要的硬币个数再加上这一堆硬币中的某个面值之和,如果这样分解之后最终的硬币数是最少的,那么问题就得到答案了。- T0 z* D* e) H6 u, \
单是上面的文字描述太抽象,先假定以下变量:
) L& X& a \) [8 T9 ] values[] : 保存每一种硬币的币值的数组
$ C; B. C4 }8 }/ J& E: ` valueKinds :币值不同的硬币种类数量,即values[]数组的大小+ L- @8 Z* O4 F3 g( h, d
money : 需要找零的面值
' w. s/ }5 J1 q. S' m coiUsed[] : 保存面值为 i 的纸币找零所需的最小硬币数7 I2 d) z% C/ a1 o2 |
算法描述:
0 d) W6 i# }6 k: C+ [, y 当求解总面值为 i 的找零最少硬币数 coiUsed[ i ] 时,将其分解成求解 coiUsed[ i – cents]和一个面值为 cents 元的硬币,由于 i – cents < i , 其解 coiUsed[ i – cents] 已经存在,如果面值为 cents 的硬币满足题意,那么最终解 coiUsed[ i ] 则等于 coiUsed[ i – cents] 再加上 1(即面值为 cents)的这一个硬币。1 h) ^! V. n1 B r3 @/ T
下面用代码实现并测试一下:/ k1 [% N. t6 v! F
public class CoiChange {
6 L- [3 r* ^( F5 Q0 F" Y /**
/ ]3 U% C! \9 Z9 |0 B" d * 硬币找零:动态规划算法, _! w! v, D* {1 w4 w
*
1 i$ p$ v, v& z5 E0 \ * @param values
8 y+ M: r# a! z4 V& O * :保存每一种硬币的币值的数组: ?( ^" R: w8 p s: M; a
* @param valueKinds3 E. N6 I2 x0 p) T y8 v, K
* :币值不同的硬币种类数量,即coinValue[]数组的大小
/ A$ U8 J. U* l) z * @param money
+ v: z r* ]/ T" Z$ ~ * :需要找零的面值
9 y9 M; p5 |4 G) A6 m * @param coiUsed
9 Y* X9 t# J% C! { * :保存面值为i的纸币找零所需的最小硬币数
$ B+ p) A( M$ g! j) s$ d6 l4 m */
. o; T$ }* {) H. X e public static void makeChange(int[] values, int valueKinds, int money,
0 J# k" X. i& M4 ~4 } int[] coiUsed) {; `7 Z3 p1 n" w& x3 n R3 A1 n6 d
coiUsed[0] = 0;
s) A' T2 I2 E' S; N3 t1 h // 对每一分钱都找零,即保存子问题的解以备用,即填表- B% N9 H, t* v+ P5 l1 U$ V
for (int cents = 1; cents <= money; cents++) {" j2 W3 o( B- M w. x& ]1 A9 ]8 T
// 当用最小币值的硬币找零时,所需硬币数量最多$ C* E* ~2 d/ v9 S( Q6 x0 P. c& T
int minCoi = cents;0 a3 Y% ~/ d: R' D+ n T
// 遍历每一种面值的硬币,看是否可作为找零的其中之一
, I: j% r9 l- [$ h: P8 ]& J8 d for (int kind = 0; kind < valueKinds; kind++) {8 X% x9 I* B0 R; M! ~9 }/ `
// 若当前面值的硬币小于当前的cents则分解问题并查表
' Q( `, x' B' |8 Q7 ~; Z if (values[kind] <= cents) {& _/ P% Y; P' ]) F/ }: H; M2 o3 W
int temp = coiUsed[cents - values[kind]] + 1;. R5 n# r) m; U) _3 H4 T' Y6 }/ |) ^* c
if (temp < minCoi) {/ z# G/ R* M7 N
minCoi = temp;3 s4 h; D( A, E5 ?' h& r4 f1 T
}, l7 I/ U9 O; f/ r+ ^
}
3 P4 v/ n" Y R7 v& g }2 e; s; l2 l( @ s7 v f3 c
// 保存最小硬币数/ }3 _ b- Z @. ?9 @& h! h/ a
coiUsed[cents] = minCoi;
0 \- K$ ~5 N/ r" A* } System.out.println("面值为 " + (cents) + " 的最小硬币数 : "
$ e7 S' t) t1 \8 y) }; H; e + coiUsed[cents]);5 d9 @) g& O2 Y$ c
}
. X4 k/ d. _' P. r" t8 ]7 P- Q/ J }
: n8 c& ]0 `4 g6 D public static void main(String[] args) {
7 \% I' ?4 O& c1 a9 X7 c0 b // 硬币面值预先已经按降序排列
- a) {# K5 E; P, n. s/ G& O int[] coinValue = new int[] { 25, 21, 10, 5, 1 };; p+ o/ U! b% o# K9 \3 K! A2 i
// 需要找零的面值
0 H% z8 T& q# {8 D/ U int money = 63;4 Q2 j8 h2 Y) l4 h+ W
// 保存每一个面值找零所需的最小硬币数,0号单元舍弃不用,所以要多加1( z, T& T0 {- y7 U* \
int[] coiUsed = new int[money + 1];
; A. B$ \' f9 j* b makeChange(coinValue, coinValue.length, money, coiUsed);$ z2 W% y% H& e
}; r" A$ f A8 `9 ]- s( N+ T
}
: s0 A+ i4 J w- e2 V& i8 j7 |# r 测试结果:
" P' q, ] ]( f" O J 面值为 1 的最小硬币数 : 1 面值为 2 的最小硬币数 : 2 面值为 3 的最小硬币数 : 3 面值为 4 的最小硬币数 : 4 面值为 5 的最小硬币数 : 1 面值为 6 的最小硬币数 : 2 ... ... 面值为 60 的最小硬币数 : 3 面值为 61 的最小硬币数 : 4 面值为 62 的最小硬币数 : 4 面值为 63 的最小硬币数 : 3
3 X0 v- e2 e$ c7 o; U& ^4 C% ^ 上面的代码并没有给出具体应该是哪几个面值的硬币,这个可以再使用一些数组保存而打印出来。
- d9 M4 Q) G6 u2 c6 p! c 参考lovebeyond19831993.blog.163.com/blog/static/131811444201101901819833/& A0 S6 w# t# X8 V: ]4 {
问题:) i6 E. L. C3 p* I5 w; x( n! {
假设有n种面值不同的硬币,个个面值存于数组T[1:n]中,现在用这些硬币来找钱,各种硬币的使用个数不限;* t1 C" b( R0 h8 C2 \+ Q0 [. H& R
求对于给定的钱数N,最少可以由几枚硬币组成,并输出硬币序列。" d8 k! B. V+ m3 c- ?
分析:
( m; v! W/ C4 D" U& N" h* } 假设对于i = 1...N-1, 所需最少的硬币数Count(i) 已知, 那么对于N,所需的硬币数为Min( Count(i) + Count(N-i)) , i=1...N-1;
2 E' Q% {- Q% B4 o5 O 于是一个直观的方法是用递归计算。
" F/ ]! v. L4 f: J4 I3 H; y) \ 但是,递归过程中,每次计算Count(i),都会重复计算 Count(1)....Count(i-1); 这样时间复杂度就是O(N2);
) l+ E) c& U5 N& w8 ]7 _" | 事实上,这是一个典型的动态规划问题,我们可以从1开始记录下每个钱数所需的硬币枚数,避免重复计算,为了能够输出硬币序列,我们还需要记录下每次新加入的硬币。0 \, C6 ]. O# A4 R) h3 z- X( F4 u
下面给出用动态规划解决此问题的递推式:
- D9 ?- e4 C1 Y, ]7 E, f 参数说明: 当只用面值为T[1],T[2],…T[n]来找出钱j时,所用的硬币的最小个数记为C(i,j),则C(i,j)的递推方程为:
* K V. \0 V( C1 T 算法设计:
4 _+ R* X, S* P4 {2 ^ #include <iostream>
( f W1 x8 i. O) B f$ m using namespace std;
z" e0 _, c1 E2 f' D% ]+ Y+ ~3 y int min_number_of_stamps(cot int* array, size_t array_size, int request) {
* p- x4 X5 U4 h& b if (array == NULL || array_size <= 0 || request < 1) {" u& |6 |/ l1 f0 B) L8 S
return -1;' [; r% _' Z( K6 P. D5 e3 [; ^" X- L
}& Z% m' ^ p+ |/ c4 i
int* numofStamps = new int[request + 1];4 [ b5 w4 T# ]
numofStamps[0] = 0;; Z$ @: Q% \* _0 X
for(int i = 1; i <= request; i++){
) l1 N8 W, Z' L' K% j7 I1 ~5 {3 C: O$ S int cn = i;
; h2 _' R4 q o! b) B for(size_t j = 0; j < array_size; j++){
$ w9 n8 H U( R( {* ^" k1 d if(array[j] <= i){
V/ B) T/ i! m0 e int tmp = numofStamps[i - array[j]] + 1;
) u' g' o& e6 ~+ ~* w* A3 ^ if(tmp < cn)/ o. T' ~* L' @7 N6 q
cn = tmp;. e% P1 t: M$ u5 R- n J* s/ q
}
5 w5 `3 m" |# A( C, H# x6 D }5 z. V- `6 a8 O' z0 b+ U4 w, R' {4 f
numofStamps[i] = cn;
$ K# Z8 x% @8 b/ w }
/ C- x9 S, ^, ~0 o int t = numofStamps[request];
" \5 x7 D0 o4 n Z" @8 X1 g delete[] numofStamps; u/ j- p( N/ @: a% m) x
return t;
/ q0 I( [% R$ g! z& _ }* X( ^/ { C" }3 P% Y
int main() {
4 g; b' u9 v" d9 U: _ int array[] = { 90, 30, 24, 15, 12, 10, 5, 3, 2, 1 };: Z9 i9 n+ p3 C+ b1 E) ]0 R& |5 m% x
int array_size = sizeof(array) / sizeof(int);
e1 O) Q0 Y) c6 y int t = min_number_of_stamps(array, array_size, 32);
$ ]9 U' V& S% y! ^/ J$ h cout << t << endl;
4 v4 i, }% R$ k2 F' @+ R }
+ |- O- U; B, m4 s |
|