1、问题描述
因为我的公司每个月给员工会有一定的交通费额度,需要拿发票去报销才能获得的。要求的是发票总金额不能大于报销的额度。因此在实际报销的时候,你要一张张发票去排列组合经可能的把报销金额往报销额度那里去凑。比如你有1000元额度,那么你报销的时候就要尽可能的把你要报销的发票往1000元去凑,因为发票金额不能大于报销额度。
2、解决方法
其实就是一个求限定条件下最优解的问题,如果人工手动去凑发票费时又费力,而且还不能保证你凑的一定是最优解。所以就写个程序帮助求最优解的排列组合。
3、代码
3.1、列出所有的组合,并一一计算这些组合的和并保存不大于给定值的组合
package Algorithm;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import Algorithm.MyInvoice.Compose;
public class MyInvoiceV3 {
static List<Compose> composeList = new ArrayList();
//和不能超过的最大金额
static double SUM = 1000;
class Compose {
List<Double> list;
Double sum;
Compose(List<Double> list, Double sum1) {
this.list = list;
this.sum = sum1;
}
Double getSum() {
return this.sum;
}
public String toString() {
// 把double转成带2个小数点的十进制数
return list.toString() + ":" + new BigDecimal(sum).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
public static void main(String[] args) {
// 一组发票金额
String[] array = new String[] { "10.1", "200.14", "300", "450.91", "556.5", "226", "10.1", "200.14", "300",
"130" };
for (int i = 0; i < array.length; i++) {
combinationSelect(array, i);
}
for (Compose c : composeList.stream().sorted(Comparator.comparing(Compose::getSum).reversed())
.collect(Collectors.toList())) {
System.out.println(c);
}
}
/**
* 组合选择(从列表中选择n个组合)
*
* @param dataList 待选列表
* @param n 选择个数
*/
public static void combinationSelect(String[] dataList, int n) {
// System.out.println(String.format("C(%d, %d) = %d", dataList.length, n,
// combination(dataList.length, n)));
combinationSelect(dataList, 0, new String[n], 0);
}
/**
* 计算组合数,即C(n, m) = n!/((n-m)! * m!)
*
* @param n
* @param m
* @return
*/
public static long combination(int n, int m) {
return (n >= m) ? factorial(n) / factorial(n - m) / factorial(m) : 0;
}
/**
* 计算阶乘数,即n! = n * (n-1) * ... * 2 * 1
*
* @param n
* @return
*/
private static long factorial(int n) {
return (n > 1) ? n * factorial(n - 1) : 1;
}
/**
* 组合选择
*
* @param dataList 待选列表
* @param dataIndex 待选开始索引
* @param resultList 前面(resultIndex-1)个的组合结果
* @param resultIndex 选择索引,从0开始
*/
private static void combinationSelect(String[] dataList, int dataIndex, String[] resultList, int resultIndex) {
int resultLen = resultList.length;
int resultCount = resultIndex + 1;
if (resultCount > resultLen) { // 全部选择完时,输出组合结果
// System.out.println(Arrays.asList(resultList));
List<Double> resultList1 = new ArrayList<>();
for (int i = 0; i < resultList.length; i++) {
resultList1.add(Double.parseDouble(resultList[i]));
}
double sum1 = resultList1.stream().mapToDouble(Double::doubleValue).sum();
// System.out.println("sum1="+sum1);
if (sum1 <= SUM) {
Compose compose = new MyInvoiceV3().new Compose(resultList1, sum1);
composeList.add(compose);
}
return;
}
// 递归选择下一个
for (int i = dataIndex; i < dataList.length + resultCount - resultLen; i++) {
resultList[resultIndex] = dataList[i];
combinationSelect(dataList, i + 1, resultList, resultIndex + 1);
}
}
}
这个方法我是直接用别人写的用Java实现排列、组合算法_codertcm的博客-CSDN博客
自己只是针对我的需求做了一些小修改。
3.2、用二进制罗列所有可能的数字组合并一一计算出它们的和
package Algorithm;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class MyInvoiceV2 {
class Compose {
List<Double> list;
Double sum;
Compose(List<Double> list, Double sum1) {
this.list = list;
this.sum = sum1;
}
Double getSum() {
return this.sum;
}
public String toString() {
// 把double转成带2个小数点的十进制数
return list.toString() + ":" + new BigDecimal(sum).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
public static void main(String[] args) {
//一组发票的金额
double[] data = new double[] { 10.1, 200.14, 300, 450.91, 556.5, 226, 10.1, 200.14, 300, 450.91 };
//代码里的1000表示和不能超过的最大金额
for (Compose c : binaryCal(data, 1000).stream().sorted(Comparator.comparing(Compose::getSum).reversed())
.collect(Collectors.toList())) {
System.out.println(c);
}
}
public static List<Compose> binaryCal(double[] a, double m) {
MyInvoiceV2 t = new MyInvoiceV2();
List<Compose> composeList = new ArrayList<Compose>();
int n = a.length;
// 最多有2的n次方种组合
int max = 1 << n;
for (int i = 1; i < max; i++) {
// 转为二进制
String binaryNum = Integer.toBinaryString(i);
// 转成相同的位数,不足n位的在前补0
// 用二进制表示所有可能的组合
binaryNum = toSameLen(binaryNum, n);
char[] bitNum = binaryNum.toCharArray();
List<Double> sumList = new ArrayList<Double>();
double sum = 0;
for (int j = 0; j < bitNum.length; j++) {
// 二进制数当前位置为1,则加起来
if (bitNum[j] == '1') {
sumList.add(a[j]);
sum += a[j];
}
}
// 和小于等于m了,则记录下来
if (sum <= m) {
Compose compose = t.new Compose(sumList, sum);
composeList.add(compose);
}
}
return composeList;
}
private static String toSameLen(String binaryNum, int len) {
// 数的长度
int numLen = binaryNum.length();
if (numLen == len) {
return binaryNum;
}
StringBuilder sb = new StringBuilder();
// 差几位补几个0
for (int i = 0; i < len - numLen; i++) {
sb.append(0);
}
return sb.append(binaryNum).toString();
}
}
我觉得这种方法很巧妙,是用了这篇文章作者的代码Java笔试题:在一个数组中,求出所有和为m的组合_Listener10的博客-CSDN博客
用了二进制来罗列所有的可能组合。虽然也是穷举,但我没有想到。
4、总结
无
5、参考资料
java排列组合(递归算法)_Ansel_TbN1的博客-CSDN博客_递归法求排列组合公式
Java笔试题:在一个数组中,求出所有和为m的组合_Listener10的博客-CSDN博客
用Java实现排列、组合算法_codertcm的博客-CSDN博客