实现通用ATM算法的有效方法是什么?

时间:2017-11-21 12:45:27

标签: algorithm performance

上周我参加了面试,我被要求为通用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. 从我喜欢的成功变体中选择并将其作为结果返回。

  4. 我在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
    ));
    

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

    谢谢!

1 个答案:

答案 0 :(得分: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个。)一旦我们改变了,我们只是扭转过程以回到我们所拥有的实际面额。

这种优化版本的面额总能快速找到答案,我们可以从中反复查找实际账单。