【问题标题】:What is the efficient way to implement universal ATM algorithm? [duplicate]实现通用ATM算法的有效方法是什么? [复制]
【发布时间】:2017-11-21 12:45:26
【问题描述】:

上周我参加了面试,我被要求为通用 ATM 算法编写代码。

所以我必须实现 ATM 功能,它可以获取所有可用纸币及其限额和所需金额,并返回任何可能的方式,将给定纸币中的这些金额与其限额相结合。

ATM 函数实现的第一次迭代非常简单,因为限制看起来像

{
    1000: 20,
    500: 430,
    100: 300,
    50: 23,
    10: 23
}

但在最后一次迭代的任务限制看起来像

{
    1000: 20,
    500: 430,
    100: 300,
    50: 23,
    30: 23
}

我想,我成功地实现了这个算法的工作版本,但我对它的最终复杂性不满意,我觉得应该有更有效的东西。

我的决定如下:

  1. 为给定货币创建所有可能的子集,因此 [100, 50, 30] set 将被转换为 7 个子集:[[100], [50], [30], [100, 50], [100, 30], [50, 30], [100, 50, 30]]。
  2. 尝试在每个子集中形成所需的金额,所以如果我必须处理 有 180 的数量,我会尝试形成这个数量

    • 仅使用 100 张钞票,此尝试失败
    • 仅使用 50 张纸币,此尝试失败
    • 仅使用 30 张钞票,此尝试成功,结果类似于 {30:6}
    • 使用 50 和 30 张纸币,此尝试成功,结果类似于 {50:3, 30:1}
    • 对于 100 和 50 纸币,此尝试失败
    • 对于 100 和 30 纸币,此尝试失败
    • 对于 100、50 和 30 张纸币,此尝试成功,结果类似于 {100:1,50:1,30:1}
  3. 从成功的变体中选择任何我喜欢的并作为结果返回。

我在 JavaScript 中的决定的工作代码如下:

function getAllSubsetsInSet(set) {
    const result = [];
    let mask = 1;

    do {
        let maskString = mask.toString(2).padStart(set.length, '0');
        result.push(set.filter((item, index) => maskString[index] === '1'));
        mask++;
    }while (mask < (2 ** set.length));

    return result;
}

function getMoney(currencies, limits, amount) {
    const sorted = currencies.sort((a, b) => b - a);

    let workingLimits = {
        ...limits
    };
    let workingAmount = amount;
    let result = {};

    for (let i = 0; i < sorted.length; i++) {
        let currentCurrency = sorted[i];
        let desiredBanknotes = Math.floor(workingAmount / currentCurrency);
        let availableBanknotes = workingLimits[currentCurrency];
        let banknotesToBeUsed = (availableBanknotes < desiredBanknotes) ? availableBanknotes : desiredBanknotes;

        workingAmount = (workingAmount - (banknotesToBeUsed * currentCurrency));
        workingLimits[currentCurrency] = availableBanknotes - banknotesToBeUsed;
        result[currentCurrency] = banknotesToBeUsed;
    }

    if (workingAmount > 0) {
        return {
            result: {},
            limits,
            error: true
        }
    }

    return {
        result: result,
        limits: workingLimits,
        error: false
    }
}

function ATM(limits, amount) {
    let currencies = Object.keys(limits).map(item => Number(item));
    let allCurrencyCombinations = getAllSubsetsInSet(currencies);
    let resultsForEachCombination = allCurrencyCombinations.map(combination => {
        return getMoney(combination, limits, amount);
    });

    const succeedResults = resultsForEachCombination.filter(variant => !variant.error);
    if (succeedResults.length) {
        return succeedResults;
    }

    return {
        result: 'No possible ways',
        limits
    }
}

console.log(ATM(
    {
        1000: 20,
        500: 430,
        100: 300,
        50: 23,
        30: 90
    },
    180
));

但是请,谁能帮助我正确有效的方式来实现这个逻辑?任何编程语言和伪代码都可以。

谢谢!

【问题讨论】:

  • 这两个列表的限制不是完全一样吗?另外,您必须返回所有种可能的组合,还是只返回一个
  • @tobias_k 不,10 面额更改为 30 面额,不再划分其他面额。因此,它将打破天真的贪心算法。
  • 自从您发布了这个问题以来,您似乎错过了一个关键的搜索词。尝试“总和到目标值”。这个问题已经在网上用多种语言解决了很多次了。
  • @Prune,谢谢你的搜索词,我什至不知道这样的词组合。我会尽快学习,如果这解决了我的问题,我会回答。

标签: algorithm performance


【解决方案1】:

有一些更有效的东西。只需编写一个递归算法,首先尝试尽可能多的大钞票的贪婪行为。如果有答案,通常会很快找到。但失败会很慢。

在一次采访中,我只会在性能说明中给出这个答案,并表明我知道如果需要的话我知道如何加快速度。

如果他们问如何加快速度,您首先要寻找能够将大纸币视为小纸币的任何机会,因为有足够的小纸币可以将大纸币分开,从而可以制作小纸币的任意倍数.一旦您解决了面额较少的版本,您就可以通过贪婪的方式逆向计算您实际分发的钞票。

在您的第一个案例中,您的筹码量减少到 10 个,答案是立竿见影的。不需要递归。

第二种情况更有趣。当您开始时,有21 * 431 * 301 * 24 * 24 = 1_569_226_176 可能需要考虑的注释组合。 (请注意,每个因素都有一个 +1,因为您可以有 0 到 n。另请注意,您可以修剪搜索空间以避免考虑很多因素。但数量会很大。)

但是你可以在心里用 2500 个音符代替 1000 个音符,因为你有一个 500 个音符。然后用 100 个音符替换 500 个音符,因为您有 4 100 个音符。 (注意,可整除的 -1 是因为您不需要能够用 100 个音符产生 500。只需值 0、100、200、300 和 400。)同样是 100 个音符和 50 个音符。但后来我们停止了,因为 30 个音符不能除以 50 个音符。这转

{
    1000: 20,
    500: 430,
    100: 300,
    50: 23,
    30: 23
}

改变:

{
    50: 5323,
    30: 23
}

这会更快,因为需要考虑的组合更少。仅在最坏的情况下5324 * 24 = 127_776 但它仍然没有我们能得到的那么快。

最终的性能增强是用几张小钞票和一堆大钞票代替成堆的小钞票。在这种情况下,30 和 50 的最小公倍数是 150。所以我们实际上可以将其视为一些杂散音符来进行最终更改,以及一整叠 150 个音符。

{
    150: 178, // 174 from 50s, 4 from 30s
    50: 1,
    30: 3
}

现在我们确实需要做很少的工作来确定我们是否可以做出改变。 (一个简单的蛮力的原始可能性下降到179 * 2 * 4 = 1432。如果你在这一点上有点聪明,你只需要看看其中的8个。)一旦我们有了改变,我们只需将过程反转为回到我们拥有的实际面额。

而且面额的优化版本总是能很快找到答案,我们可以从中逆向寻找实际的钞票。

【讨论】:

  • 如果所有的面额都是相对质数,这效果如何?
  • @Prune 在最坏的情况下,面额数量会出现组合爆炸,但纸币数量将呈线性增长。无法回避的事实是,如果您有大量的面额,您是否可以进行更改是一个NP完全问题。
  • 如果你有很多面额和相对较少的钞票,那么纯动态规划解决方案更快。如果你有很多账单,那么这应该比动态编程更快。
  • 我真的不明白将大笔记分成小笔记的意义。我会从尽可能大的音符开始,作为一个片段,然后递归,如果剩余的音符不合适,则回溯。
  • @btilly 我知道。这是一个主要问题,但您有权澄清。由于这是一个面试问题,我预计考官会提高标准,直到你达到 NP 或你的眼睛转回你的脑海。
猜你喜欢
  • 2017-03-12
  • 1970-01-01
  • 2011-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-21
  • 1970-01-01
相关资源
最近更新 更多