第1章 游戏之乐——饮料供货
饮料供货
1. 问题描述
在微软亚洲研究院上班,大家早上来的第一件事是干啥呢?查看邮件?No,是去水房拿饮料:酸奶,豆浆,绿茶、王老吉、咖啡、可口可乐……(当然,还是有很多同事把拿饮料当做第二件事)。
管理水房的阿姨们每天都会准备很多的饮料给大家,为了提高服务质量,她们会统计大家对每种饮料的满意度。一段时间后,阿姨们已经有了大批的数据。某天早上,当实习生小飞第一个冲进水房并一次拿了五瓶酸奶、四瓶王老吉、三瓶鲜橙多时,阿姨们逮住了他,要他帮忙。
从阿姨们统计的数据中,小飞可以知道大家对每一种饮料的满意度。阿姨们还告诉小飞,STC(Smart Tea Corp.)负责给研究院供应饮料,每天总量为V。STC很神奇,他们提供的每种饮料之单个容量都是2的方幂,比如王老吉,都是23=8升的,可乐都是25=32升的。当然STC的存货也是有限的,这会是每种饮料购买量的上限。统计数据中用饮料名字、容量、数量、满意度描述每一种饮料。
那么,小飞如何完成这个任务,求出保证最大满意度的购买量呢?
我们先把这个问题“数学化”一下吧。
假设STC共提供n种饮料,用(Si、Vi、Ci、Hi、Bi)(对应的是饮料名字、容量、可能的最大数量、满意度、实际购买量)来表示第i种饮料(i = 0, 1,…, n-1),其中可能的最大数量指如果仅买某种饮料的最大可能数量。
【解法一】动态规划
饮料供货是一个求最优解问题。需要在给定最大容量V的前提下,从不同容量不同满意度的饮料中选择满意度最大的集合。
动态规划
动态规划是最常用的解决最优化问题的方法,很容易应用到本题的需求中。用opt[V,i]表示从第i,i+1,i+2,...,n-1种饮料中,算出总量为V的方案中满意度之和的最大值。
动态规划方程为:opt[V,i] = max{k*Hi + opt[V-k*Vi, i+1]}
根据以上的推到公式,就可以写出如下的动态规划求解代码,如下所示:
package chapter1youxizhileDrinkSupply; /** * 饮料供货【解法一】 * @author DELL * */ public class DrinkSupply1 { private int[] count; //对应饮料种类的最大可能数量 private int[] h; //对应各种饮料的满意度 private int[] V; //对应各种饮料的容量 private static final int INF = 1000000; //构造函数 public DrinkSupply1(int[] count, int[] h){ this.count = count; this.h = h; } /** * 计算所给方案的满意度之和的最大值 * @param v 饮料总容量 * @param T 饮料的种类数 * @return */ public int cal(int v, int T){ int i,j,k; //opt[v][i]表示从i,...,t种饮料中,算出总容量为v的方案的满意度之和的最大值 int[][] opt; opt = new int[v+1][T+1]; opt[0][T] = 0; //边界条件 for(i=1;i<=v;i++){ opt[i][T]=-INF; //边界条件 } for(j=T-1;j>=0;j--){ for(i=0;i<=v;i++){ opt[i][j] = -INF; for(k=0;k<=count[j];k++){ //遍历第j种饮料选取数量k if(i<=k*V[j]){ break; } int x = opt[i-k*V[j]][j+1]; if(x!=-INF){ x += k*h[j]; if(x > opt[i][j]){ opt[i][j]=x; } } } } } return opt[v][0]; } public static void main(String[] args){ } }
【解法二】备忘录法
package chapter1youxizhileDrinkSupply; /** * 饮料供货【解法二】 * 备忘录法 * @author DELL * */ public class DrinkSupply2 { private int[] count; //对应饮料种类的最大可能数量 private int[] h; //对应各种饮料的满意度 private int[] V; //对应各种饮料的容量 private static final int INF = 1000000; private static final int T = 100; //饮料的最大种类数 //构造函数 public DrinkSupply2(int[] count, int[] h){ this.count = count; this.h = h; } /** * 计算所给方案的满意度之和的最大值 * @param v 饮料总容量 * @param t 饮料的种类数 * @return */ public int cal(int v, int t){ int i,j; //opt[v][i]表示从i,...,t种饮料中,算出总容量为v的方案的满意度之和的最大值 int[][] opt; //子问题的记录项表,初始化时opt中存储值为-1,表示该子问题尚未求解 opt = new int[v+1][T+1]; for(i=0;i<=v;i++){ //初始化opt for(j=0;j<=t;j++){ opt[i][j] = -1; } } if(t == T){ if(v == 0) return 0; else return -INF; } if(v < 0) return -INF; else if(v == 0) return 0; else if(opt[v][t]!=-1){ //该子问题已求解,直接返回子问题的解 return opt[v][t]; } //子问题尚未求解,则求解子问题 int result = -INF; for(i=0;i<count[t];i++){ int temp = cal(v-i*V[t],t+1); if(temp != INF){ temp += i*h[t]; if(temp>result) result = temp; } } return opt[v][t] = result; } public static void main(String[] args) { // TODO Auto-generated method stub } }
【解法三】 贪心算法
实现代码如下:
package chapter1youxizhileDrinkSupply; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; /** * 饮料供货【解法三】 * 贪心算法 * @author DELL * */ public class DrinkSupply3 { private List<Map<Integer,List<Integer>>> listMap; public DrinkSupply3(){ listMap = new ArrayList<Map<Integer, List<Integer>>>(); System.out.println("请输入饮料容量及该容量饮料的各个满意度:"); int v; //临时存储获取的容量 String[] s;//临时存放获取的满意度 String flag;//判断是否继续输入 do{ System.out.print("请输入饮料容量,按增序输入:"); Scanner input1 = new Scanner(System.in); v = input1.nextInt(); System.out.print("请输入该容量的各个满意度(以逗号分隔):"); Scanner input2 = new Scanner(System.in); s = input2.nextLine().split(","); List<Integer> list = new ArrayList<Integer>(); //存放每种容量饮料的满意度 for(int i=0;i<s.length;i++){ list.add(Integer.parseInt(s[i])); } Map<Integer,List<Integer>> map = new HashMap<Integer, List<Integer>>();//容量与满意度对应 map.put(v, list); listMap.add(map); System.out.print("是否继续(y/n):"); Scanner input3 = new Scanner(System.in); flag = input3.next(); }while(flag.equalsIgnoreCase("y")); mapArr = new Map[listMap.size()]; max = new int[listMap.size()]; //用于存放买各种容量的饮料的各种组合最大满意度 list = new ArrayList<Integer>(); //存放每种容量饮料的满意度 } /** * 根据容量获取2的幂指数,向下取整 * @param V 饮料容量 */ private int getPower(int V){ int i = 0; while(Math.pow(2, i)<=V){ i++; } return i-1; } /** * 获取一组满意度中的最大值 * @param list 满意度列表 * @return 最大满意度 */ private int maxH(List<Integer> list){ Integer a[] = new Integer[list.size()]; list.toArray(a); int max,i; max = a[0]; for(i=1;i<a.length;i++){ if(a[i]>max) max = a[i]; } return max; } /** * 计算最大满意度 * @param v 饮料的总容量单位L * @return 最大满意度 */ Map<Integer,List<Integer>> mapArr[]; int max[]; List<Integer> list; public int cal(int v){ listMap.toArray(mapArr); if(v == 1){ list = mapArr[0].get(v); return maxH(list); } if(v == 2){ list = mapArr[1].get(v); int max2 = maxH(list); list = mapArr[0].get(1); int max1 = maxH(list); if(max2>=max1*2){ return max2; }else return max1*2; } //用于计算存放买各种容量的饮料的各种组合最大满意度 for(int i=0;i<listMap.size();i++){ max[i]=maxH(mapArr[i].get((int)Math.pow(2, i))); for(int j=0;j<i;j++){ if(Math.pow(2, i-j)*max[j]>max[i]) max[i]=(int) (Math.pow(2, i-j)*max[j]); } } int i = mapArr.length-1; //取得容量最大的饮料的指数 int maxAll; //最终结果 int j; if(v>=Math.pow(2, i)){ j = i; }else{ j = getPower(v); } int n = (int) Math.pow(2, j); if(v%n==0){ maxAll = v/n*max[j]; //整除,直接计算 }else if(v%n-getPower(v%n)==0){ maxAll = v/n*max[j]+max[getPower(v%n)]; //是2的整次幂 }else{ maxAll = v/n*max[j]+cal(v%n); } return maxAll; } public static void main(String[] args) { DrinkSupply3 ds = new DrinkSupply3(); System.out.println(ds.cal(5)); } }
程序运行结果如下:
请输入饮料容量及该容量饮料的各个满意度: 请输入饮料容量,按增序输入:1 请输入该容量的各个满意度(以逗号分隔):4,5 是否继续(y/n):y 请输入饮料容量,按增序输入:2 请输入该容量的各个满意度(以逗号分隔):6 是否继续(y/n):y 请输入饮料容量,按增序输入:4 请输入该容量的各个满意度(以逗号分隔):10 是否继续(y/n):n 20 请输入饮料容量及该容量饮料的各个满意度: 请输入饮料容量,按增序输入:1 请输入该容量的各个满意度(以逗号分隔):4,5 是否继续(y/n):y 请输入饮料容量,按增序输入:2 请输入该容量的各个满意度(以逗号分隔):6 是否继续(y/n):y 请输入饮料容量,按增序输入:4 请输入该容量的各个满意度(以逗号分隔):10 是否继续(y/n):n 25