1.引入
动态规划的思想是从顶向下、从最后到最初考虑;
码代码时从底向上、从头到尾开始写起。
1.1.给定一排硬币,面值不等,均大于0,则如何选择硬币,使得选择的硬币都不相邻,同时选择出来的硬币的总值最大?
从顶向下考虑:
假设F[n]为:从1-n号硬币中可以选到的最大价值。对于第n号硬币,我们有两个选择:
我们不要第n号硬币的,则F[n]=F[n-1],选择完前n枚硬币的总价值和选择完前n-1枚硬币的总价值是一样的;
我们是要这第n号硬币的,则最后的总价值是F[n]=F[n-2]+val[n]。
表示我们要了第n号硬币(价值为val[n]),但是第n-1号硬币就不能要了,所以还可以得到的价值为F[n-2],表示从1-[n-2]号硬币可以得到的最大价值。
根据这个规律就可以得到F[n]的表达式了:
- F[1]=val[1]
表示就是如果只有一枚硬币,那总价值就是这个硬币的价值 - F[n]=max(F[n-1],F[n-2]+val[n])
表示我要第n枚硬币和不要第n枚硬币这两个选择中可以产生的最大价值
伪代码
算法 CoinRow(val[1...n])
//算出间隔硬币的最大硬币价值
//输入:一排硬币的价值val[n]
//输出:最大硬币价值F[n]
//伪代码中,数组下角标从1开始计
F[0]=0;F[1]=val[1];
for i←2 to n do
F[n]=max(F[n-1],F[n-2]+val[i])
return F[n]
JAVA描述:
import java.util.Scanner;
public class DPcoin {
public static void main(String[] args) {
System.out.print("input n:");
Scanner input=new Scanner(System.in);
int n=input.nextInt();
int[] coins=new int[n];
System.out.printf("input %d coins's values\n",n);
for(int i=0;i<n;i++)
coins[i]=input.nextInt();
System.out.println("the coins:");
System.out.println(coins);
int f[]=new int[n];
f[0]=coins[0];
f[1]=coins[1];
for(int i=2;i<n;i++)
f[i]=Math.max(coins[i]+f[i-2], f[i-1]);
System.out.printf("the utmost value: %d",f[n-1]);
input.close();
}
}
1.2 给定一个金额,再给定一个硬币的价值列表,计算怎么用最少的硬币来描述这个价格。
题目解释:
有的人可能会觉得这个问题没有意义,因为比如9块钱,我直接先取一个5块钱再取4个1块钱好了,比如32块钱,我直接取一个20一个10块钱2个1块钱好了,就是从大面值的开始算;
但是这是默认使用人民币的情况,如果这个国家发行的硬币面值是1、4、5元,要组合出一个8元呢?按照从大到小的顺序就是先找1个5元,再找3个1元总共4个硬币,但是实际上最优解是2个4元钱。
现在来分析问题:
先来看1.1中最核心的式子:
F[n]=max(F[n-1],F[n-2]+val[i])
1.1的约束条件是选择硬币不相邻,现在约束条件是选择硬币的步长有要求(从硬币面值的集合中进行选择),假设硬币有4种面值{1,4,5,10};
我们假设凑成面值sum需要的最少硬币数为F[sum],则我们有4种方式到达面值sum.选择的最后一个硬币面值为:
1 此时F[sum]=F[sum-1]+1,即凑成【面值为sum-1】的最少硬币数再加1
4 此时F[n]=F[sum-4]+1
5 此时F[n]=F[sum-5]+1
10 此时F[n]=F[sum-10]+1
我们选择这当中最小的一个:
F[n]=min(F[sum-1],F[sum-4],F[sum-5],F[sum-10])+1
伪代码:
算法 ChangeMaking(sum,coins[1...n])
//从面值为coins的硬币集合中,用最少的硬币,凑出总价值为sum的面值
//输入:总价值,硬币面值的数组
//输出:使用的硬币数F[n]
F[0]=0;F[1]=1;
for i←2 to sum do
//i循环的总面值数,即从2块钱一直算到sum块钱需要的最少硬币数
tmp←∞;
for j←1 to n do
//j循环的是硬币的类别
//到达面值sum前有4种选择,4种中有一个花费了最少的硬币数,最少硬币数用tmp保存
if sum≥coins[j]
tmp←min(F[sum-coins[j]],tmp)
else
break
F[i]←tmp+1
return F[n]
JAVA描述:
import java.util.Scanner;
public class ChangeMaking {
public static void main(String[] args) {
System.out.println("input the amount of change:");
Scanner input=new Scanner(System.in);
int sum=input.nextInt();
int coins[]= {1,4,5,10,20};
int F[]=new int[sum+1];
F[0]=0;F[1]=1;
int tmp,j;
for(int i=2;i<=sum;i++) {
j=0;tmp=Integer.MAX_VALUE;
while(j<coins.length&&i>=coins[j]) {
tmp=Math.min(F[i-coins[j]], tmp);
j++;
}
F[i]=++tmp;
}
System.out.printf("coin cnt:%d",F[sum]);
input.close();
}
}
1.3 m*n的方格中有一些硬币,硬币的面值不一,从左上角一直向下或向右走走到右下角,怎么选择路径使得收集的硬币总额最大?
从顶向下考虑:
假设累计到最右下角时的最大硬币总额为F[m][n],则F[m][n]可以表示为:
- F[m][n]=max(F[m-1][n],F[m][n-1]+coins[m][n-1])
同时除了第一排和第一列的格子,其他地方的格子累计最大硬币总额也可以表示为:
- F[i][j]=max(F[i-1][j],F[i][j-1]+coins[i][j-1])
即如果我要计算走到第i行第j列格子时,可以得到的最大硬币总额,我要先算这个格子上边和左边的最大硬币总额,取更大的那个,然后再加上自己当前格子上的硬币的钱,就可以得到走到这个地方可以得到的最大硬币面额。
伪代码:
算法 CollectionCoins(coins[m][n])
//计算从(1,1)走到(m,n)能收获的最大硬币总额
//输入:一个m*n格子上硬币的面值
//输出:最大硬币总额 F[m][n]
F[1][1]=coins[1][1]
//先计算出第一排的结果
for i←2 to n do
F[1][i]←F[1][i-1]+coins[1][i]
for i←2 to m do
F[i][2]←F[i][1]+coins[i][2] //每一排第一个F值单独计算
for j←2 to n do
F[i][j]←max(F[i-1][j],F[i][j-1])+coins[i][j]
return F[m][n]
JAVA实现:
public class CollectCoins {
public static void main(String[] args) {
int coins[][]= {
{3,4,1,5,0,7,6},
{5,4,8,2,4,1,7},
{6,0,9,1,4,3,1,}
};
int F[][]=new int[coins.length][coins[0].length];
F[0][0]=coins[0][0];F[1][0]=F[0][0]=coins[1][0];
//初始化F第一排
for(int i=1;i<coins[0].length;i++) {
F[0][i]=F[0][i-1]+coins[0][i];
}
for(int i=1;i<coins.length;i++) {
F[i][0]=F[i-1][0]+coins[i][0];
for(int j=1;j<coins[0].length;j++) {
F[i][j]=Math.max(F[i-1][j], F[i][j-1])+coins[i][j];
}
}
System.out.print(F[coins.length-1][coins[0].length-1]);
}
}
总结:
首先关键的是求F表达式之间的关系。然后可以发现,前面三个题中的动态规划其实都将所有过程中的最优解解出来了:
比如第一题,其实求出了取到第1、2、3…n个硬币时可以得到的最大面额F[i];
第二题,先求总额为1块钱时的最少硬币组合、再求总额为2块钱时的最少硬币组合、…最后求到总额为sum块钱时的最少硬币组合;
第三个问题其实将表格中每个位置可以得到的最大硬币总额都求出来了。