题目1:给定数组arr,arr中所有的值都是正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求组成aim的最少货币数。
举例:
arr[5,2,3],aim=20。 4张5元可以组成20元,其他的找钱方案都要使用更多张的货币,所以返回4。
题解:
一眼看去这道题好像可以用贪心算法可解,但是仔细分析发现有些值是不可以的,例如arr[1,3,4],aim=6 :用贪心算法算最少钱数为3 (4+1+1),但是我们可以明显的发现用两张3元的就够了,所以用贪心算法不可解。
其实这是一道经典的动态规划方法,我们可以构造一个dp数组,如果arr的长度为N,则dp数组的行数为N,列数为aim+1,dp[i][j] 的含义是:在可以任意使用arr[0..i]货币的情况下,组成j所需要的最小张数。
明白以上定义后我们初始化第一行与第一列,第一行dp[0][0..aim]中每一个元素dp[0][j]表示用arr[0]货币找开面额 j所需要的最少货币数,此时我们只能选取arr[0]这一张货币,所以只有arr[0]的整数倍的面额钱才可以找开,例如当arr[0]=3,aim=10时,只能找开3,6,9的货币,而其他面额的则无法找开,所以将arr[0][3,6,9]初始化为1,2,3 除此之外其他值初始化为整形int的最大值INT_MAX表示无法找开。对于第一列dp[0..n][0] 中的每一个元素dp[i][0]表示用arr[i]组成面额为0的钱的最少货币数,完全不需要任何货币,直接初始化为0即可。
对于剩下的任意dp[i][j],我们依次从左到右,从上到下计算,dp[i][j]的值可能来自下面:
- 完全不使用当前货币arr[i]的情况下的最少张数,即dp[i-1][j]的值
- 只使用1张当前货币arr[i]的情况下的最少张数,即dp[i-1][j-arr[i]]+1
- 只使用2张当前货币arr[i]的情况下的最少张数,即dp[i-1][j-2*arr[i]]+2
- 只使用3张当前货币arr[i]的情况下的最少张数,即dp[i-1][j-3*arr[i]]+3
- …..
-
import java.util.*; public class Coin3 { public static void main(String args[]){ Scanner in=new Scanner(System.in); int arr[]=new int[]{1,3,5}; while(in.hasNext()){ int total=in.nextInt(); int dp[][]=new int[arr.length][total+1]; //注意初始化,当找零为0时,初始化为1种 for(int i=0;i<arr.length;i++){ dp[i][0]=1; } for(int j=1;j<=total;j++){ if(j%arr[0]==0){ dp[0][j]=1; }else{ dp[0][j]=0; } } //注意更新策略,使用在arr[i][j]处使用arr[i]和不使用arr[i]两种情况之和 for(int i=1;i<arr.length;i++){ for(int j=1;j<=total;j++){ if(arr[i]>j){ dp[i][j]=dp[i-1][j]; }else{ dp[i][j]=dp[i-1][j]+dp[i][j-arr[i]]; } } } System.out.println(dp[arr.length-1][total]); } } }
以上所有情况中,最终取张数最小的,即dp[i][j] = min( dp[i-1][j-k*arr[i]]+k )( k>=0 )
=>dp[i][j] = min{ dp[i-1][j], min{ dp[i-1][j-x*arr[i]]+x (1<=x) } } 令x = y+1
=>dp[i][j] = min{ dp[i-1][j], min{ dp[i-1][j-arr[i]-y*arr[i]+y+1 (0<=y) ] } }
又有 min{ dp[i-1][j-arr[i]-y*arr[i]+y (0<=y) ] } => dp[i][ j-arr[i] ] ,所以,最终有:dp[i][j] = min{ dp[i-1][j], dp[i][j-arr[i]]+1 }。如果j-arr[i] < 0,即发生了越界,说明arr[i]太大了,用一张都会超过钱数j,此时dp[i][j] = dp[i-1][j]。
import java.util.*; //每种类型的货币可以使用多次
public class coin1 {
public static void main(String args[]){
Scanner in=new Scanner(System.in);
while(in.hasNext()){
int arr[]=new int[]{1,3,4,5};
int total=in.nextInt();
int dp[][]=new int[arr.length][total+1];
//初始化边界(第一列),需要找零为0时,所需要的硬币数为0
for(int i=0;i<arr.length;i++)
dp[i][0]=0;
//初始化边界(第一行),只用arr[0]去找零
for(int j=1;j<=total;j++){
if(j%arr[0]==0){
dp[0][j]=j/arr[0];
}else {
dp[0][j]=1000000;
}
}
//动态更新每一行和每一列,注意理解动态规划的公式
for(int i=1;i<arr.length;i++){
for(int j=1;j<=total;j++){
if(j>=arr[i]){
dp[i][j]=Math.min(dp[i-1][j],dp[i][j-arr[i]]+1);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
System.out.println(dp[arr.length-1][total]);
}
}
}
题目2: 给定数组arr,arr中所有的值都为正数,每个值仅代表一张钱的面值,再给定一个整数aim代表要找的钱数,求组成aim的最小货币数。
题解: 相对于上一题,这道题的arr中的钱只有一张,而不是任意多张,构造dp数组的含义也同上,但是此时略有不同,
dp第一行dp[0][0..aim]的值表示只使用一张arr[0]货币的情况下,找某个钱数的最小张数。比如arr[0]=2,那么能找开的钱数仅为2, 所以令dp[0][2]=1。因为只有一张钱,所以其他位置所代表的钱数一律找不开,一律设为INT_MAX。第一列dp[0…N-1]表示找的钱数为0时需要的最少张数,钱数为0时完全不需要任何货币,所以全设为0即可。
剩下的位置从左到右,从上到下计算,dp[i][j]可能的值来自于以下两种情况
- dp[i][j]的值代表在可以任意使用arr[0..i]货币的情况下,组成j所需要的最小张数。可以任意使用arr[0..i]货币的情况当然包括不使用arr[i]的货币,而只使用任意arr[0..i-1]货币的情况,所以dp[i][j]的值可能为dp[i-1][j]。
- 因为arr[i]只有一张不能重复使用,所以我们考虑dp[i-1][j-arr[i]]的值,这个值代表在可以任意使用arr[0..i-1]货币的情况下,组成j-arr[i]所需的最小张数。从钱数为j-arr[i]到钱数j,只用在加上这张arr[i]即可。所以dp[i][j]的值可能等于do[i-1][j-arr[i]]+1。
- 如果dp[i-1][j-arr[i]]中j-arr[i] < 0,也就是位置越界了,说明arr[i]太大了,只用一张就会超过钱数j,令dp[i][j]=dp[i-1][j]即可。
import java.util.*; public class Coin2 { public static void main(String args[]){ Scanner in=new Scanner(System.in); while(in.hasNext()){ int arr[]=new int[]{1,1,3,3,3,4,4,4}; int total=in.nextInt(); int dp[][]=new int[arr.length][total+1]; for(int i=0;i<arr.length;i++) dp[i][0]=0; //初始化 for(int j=0;j<=total;j++){ if(j==arr[0]){ dp[0][j]=1; }else{ dp[0][j]=100010; } } //更新策略 for(int i=1;i<arr.length;i++){ for(int j=1;j<=total;j++){ if(arr[i]>j){ dp[i][j]=dp[i-1][j]; }else{ dp[i][j]=Math.min(dp[i-1][j-arr[i]]+1,dp[i-1][j]); } } } if(dp[arr.length-1][total]>100000){ System.out.println("找不开"); }else{ System.out.println(dp[arr.length-1][total]); } } } }