经常在一家快餐店吃饭,店里的消费方式是微信小程序扫码充值然后消费。
今天我打完饭菜准备付钱,发现余额只剩 7块。 这时候我脑袋里蹦出一个问题,要是我突然不想在这吃了,我该如何用最少的充值消费次数把钱用完。大概想了一下
应该是充 50元,然后吃三顿 一荤一素19元。 7 + 50 - 19 - 19 -19 == 0
那要是我剩下的是其它金额呢,或者说以后我换了一家店 充值和消费金额都不同呢。我该如何用一个通用模型来解决这类问题。
听着挺扯淡的! 但是这在数学上应该是一个线性规划问题,要想明白还不简单。
这里我想的是如何通过编程来快速解决。那最容易想到的应该就是 dfs,直接模拟各种情况,然后返回成功的情况,直接coding。
public class EatingProblem {
public static void main(String[] args) {
new EatingProblem().minOperation(new int[]{10, 30, 50}, new int[]{19, 21, 25}, 7);
}
/**
* 充值金额数组
*/
int[] rechargeArr;
/**
* 消费金额数组
*/
int[] consumeArr;
/**
* 最少次操作
*
* @param rechargeArr 充值金额数组
* @param consumeArr 消费金额数组
* @param balance 余额
*/
private void minOperation(int[] rechargeArr, int[] consumeArr, int balance) {
this.rechargeArr = rechargeArr;
this.consumeArr = consumeArr;
dfs(balance, new StringBuilder());
}
/**
* @param balance 余额
* @param sb 记录操作步骤的字符串
*/
private void dfs(int balance, StringBuilder sb) {
if (balance < 0) {
return;
} else if (balance == 0) {
String last = String.format("--消费: %d, ^^^^余额: 0", balance);
System.out.println(sb + last);
return;
}
//充值
for (int cuReVal : rechargeArr) {
balance += cuReVal;
//当前操作步骤,用字符串记录
String cuStr = String.format("--充值: %d, 余额: %d ", cuReVal, balance);
sb.append(cuStr);
dfs(balance, sb);
sb.delete(sb.length() - cuStr.length(), sb.length());
balance -= cuReVal;
}
//消费
for (int cuConsVal : consumeArr) {
balance -= cuConsVal;
//当前操作步骤,用字符串记录
String cuStr = String.format("--消费: %d, 余额: %d ", cuConsVal, balance);
sb.append(cuStr);
dfs(balance, sb);
sb.delete(sb.length() - cuStr.length(), sb.length());
balance += cuConsVal;
}
}
}
于是乎吧,我就写了一个 dfs。虽然总感觉怪怪的,先跑一下吧!
E x c e p t i o n i n t h r e a d " m a i n " j a v a . l a n g . S t a c k O v e r f l o w E r r o r . . . \color{#FF3030}{Exception in thread "main" java.lang.StackOverflowError...} Exceptioninthread"main"java.lang.StackOverflowError...
直接爆栈了,因为除了余额(balance), 没有其它终止条件了,比如充值那就可以一直加一直加。
所以得加一个中止条件,结合我这题的目的,我准备加一个次数限制,规定多少次操作内消费完,大于这个次数直接中止。改进代码:
public class EatingProblem {
public static void main(String[] args) {
new EatingProblem().minOperation(new int[]{10, 30, 50}, new int[]{19, 21, 25}, 7, 5);
}
/**
* 充值金额数组
*/
int[] rechargeArr;
/**
* 消费金额数组
*/
int[] consumeArr;
/**
* 限制操作次数
*/
int limitTimes;
/**
* 最少次操作
*
* @param rechargeArr 充值金额数组
* @param consumeArr 消费金额数组
* @param balance 余额
* @param limitTimes 限制操作次数
*/
private void minOperation(int[] rechargeArr, int[] consumeArr, int balance, int limitTimes) {
this.limitTimes = limitTimes;
this.rechargeArr = rechargeArr;
this.consumeArr = consumeArr;
dfs(balance, new StringBuilder(), 0);
}
/**
* @param balance 余额
* @param sb 记录操作步骤的字符串
* @param times 实际操作次数
*/
private void dfs(int balance, StringBuilder sb, int times) {
if (times >= limitTimes) {
return;
}
if (balance < 0) {
return;
} else if (balance == 0) {
String last = String.format("--消费: %d, ^^^^余额: 0", balance);
System.out.println(sb + last);
return;
}
//充值
for (int cuReVal : rechargeArr) {
balance += cuReVal;
//当前操作步骤,用字符串记录
String cuStr = String.format("--充值: %d, 余额: %d ", cuReVal, balance);
sb.append(cuStr);
dfs(balance, sb, times + 1);
sb.delete(sb.length() - cuStr.length(), sb.length());
balance -= cuReVal;
}
//消费
for (int cuConsVal : consumeArr) {
balance -= cuConsVal;
//当前操作步骤,用字符串记录
String cuStr = String.format("--消费: %d, 余额: %d ", cuConsVal, balance);
sb.append(cuStr);
dfs(balance, sb, times + 1);
sb.delete(sb.length() - cuStr.length(), sb.length());
balance += cuConsVal;
}
}
}
这次看着没问题了,我把限制次数从1一直设置到5,终于有了结果,也就是最短4次操作后能将余额化为0。运行结果是:
–充值: 50, 余额: 57 --消费: 19, 余额: 38 --消费: 19, 余额: 19 --消费: 19, 余额: 0 --消费: 0, ^^^^余额: 0
符合预期,和我上面人脑相出的答案一样。
如果把限制次数往上加,比如设置为6,看看结果:
--充值: 10, 余额: 17 --充值: 50, 余额: 67 --消费: 21, 余额: 46 --消费: 21, 余额: 25 --消费: 25, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 10, 余额: 17 --充值: 50, 余额: 67 --消费: 21, 余额: 46 --消费: 25, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 10, 余额: 17 --充值: 50, 余额: 67 --消费: 25, 余额: 42 --消费: 21, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 30, 余额: 37 --充值: 30, 余额: 67 --消费: 21, 余额: 46 --消费: 21, 余额: 25 --消费: 25, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 30, 余额: 37 --充值: 30, 余额: 67 --消费: 21, 余额: 46 --消费: 25, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 30, 余额: 37 --充值: 30, 余额: 67 --消费: 25, 余额: 42 --消费: 21, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 30, 余额: 37 --消费: 21, 余额: 16 --充值: 30, 余额: 46 --消费: 21, 余额: 25 --消费: 25, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 30, 余额: 37 --消费: 21, 余额: 16 --充值: 30, 余额: 46 --消费: 25, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 30, 余额: 37 --消费: 25, 余额: 12 --充值: 30, 余额: 42 --消费: 21, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --充值: 10, 余额: 67 --消费: 21, 余额: 46 --消费: 21, 余额: 25 --消费: 25, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --充值: 10, 余额: 67 --消费: 21, 余额: 46 --消费: 25, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --充值: 10, 余额: 67 --消费: 25, 余额: 42 --消费: 21, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --消费: 19, 余额: 38 --消费: 19, 余额: 19 --消费: 19, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --消费: 21, 余额: 36 --充值: 10, 余额: 46 --消费: 21, 余额: 25 --消费: 25, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --消费: 21, 余额: 36 --充值: 10, 余额: 46 --消费: 25, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --消费: 21, 余额: 36 --消费: 21, 余额: 15 --充值: 10, 余额: 25 --消费: 25, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --消费: 21, 余额: 36 --消费: 25, 余额: 11 --充值: 10, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --消费: 25, 余额: 32 --充值: 10, 余额: 42 --消费: 21, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
--充值: 50, 余额: 57 --消费: 25, 余额: 32 --消费: 21, 余额: 11 --充值: 10, 余额: 21 --消费: 21, 余额: 0 --消费: 0, ^^^^余额: 0
那操作方式又多很多了,当然还可以将限制条件设置为最多充值不超过多少钱等。以及衍生到一些其它类似的线性规划问题。